From c2de1b8748406a534c459fc67839f49c87015bb4 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 6 Nov 2022 17:52:48 +0800 Subject: [PATCH 01/40] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=20MornyConfig?= =?UTF-8?q?=20=E4=BB=A5=E5=8F=8A=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=9F=BA=E4=BA=8E=20MornyConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 6 +- .../cono/morny/GradleProjectConfigures.java | 6 +- .../cc/sukazyo/cono/morny/MornyCoeur.java | 101 +++++-------- .../cc/sukazyo/cono/morny/MornyConfig.java | 137 ++++++++++++++++++ .../cc/sukazyo/cono/morny/MornyTrusted.java | 35 ++--- .../cc/sukazyo/cono/morny/ServerMain.java | 70 ++++----- .../cono/morny/bot/event/OnCallMe.java | 7 +- .../event/OnUpdateTimestampOffsetLock.java | 4 +- 8 files changed, 225 insertions(+), 141 deletions(-) create mode 100644 src/main/java/cc/sukazyo/cono/morny/MornyConfig.java diff --git a/gradle.properties b/gradle.properties index 338d93f..0684f5b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,12 @@ ## Core -VERSION = 0.8.0.11 +VERSION = 1.0.0-alpha1 -CODENAME = putian +CODENAME = beiping # dependencies -libSpotbugsVersion = 4.7.2 +libSpotbugsVersion = 4.7.3 libMessivaVersion = 0.1.0.1 diff --git a/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java b/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java index 8735b7d..459f791 100644 --- a/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java +++ b/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java @@ -4,7 +4,7 @@ package cc.sukazyo.cono.morny; * the final field that will be updated by gradle automatically. */ public class GradleProjectConfigures { - public static final String VERSION = "0.8.0.11"; - public static final String CODENAME = "putian"; - public static final long COMPILE_TIMESTAMP = 1667376095614L; + public static final String VERSION = "1.0.0-alpha1"; + public static final String CODENAME = "beiping"; + public static final long COMPILE_TIMESTAMP = 1667581079623L; } diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java b/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java index 9c0cee9..452d0e4 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java @@ -14,7 +14,6 @@ import com.pengrad.telegrambot.request.GetMe; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Set; import static cc.sukazyo.cono.morny.Log.logger; @@ -27,6 +26,9 @@ public class MornyCoeur { /** 当前程序的 Morny Coeur 实例 */ private static MornyCoeur INSTANCE; + /** 当前 Morny 的启动配置 */ + public final MornyConfig config; + /** 当前 Morny 的{@link MornyTrusted 信任验证机}实例 */ private final MornyTrusted trusted; /** 当前 Morny 的 telegram 命令管理器 */ @@ -36,7 +38,6 @@ public class MornyCoeur { /** morny 的 bot 账户 */ private final TelegramBot account; private final ExtraAction extraActionInstance; - private final boolean isRemoveCommandListWhenExit; /** * morny 的 bot 账户的用户名
*
@@ -53,58 +54,36 @@ public class MornyCoeur { * 这个字段将会在登陆成功后赋值为登录到的 bot 的 id。 */ public final long userid; - /** - * morny 的事件忽略前缀时间
- *
- * {@link cc.sukazyo.cono.morny.bot.event.OnUpdateTimestampOffsetLock} - * 会根据这里定义的时间戳取消掉比此时间更早的事件链 - */ - public final long latestEventTimestamp; /** * morny 主程序启动时间
* 用于统计数据 */ - public static final long coeurStartTimestamp = System.currentTimeMillis(); - - public static final long DINNER_CHAT_ID = -1001707106392L; + public static final long coeurStartTimestamp = ServerMain.systemStartupTime; private record LogInResult(TelegramBot account, String username, long userid) { } /** * 执行 bot 初始化 * - * @param botKey bot 的 telegram bot api token - * @param botUsername bot 的 username 限定。如果为 null 则表示不限定, - * 如果指定,则登录时会检查所登陆的 bot 的用户名是否与此相等 - * @param master morny 实例所信任的主人的 id。用于初始化 {@link #trusted} - * @param trustedChat morny 实例所信任的群组的 id。用于初始化 {@link #trusted} - * @param latestEventTimestamp 事件处理器会处理事件的最早时间戳 —— - * 只有限定的 message 事件会受此影响。 - * 单位为毫秒 + * @param config Morny 实例的配置选项数据 */ - private MornyCoeur ( - @Nullable String botApi, @Nullable String botApi4File, - @Nonnull String botKey, @Nullable String botUsername, - long master, long trustedChat, Set trustedRDinner, - long latestEventTimestamp, - boolean isRemoveCommandListWhenExit - ) { + private MornyCoeur (MornyConfig config) { + + this.config = config; - this.latestEventTimestamp = latestEventTimestamp; - this.isRemoveCommandListWhenExit = isRemoveCommandListWhenExit; configureSafeExit(); - logger.info("args key:\n " + botKey); - if (botUsername != null) { - logger.info("login as:\n " + botUsername); + logger.info("args key:\n " + config.telegramBotKey); + if (config.telegramBotUsername != null) { + logger.info("login as:\n " + config.telegramBotUsername); } try { - final LogInResult loginResult = login(botApi, botApi4File, botKey, botUsername); + final LogInResult loginResult = login(config.telegramBotApiServer, config.telegramBotApiServer4File, config.telegramBotKey, config.telegramBotUsername); this.account = loginResult.account; this.username = loginResult.username; this.userid = loginResult.userid; - this.trusted = new MornyTrusted(master, trustedChat, trustedRDinner); + this.trusted = new MornyTrusted(this); StringBuilder trustedReadersDinnerIds = new StringBuilder(); trusted.getTrustedReadersOfDinnerSet().forEach(id -> trustedReadersDinnerIds.append("\n ").append(id)); logger.info(String.format(""" @@ -114,7 +93,7 @@ public class MornyCoeur { - trusted chat (id) %d - trusted reader-of-dinner (id)%s""", - master, trustedChat, trustedReadersDinnerIds + config.trustedMaster, config.trustedChat, trustedReadersDinnerIds )); } catch (Exception e) { @@ -135,32 +114,28 @@ public class MornyCoeur { * 如果 morny 已经初始化,则不会进行初始化,抛出错误消息并直接退出方法。 * * @see #MornyCoeur 程序初始化方法 + * @param config morny 实例的配置选项数据 */ - public static void main ( - @Nullable String botApi, @Nullable String botApi4File, - @Nonnull String botKey, @Nullable String botUsername, - long master, long trustedChat, Set trustedRDinner, long latestEventTimestamp, - boolean isAutomaticResetCommandList, boolean isRemoveCommandListWhenExit - ) { + public static void main (MornyConfig config) { if (INSTANCE == null) { + logger.info("Coeur Starting"); - INSTANCE = new MornyCoeur( - botApi, botApi4File, - botKey, botUsername, - master, trustedChat, trustedRDinner, - latestEventTimestamp, - isRemoveCommandListWhenExit - ); + INSTANCE = new MornyCoeur(config); + MornyDaemons.start(); + logger.info("start telegram events listening"); EventListeners.registerAllListeners(); INSTANCE.account.setUpdatesListener(OnUpdate::onNormalUpdate); - if (isAutomaticResetCommandList) { + + if (config.commandLoginRefresh) { logger.info("resetting telegram command list"); commandManager().automaticUpdateList(); } + logger.info("Coeur start complete"); return; + } logger.error("Coeur already started!!!"); } @@ -179,7 +154,7 @@ public class MornyCoeur { private void exitCleanup () { logger.info("clean:save tracker data."); MornyDaemons.stop(); - if (isRemoveCommandListWhenExit) { + if (config.commandLogoutClear) { commandManager.automaticRemoveList(); } } @@ -192,16 +167,20 @@ public class MornyCoeur { } /** - * 登录 bot
- *
+ * 登录 bot. + *

* 会反复尝试三次进行登录。如果登录失败,则会直接抛出 RuntimeException 结束处理。 * 会通过 GetMe 动作验证是否连接上了 telegram api 服务器, * 同时也要求登录获得的 username 和 {@link #username} 声明值相等 * - * @param api bot client 将会连接到的 telegram bot api 位置 - * @param api4File bot client 将会连接到的 telegram file api 位置,如果不指定则会跟随 {@code api} 选项的设定 - * @param key bot 的 api-token - * @param requireName 要求登录到的需要的 username,如果登陆后的 username 与此不同则会报错退出 + * @param api bot client 将会连接到的 telegram bot api 位置。 + * 填入 {@code null} 则使用默认的 {@code "https://api.telegram.org/bot"} + * @param api4File bot client 将会连接到的 telegram file api 位置。 + * 如果传入 {@code null} 则会跟随 {@param api} 选项的设定(具体为在 {@param api} 路径的后面添加 {@code /file} 路径)。 + * 如果两者都为 {@code null},则跟随默认的 {@value FileApi#FILE_API} + * @param key bot 的 api-token. 必要值 + * @param requireName 要求登录到的需要的 username,如果登陆后的 username 与此不同则会报错退出。 + * 填入 {@code null} 则表示不对 username 作要求 * @return 成功登录后的 {@link TelegramBot} 对象 */ @Nonnull @@ -281,13 +260,13 @@ public class MornyCoeur { } /** + * 获取当前 morny 的配置数据 * - * 获取忽略时间点 - * - * @return {@link #latestEventTimestamp MornyCoeur.latestEventTimestamp} + * @return {@link #config MornyCoeur.config} */ - public static long getLatestEventTimestamp () { - return INSTANCE.latestEventTimestamp; + @Nonnull + public static MornyConfig config () { + return INSTANCE.config; } /** diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java new file mode 100644 index 0000000..004302c --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java @@ -0,0 +1,137 @@ +package cc.sukazyo.cono.morny; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashSet; +import java.util.Set; + +public class MornyConfig { + + /* ======================================= * + * Config props Names Definition * + * ======================================= */ + + public static final String PROP_TOKEN_KEY_DEFAULT = "TELEGRAM_BOT_API_TOKEN"; + public static final String PROP_TOKEN_MORNY_KEY = "MORNY_TG_TOKEN"; + public static final String[] PROP_TOKEN_KEY = {PROP_TOKEN_KEY_DEFAULT, PROP_TOKEN_MORNY_KEY}; + + /* ======================================= * + * telegram bot login config * + * ======================================= */ + + /** + * Morny Telegram 使用的 API 服务器. + *

+ * 不设定的话,默认将会使用 {@code https://api.telegram.org/bot} + */ + @Nullable public final String telegramBotApiServer; + /** + * Morny Telegram 使用的 API 服务器的 file 服务路径. + *

+ * 不设定的话,默认将会使用 {@value com.pengrad.telegrambot.impl.FileApi#FILE_API} + */ + @Nullable public final String telegramBotApiServer4File; + + /** + * morny 使用的 telegram bot 的 bot api token. + *

+ * 这个值必须设定。 + */ + @Nonnull public final String telegramBotKey; + /** + * morny 所使用的 bot 的 username. + *

+ * 如果设定了这个值,则在 morny 登录 bot 时将会检查所登录的 bot 的 username 是否和这里设定的 username 匹配。 + * 如果不匹配,则会拒绝登录然后报错。 + *

+ * 如果没有设定这个值,则不会对登录 bot 的 username 进行限制。 + */ + @Nullable public final String telegramBotUsername; + + /* ======================================= * + * morny trusted config * + * ======================================= */ + + /** + * morny 的主人. + *

+ * 这项值的对象总是会被{@link MornyTrusted 信任管理器}认为是可信任的 + */ + public final long trustedMaster; + /** + * morny 可信群聊的 id. + *

+ * {@link MornyTrusted 信任管理器}将会认为这个群聊中的所有拥有 + * {@link com.pengrad.telegrambot.model.ChatMember.Status#administrator administrator} 权限的成员是可信任的。 + *

+ * id 需要符合 bot api 标准。 + */ + public final long trustedChat; + + /* ======================================= * + * system: event ignore * + * ======================================= */ + + public final boolean eventIgnoreOutdated; + /** + * morny 的事件忽略前缀时间
+ *
+ * {@link cc.sukazyo.cono.morny.bot.event.OnUpdateTimestampOffsetLock} + * 会根据这里定义的时间戳取消掉比此时间更早的事件链 + */ + public final long eventOutdatedTimestamp; + + /* ======================================= * + * system: command list automation * + * ======================================= */ + + public final boolean commandLoginRefresh; + public final boolean commandLogoutClear; + + /* ======================================= * + * function: dinner query tool * + * ======================================= */ + + @Nonnull public final Set dinnerTrustedReaders; + public final long dinnerChatId; + + public MornyConfig (@Nonnull Prototype prototype) throws CheckFailure { + this.telegramBotApiServer = prototype.telegramBotApiServer; + this.telegramBotApiServer4File = prototype.telegramBotApiServer4File; + if (prototype.telegramBotKey == null) throw new CheckFailure.NullTelegramBotKey(); + this.telegramBotKey = prototype.telegramBotKey; + this.telegramBotUsername = prototype.telegramBotUsername; + this.trustedMaster = prototype.trustedMaster; + this.trustedChat = prototype.trustedChat; + this.eventIgnoreOutdated = prototype.eventIgnoreOutdated; + if (prototype.eventOutdatedTimestamp < 1) throw new CheckFailure.UnsetEventOutdatedTimestamp(); + this.eventOutdatedTimestamp = prototype.eventOutdatedTimestamp; + this.commandLoginRefresh = prototype.commandLoginRefresh; + this.commandLogoutClear = prototype.commandLogoutClear; + this.dinnerTrustedReaders = prototype.dinnerTrustedReaders; + this.dinnerChatId = prototype.dinnerChatId; + } + + public static class CheckFailure extends Exception { + public static class NullTelegramBotKey extends CheckFailure {} + public static class UnsetEventOutdatedTimestamp extends CheckFailure {} + } + + public static class Prototype { + + @Nullable public String telegramBotApiServer = null; + @Nullable public String telegramBotApiServer4File = null; + @Nullable public String telegramBotKey = null; + @Nullable public String telegramBotUsername = null; + public long trustedMaster = 793274677L; + public long trustedChat = -1001541451710L; + public boolean eventIgnoreOutdated = false; + public long eventOutdatedTimestamp = -1; + public boolean commandLoginRefresh = false; + public boolean commandLogoutClear = false; + @Nonnull public Set dinnerTrustedReaders = new HashSet<>(); + public long dinnerChatId = -1001707106392L; + + } + +} diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java b/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java index ba93789..ae2b3bb 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java @@ -1,7 +1,7 @@ package cc.sukazyo.cono.morny; import com.pengrad.telegrambot.model.ChatMember.Status; -import java.util.HashSet; + import java.util.Set; /** @@ -9,27 +9,10 @@ import java.util.Set; */ public class MornyTrusted { - /** - * 群聊id,其指向的群聊指示了哪个群的成员是受信任的 - * @see #isTrusted(long) 受信检查 - */ - public final Long TRUSTED_CHAT_ID; + private final MornyCoeur instance; - /** - * morny 的主人
- * 这项值的对象总是会被认为是可信任的 - */ - public final long MASTER; - - private final Set TRUSTED_READERS_OF_DINNER; - - public MornyTrusted (long master, long trustedChatId, Set trustedRDinner) { - this.TRUSTED_CHAT_ID = trustedChatId; - this.MASTER = master; - this.TRUSTED_READERS_OF_DINNER = new HashSet<>(){{ - this.add(master); - this.addAll(trustedRDinner); - }}; + public MornyTrusted (MornyCoeur instance) { + this.instance = instance; } /** @@ -37,22 +20,22 @@ public class MornyTrusted { *
* 用户需要受信任才能执行一些对程序甚至是宿主环境而言危险的操作,例如关闭程序
*
- * 它的逻辑(目前)是检查群聊 {@link #TRUSTED_CHAT_ID} 中这个用户是否为群组管理员 + * 它的逻辑(目前)是检查群聊 {@link MornyConfig#trustedChat} 中这个用户是否为群组管理员 * * @param userId 需要检查的用户的id * @return 所传递的用户id对应的用户是否受信任 */ public boolean isTrusted (long userId) { - if (userId == MASTER) return true; - return MornyCoeur.extra().isUserInGroup(userId, TRUSTED_CHAT_ID, Status.administrator); + if (userId == instance.config.trustedMaster) return true; + return MornyCoeur.extra().isUserInGroup(userId, instance.config.trustedChat, Status.administrator); } public boolean isTrustedForDinnerRead (long userId) { - return TRUSTED_READERS_OF_DINNER.contains(userId); + return instance.config.dinnerTrustedReaders.contains(userId); } public Set getTrustedReadersOfDinnerSet () { - return Set.copyOf(TRUSTED_READERS_OF_DINNER); + return Set.copyOf(instance.config.dinnerTrustedReaders); } } diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index 17a831f..7e6f6ac 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -4,9 +4,6 @@ import cc.sukazyo.cono.morny.util.CommonFormat; import javax.annotation.Nonnull; -import java.util.HashSet; -import java.util.Set; - import static cc.sukazyo.cono.morny.Log.logger; /** @@ -18,8 +15,7 @@ import static cc.sukazyo.cono.morny.Log.logger; */ public class ServerMain { - public static final String PROP_TOKEN_KEY = "TELEGRAM_BOT_API_TOKEN"; - public static final String PROP_TOKEN_MORNY_KEY = "MORNY_TG_TOKEN"; + public static final long systemStartupTime = System.currentTimeMillis(); private static final String THREAD_MORNY_INIT = "morny-init"; @@ -57,7 +53,7 @@ public class ServerMain { * 与 {@code --only-hello} 参数不兼容 —— 会导致程序完全没有任何输出 * *

  • - * {@code --outdated-block} 会使得 {@link MornyCoeur#latestEventTimestamp} + * {@code --outdated-block} 会使得 {@link MornyConfig#eventIgnoreOutdated} * 赋值为程序启动的时间,从而造成阻挡程序启动之前的消息事件处理效果。 *
  • *
  • @@ -85,20 +81,12 @@ public class ServerMain { //# //# 启动参数设置区块 //# - + final MornyConfig.Prototype config = new MornyConfig.Prototype(); boolean versionEchoMode = false; boolean welcomeEchoMode = false; boolean showWelcome = true; - String key = null; - String username = null; - boolean outdatedBlock = false; - long master = 793274677L; - Set trustedReadersOfDinner = new HashSet<>(); - long trustedChat = -1001541451710L; - boolean autoCmdList = false; - boolean autoCmdRemove = false; - String api = null; - String api4File = null; + + config.eventOutdatedTimestamp = systemStartupTime; for (int i = 0; i < args.length; i++) { @@ -106,7 +94,7 @@ public class ServerMain { switch (args[i]) { case "--outdated-block", "-ob" -> { - outdatedBlock = true; + config.eventIgnoreOutdated = true; continue; } case "--no-hello", "-hf", "--quiet", "-q" -> { @@ -123,51 +111,51 @@ public class ServerMain { } case "--token", "-t" -> { i++; - key = args[i]; + config.telegramBotKey = args[i]; continue; } case "--username", "-u" -> { i++; - username = args[i]; + config.telegramBotUsername = args[i]; continue; } case "--master", "-mm" -> { i++; - master = Long.parseLong(args[i]); + config.trustedMaster = Long.parseLong(args[i]); continue; } case "--trusted-chat", "-trs" -> { i++; - trustedChat = Long.parseLong(args[i]); + config.trustedChat = Long.parseLong(args[i]); continue; } //noinspection SpellCheckingInspection case "--trusted-reader-dinner", "-trsd" -> { i++; - trustedReadersOfDinner.add(Long.parseLong(args[i])); + config.dinnerTrustedReaders.add(Long.parseLong(args[i])); continue; } case "--auto-cmd", "-cmd", "-c" -> { - autoCmdList = true; - autoCmdRemove = true; + config.commandLoginRefresh = true; + config.commandLogoutClear = true; continue; } case "--auto-cmd-list", "-ca" -> { - autoCmdList = true; + config.commandLoginRefresh = true; continue; } case "--auto-cmd-remove", "-cr" -> { - autoCmdRemove = true; + config.commandLogoutClear = true; continue; } case "--api", "-a" -> { i++; - api = args[i]; + config.telegramBotApiServer = args[i]; continue; } case "--api-files", "files-api", "-af" -> { i++; - api4File = args[i]; + config.telegramBotApiServer4File = args[i]; continue; } } @@ -180,7 +168,7 @@ public class ServerMain { String propToken = null; String propTokenKey = null; - for (String iKey : new String[]{PROP_TOKEN_KEY, PROP_TOKEN_MORNY_KEY}) { + for (String iKey : MornyConfig.PROP_TOKEN_KEY) { if (System.getenv(iKey) != null) { propToken = System.getenv(iKey); propTokenKey = iKey; @@ -228,21 +216,19 @@ public class ServerMain { //# if (propToken != null) { - key = propToken; + config.telegramBotKey = propToken; logger.info("Parameter set by EnvVar $"+propTokenKey); } - if (key == null) { - logger.info("Parameter required has no value:\n --token."); - return; - } + Thread.currentThread().setName(THREAD_MORNY_INIT); - MornyCoeur.main( - api, api4File, - key, username, - master, trustedChat, trustedReadersOfDinner, - outdatedBlock?System.currentTimeMillis():0, - autoCmdList, autoCmdRemove - ); + try { + MornyCoeur.main(new MornyConfig(config)); + } catch (MornyConfig.CheckFailure.NullTelegramBotKey ignore) { + logger.info("Parameter required has no value:\n --token."); + } catch (MornyConfig.CheckFailure e) { + logger.error("Unknown failure occurred while starting ServerMain!:"); + e.printStackTrace(System.out); + } } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java index c448881..89ed0fc 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java @@ -1,7 +1,6 @@ package cc.sukazyo.cono.morny.bot.event; import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.MornyTrusted; import cc.sukazyo.cono.morny.bot.api.EventListener; import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.util.CommonFormat; @@ -27,10 +26,10 @@ public class OnCallMe extends EventListener { /** * 主人的 telegram user id,同时被用于 chat id
    - * 跟随 {@link MornyTrusted#MASTER} 的值 + * 跟随 {@link cc.sukazyo.cono.morny.MornyConfig#trustedMaster} 的值 * @since 0.4.2.1 */ - private static final long ME = MornyCoeur.trustedInstance().MASTER; + private static final long ME = MornyCoeur.config().trustedMaster; /** * 监听私聊 bot 的消息进行呼叫关键字匹配。 @@ -112,7 +111,7 @@ public class OnCallMe extends EventListener { boolean isAllowed = false; Message lastDinnerData = null; if (MornyCoeur.trustedInstance().isTrustedForDinnerRead(event.message().from().id())) { - lastDinnerData = MornyCoeur.extra().exec(new GetChat(MornyCoeur.DINNER_CHAT_ID)).chat().pinnedMessage(); + lastDinnerData = MornyCoeur.extra().exec(new GetChat(MornyCoeur.config().dinnerChatId)).chat().pinnedMessage(); SendResponse sendResp = MornyCoeur.extra().exec(new ForwardMessage( event.message().from().id(), lastDinnerData.forwardFromChat().id(), diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java index 80eff7f..faee77f 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java @@ -7,7 +7,7 @@ import com.pengrad.telegrambot.model.Update; import javax.annotation.Nonnull; /** - * 阻止 {@link MornyCoeur#latestEventTimestamp 指定时间} 之前的事件处理. + * 阻止 {@link cc.sukazyo.cono.morny.MornyConfig#eventOutdatedTimestamp 指定时间} 之前的事件处理. *

    * 只支持以下事件 *

      @@ -27,7 +27,7 @@ public class OnUpdateTimestampOffsetLock extends EventListener { * @since 0.4.2.7 */ public boolean isOutdated(long timestamp) { - return timestamp < MornyCoeur.getLatestEventTimestamp()/1000; + return timestamp < MornyCoeur.config().eventOutdatedTimestamp/1000; } @Override From e3c273b370f341191c24bc82114e788c5d2acf8d Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 6 Nov 2022 20:07:11 +0800 Subject: [PATCH 02/40] =?UTF-8?q?=E5=B0=86=20GradleProjectConfigures=20?= =?UTF-8?q?=E5=8F=98=E6=9B=B4=E4=B8=BA=E4=BD=BF=E7=94=A8=20BuildConfig=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=EF=BC=8C=E6=B7=BB=E5=8A=A0=E4=BA=86=20versio?= =?UTF-8?q?n=5Fdelta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - archiveBaseName 更名为 'morny-coeur' - 现在在 getJarMd5 时遇到文件读写错误不会再输出错误堆栈信息了 - gradle 基本的参数化 --- .gitignore | 1 + build.gradle | 64 +++++++++++-------- gradle.properties | 7 +- .../cono/morny/GradleProjectConfigures.java | 10 --- .../cc/sukazyo/cono/morny/MornySystem.java | 5 +- .../cc/sukazyo/cono/morny/ServerMain.java | 6 +- .../cono/morny/bot/command/MornyCommands.java | 10 +-- .../java/cc/sukazyo/cono/morny/MornyCLI.java | 2 +- 8 files changed, 57 insertions(+), 48 deletions(-) delete mode 100644 src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java diff --git a/.gitignore b/.gitignore index 881a611..df3d0f1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /build/ /bin/ .project +lcoal.properties # debug dir /run/ diff --git a/build.gradle b/build.gradle index 0e2a6aa..d721443 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,33 @@ +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + plugins { id 'java' id 'java-library' id 'maven-publish' id 'application' id 'com.github.johnrengelman.shadow' version '7.1.0' + id 'com.github.gmazzo.buildconfig' version '3.1.0' } -group 'cc.sukazyo' -version VERSION -project.ext.archiveBaseName = 'Coeur_Morny_Cono' -project.ext.artifactId = 'morny-coeur' -mainClassName = 'cc.sukazyo.cono.morny.ServerMain' +final String proj_group = 'cc.sukazyo' +final String proj_package = "${proj_group}.cono.morny" +final String proj_archive_name = MORNY_ARCHIVE_NAME + +final String proj_version_base = VERSION +final String proj_version_delta = VERSION_DELTA +final String proj_version_use_delta = Boolean.parseBoolean(VERSION_DELTA) +final String proj_version = proj_version_base + (proj_version_use_delta ? "-δ${proj_version_delta}" : "") +final String proj_version_codename = CODENAME + +final JavaVersion proj_java = JavaVersion.VERSION_17 +final Charset proj_file_encoding = StandardCharsets.UTF_8 + +group proj_group +version proj_version +project.ext.archiveBaseName = proj_archive_name +project.ext.artifactId = proj_archive_name +mainClassName = "${proj_package}.ServerMain" repositories { mavenCentral() @@ -31,47 +48,44 @@ dependencies { } -task updateVersionCode { - ant.replaceregexp(match:'VERSION = ["a-zA-Z0-9.\\-_+@]+;', replace:"VERSION = \"$project.version\";", flags:'g', byline:true) { - fileset(dir: 'src/main/java/cc/sukazyo/cono/morny', includes: 'GradleProjectConfigures.java') - } - ant.replaceregexp(match:'CODENAME = ["a-zA-Z0-9]+;', replace:"CODENAME = \"${CODENAME}\";", flags:'g', byline:true) { - fileset(dir: 'src/main/java/cc/sukazyo/cono/morny', includes: 'GradleProjectConfigures.java') - } - ant.replaceregexp(match:'COMPILE_TIMESTAMP = [0-9]+L;', replace:"COMPILE_TIMESTAMP = ${System.currentTimeMillis()}L;", flags:'g', byline:true) { - fileset(dir: 'src/main/java/cc/sukazyo/cono/morny', includes: 'GradleProjectConfigures.java') - } -} - -compileJava.dependsOn updateVersionCode - test { useJUnitPlatform() } java { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility proj_java + targetCompatibility proj_java withSourcesJar() } tasks.withType(JavaCompile) { - options.encoding = "UTF-8" + options.encoding = proj_file_encoding.name() } tasks.withType(Javadoc) { - options.encoding = 'UTF-8' - options.docEncoding = 'UTF-8' - options.charSet = 'UTF-8' + options.encoding = proj_file_encoding.name() + options.docEncoding = proj_file_encoding.name() + options.charSet = proj_file_encoding.name() } tasks.test { useJUnitPlatform() } +buildConfig { + + packageName(proj_package) + + buildConfigField('String', 'VERSION', "\"${proj_version}\"") + buildConfigField('String', 'VERSION_DELTA', "\"${proj_version_delta}\"") + buildConfigField('String', 'CODENAME', "\"${proj_version_codename}\"") + buildConfigField('long', 'COMPILE_TIMESTAMP', "${System.currentTimeMillis()}L") + +} + shadowJar { archiveBaseName.set("${project.ext.archiveBaseName}") archiveVersion.set("${project.version}") diff --git a/gradle.properties b/gradle.properties index 0684f5b..b65028a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,11 @@ ## Core -VERSION = 1.0.0-alpha1 +MORNY_ARCHIVE_NAME = morny-coeur + +VERSION = 1.0.0-alpha2 + +USE_DELTA = true +VERSION_DELTA = 11061900 CODENAME = beiping diff --git a/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java b/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java deleted file mode 100644 index 459f791..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/GradleProjectConfigures.java +++ /dev/null @@ -1,10 +0,0 @@ -package cc.sukazyo.cono.morny; - -/** - * the final field that will be updated by gradle automatically. - */ -public class GradleProjectConfigures { - public static final String VERSION = "1.0.0-alpha1"; - public static final String CODENAME = "beiping"; - public static final long COMPILE_TIMESTAMP = 1667581079623L; -} diff --git a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java index e9a486d..a62290e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java @@ -16,7 +16,7 @@ public class MornySystem { * 程序的语义化版本号
      * 会由 gradle 任务 {@code updateVersionCode} 更新 */ - public static final String VERSION = GradleProjectConfigures.VERSION; + public static final String VERSION = BuildConfig.VERSION; /** * Morny Coeur 当前的版本代号.
      @@ -26,7 +26,7 @@ public class MornySystem { *
      * 会由 gradle 任务 {@code updateVersionCode} 更新 */ - public static final String CODENAME = GradleProjectConfigures.CODENAME; + public static final String CODENAME = BuildConfig.CODENAME; /** * 获取程序 jar 文件的 md5-hash 值
      @@ -43,7 +43,6 @@ public class MornySystem { try { return FileUtils.getMD5Three(MornyCoeur.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); } catch (IOException | URISyntaxException e) { - e.printStackTrace(System.out); return ""; } catch (NoSuchAlgorithmException e) { e.printStackTrace(System.out); diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index 7e6f6ac..2e330b2 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -192,8 +192,8 @@ public class ServerMain { %s [UTC]""", MornySystem.VERSION, MornySystem.CODENAME.toUpperCase(), MornySystem.getJarMd5(), - GradleProjectConfigures.COMPILE_TIMESTAMP, - CommonFormat.formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0) + BuildConfig.COMPILE_TIMESTAMP, + CommonFormat.formatDate(BuildConfig.COMPILE_TIMESTAMP, 0) )); return; @@ -207,7 +207,7 @@ public class ServerMain { - version %s (%s)(%d) - Morny %s""", MornySystem.VERSION, - MornySystem.getJarMd5(), GradleProjectConfigures.COMPILE_TIMESTAMP, + MornySystem.getJarMd5(), BuildConfig.COMPILE_TIMESTAMP, MornySystem.CODENAME.toUpperCase() )); diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index 7b37258..4bbe09c 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -1,6 +1,6 @@ package cc.sukazyo.cono.morny.bot.command; -import cc.sukazyo.cono.morny.GradleProjectConfigures; +import cc.sukazyo.cono.morny.BuildConfig; import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.MornySystem; import cc.sukazyo.cono.morny.data.MornyJrrp; @@ -249,8 +249,8 @@ public class MornyCommands { escapeHtml(MornySystem.CODENAME.toUpperCase()), escapeHtml(MornySystem.VERSION), escapeHtml(MornySystem.getJarMd5()), - GradleProjectConfigures.COMPILE_TIMESTAMP, - escapeHtml(formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0)) + BuildConfig.COMPILE_TIMESTAMP, + escapeHtml(formatDate(BuildConfig.COMPILE_TIMESTAMP, 0)) ) ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); } @@ -310,8 +310,8 @@ public class MornyCommands { escapeHtml(MornySystem.VERSION), escapeHtml(MornySystem.CODENAME), escapeHtml(MornySystem.getJarMd5()), - escapeHtml(formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0)), - GradleProjectConfigures.COMPILE_TIMESTAMP, + escapeHtml(formatDate(BuildConfig.COMPILE_TIMESTAMP, 0)), + BuildConfig.COMPILE_TIMESTAMP, // continuous escapeHtml(formatDuration(System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp)), System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp, diff --git a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java index 1b6eea7..c26fca6 100644 --- a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java +++ b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java @@ -9,7 +9,7 @@ public class MornyCLI { public static void main (String[] args) { Scanner line = new Scanner(System.in); - System.out.print("$ java -jar morny-coeur-"+GradleProjectConfigures.VERSION+".jar " ); + System.out.print("$ java -jar morny-coeur-"+BuildConfig.VERSION+".jar " ); String x = line.nextLine(); ServerMain.main(UniversalCommand.format(x)); From 5dfe07f58630edd5d7086cc773c204ae28bbcc61 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 6 Nov 2022 21:31:24 +0800 Subject: [PATCH 03/40] =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20git=20commit=20=E6=A0=87=E7=AD=BE=E6=94=AF=E6=8C=81?= =?UTF-8?q?=EF=BC=8Cgradle=20build=20=E7=9A=84=20git=20=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 58 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index d721443..ea2f560 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,5 @@ +import org.ajoberstar.grgit.Status + import java.nio.charset.Charset import java.nio.charset.StandardCharsets @@ -8,6 +10,27 @@ plugins { id 'application' id 'com.github.johnrengelman.shadow' version '7.1.0' id 'com.github.gmazzo.buildconfig' version '3.1.0' + id 'org.ajoberstar.grgit' version '5.0.0' +} + +final boolean proj_git = grgit!=null +final String proj_commit = proj_git ? grgit.head().id : null +final boolean proj_clean = isCleanBuild() +if (!proj_git) + print "[MornyBuild] git repository not available for current working space! git version tag will be disabled." +else if (isCleanBuild()) { + print "git: clean build at ${grgit.head().id}" +} else { + final Status status = grgit.status() + println "git: non-clean-build" + if (!status.unstaged.allChanges.empty) { + println "git: unstaged changes" + listChanges(status.unstaged) + } + if (!status.staged.allChanges.empty) { + println "[MornyBuild] git: staged changes" + listChanges(status.staged) + } } final String proj_group = 'cc.sukazyo' @@ -17,7 +40,9 @@ final String proj_archive_name = MORNY_ARCHIVE_NAME final String proj_version_base = VERSION final String proj_version_delta = VERSION_DELTA final String proj_version_use_delta = Boolean.parseBoolean(VERSION_DELTA) -final String proj_version = proj_version_base + (proj_version_use_delta ? "-δ${proj_version_delta}" : "") +String proj_version = proj_version_base +if (proj_version_use_delta) proj_version += "-δ${proj_version_delta}" +if (proj_git) proj_version += "+git.${proj_commit.substring(0, 8)}" + (proj_clean?"":".δ") final String proj_version_codename = CODENAME final JavaVersion proj_java = JavaVersion.VERSION_17 @@ -79,10 +104,12 @@ buildConfig { packageName(proj_package) - buildConfigField('String', 'VERSION', "\"${proj_version}\"") - buildConfigField('String', 'VERSION_DELTA', "\"${proj_version_delta}\"") - buildConfigField('String', 'CODENAME', "\"${proj_version_codename}\"") - buildConfigField('long', 'COMPILE_TIMESTAMP', "${System.currentTimeMillis()}L") + buildConfigField('String', 'VERSION', "\"${proj_version}\"") + buildConfigField('String', 'VERSION_DELTA', "\"${proj_version_delta}\"") + buildConfigField('String', 'CODENAME', "\"${proj_version_codename}\"") + buildConfigField('long', 'COMPILE_TIMESTAMP', "${System.currentTimeMillis()}L") + buildConfigField('String', 'COMMIT', proj_git ? "\"${proj_commit}\"" : "null") + buildConfigField('boolean', 'CLEAN_BUILD', "${proj_clean}") } @@ -92,6 +119,27 @@ shadowJar { archiveClassifier.set("fat") } +@SuppressWarnings("all") +boolean isCleanBuild () { + if (grgit == null) return false + Set changes = grgit.status().unstaged.allChanges + grgit.status().staged.allChanges + for (String file in changes) { + if (file.startsWith("src/")) return false + if (file == "build.gradle") return false + if (file == "gradle.properties") return false + } + return true +} + +void listChanges (Status.Changes listing) { + for (String file in listing.added) + println " add: ${file}" + for (String file in listing.modified) + println " mod: ${file}" + for (String file in listing.removed) + println " del: ${file}" +} + publishing { repositories{ maven { From 3c7de7037d7976c58bcf02963bc730eba359c7f5 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 6 Nov 2022 21:43:15 +0800 Subject: [PATCH 04/40] =?UTF-8?q?=E5=B0=86=20COMPILE=5FTIMESTAMP=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=9F=BA=E4=BA=8E=20commit=20time=20?= =?UTF-8?q?=E7=9A=84=20CODE=5FTIMESTAMP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++- gradle.properties | 4 ++-- src/main/java/cc/sukazyo/cono/morny/ServerMain.java | 6 +++--- .../sukazyo/cono/morny/bot/command/MornyCommands.java | 10 +++++----- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index ea2f560..c951ab2 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ String proj_version = proj_version_base if (proj_version_use_delta) proj_version += "-δ${proj_version_delta}" if (proj_git) proj_version += "+git.${proj_commit.substring(0, 8)}" + (proj_clean?"":".δ") final String proj_version_codename = CODENAME +final long proj_code_time = proj_clean ? grgit.head().dateTime.toInstant().toEpochMilli() : System.currentTimeMillis() final JavaVersion proj_java = JavaVersion.VERSION_17 final Charset proj_file_encoding = StandardCharsets.UTF_8 @@ -107,7 +108,7 @@ buildConfig { buildConfigField('String', 'VERSION', "\"${proj_version}\"") buildConfigField('String', 'VERSION_DELTA', "\"${proj_version_delta}\"") buildConfigField('String', 'CODENAME', "\"${proj_version_codename}\"") - buildConfigField('long', 'COMPILE_TIMESTAMP', "${System.currentTimeMillis()}L") + buildConfigField('long', 'CODE_TIMESTAMP', "${proj_code_time}L") buildConfigField('String', 'COMMIT', proj_git ? "\"${proj_commit}\"" : "null") buildConfigField('boolean', 'CLEAN_BUILD', "${proj_clean}") diff --git a/gradle.properties b/gradle.properties index b65028a..01d5e1a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,10 +2,10 @@ MORNY_ARCHIVE_NAME = morny-coeur -VERSION = 1.0.0-alpha2 +VERSION = 1.0.0-alpha3 USE_DELTA = true -VERSION_DELTA = 11061900 +VERSION_DELTA = 11061142 CODENAME = beiping diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index 2e330b2..6c720f8 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -192,8 +192,8 @@ public class ServerMain { %s [UTC]""", MornySystem.VERSION, MornySystem.CODENAME.toUpperCase(), MornySystem.getJarMd5(), - BuildConfig.COMPILE_TIMESTAMP, - CommonFormat.formatDate(BuildConfig.COMPILE_TIMESTAMP, 0) + BuildConfig.CODE_TIMESTAMP, + CommonFormat.formatDate(BuildConfig.CODE_TIMESTAMP, 0) )); return; @@ -207,7 +207,7 @@ public class ServerMain { - version %s (%s)(%d) - Morny %s""", MornySystem.VERSION, - MornySystem.getJarMd5(), BuildConfig.COMPILE_TIMESTAMP, + MornySystem.getJarMd5(), BuildConfig.CODE_TIMESTAMP, MornySystem.CODENAME.toUpperCase() )); diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index 4bbe09c..8b6f124 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -243,14 +243,14 @@ public class MornyCommands { - %s core md5_hash: - %s - compile timestamp: + coding timestamp: - %d - %s [UTC]""", escapeHtml(MornySystem.CODENAME.toUpperCase()), escapeHtml(MornySystem.VERSION), escapeHtml(MornySystem.getJarMd5()), - BuildConfig.COMPILE_TIMESTAMP, - escapeHtml(formatDate(BuildConfig.COMPILE_TIMESTAMP, 0)) + BuildConfig.CODE_TIMESTAMP, + escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)) ) ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); } @@ -310,8 +310,8 @@ public class MornyCommands { escapeHtml(MornySystem.VERSION), escapeHtml(MornySystem.CODENAME), escapeHtml(MornySystem.getJarMd5()), - escapeHtml(formatDate(BuildConfig.COMPILE_TIMESTAMP, 0)), - BuildConfig.COMPILE_TIMESTAMP, + escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)), + BuildConfig.CODE_TIMESTAMP, // continuous escapeHtml(formatDuration(System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp)), System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp, From f362d08f3459e4f74b6b05f3b4170f73edea0762 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Mon, 7 Nov 2022 12:54:01 +0800 Subject: [PATCH 05/40] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86=E6=89=93?= =?UTF-8?q?=E5=8C=85=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 设置了全局 archivesBaseName - 对一些属性的声明方式进行了调整 - 为 maven publish 添加了动态配置,以在没有配置 publish url 的机器上也能够正常运行基础功能 - gradle plugin: shadow: 7.1.0 -> 7.1.2 - gradle wrapper: 7.3 -> 7.5.1 - 稍微修改了 git 状态的 log 输出方式 --- build.gradle | 60 ++++++++++++++---------- gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/build.gradle b/build.gradle index c951ab2..35b05c0 100644 --- a/build.gradle +++ b/build.gradle @@ -6,9 +6,9 @@ import java.nio.charset.StandardCharsets plugins { id 'java' id 'java-library' - id 'maven-publish' id 'application' - id 'com.github.johnrengelman.shadow' version '7.1.0' + id 'maven-publish' + id 'com.github.johnrengelman.shadow' version '7.1.2' id 'com.github.gmazzo.buildconfig' version '3.1.0' id 'org.ajoberstar.grgit' version '5.0.0' } @@ -22,13 +22,13 @@ else if (isCleanBuild()) { print "git: clean build at ${grgit.head().id}" } else { final Status status = grgit.status() - println "git: non-clean-build" + print "git: non-clean-build" if (!status.unstaged.allChanges.empty) { - println "git: unstaged changes" + print "\ngit: unstaged changes" listChanges(status.unstaged) } if (!status.staged.allChanges.empty) { - println "[MornyBuild] git: staged changes" + print "\ngit: staged changes" listChanges(status.staged) } } @@ -36,6 +36,7 @@ else if (isCleanBuild()) { final String proj_group = 'cc.sukazyo' final String proj_package = "${proj_group}.cono.morny" final String proj_archive_name = MORNY_ARCHIVE_NAME +final String proj_application_main = "${proj_package}.ServerMain" final String proj_version_base = VERSION final String proj_version_delta = VERSION_DELTA @@ -49,11 +50,20 @@ final long proj_code_time = proj_clean ? grgit.head().dateTime.toInstant().toEpo final JavaVersion proj_java = JavaVersion.VERSION_17 final Charset proj_file_encoding = StandardCharsets.UTF_8 +String publish_local_url = null +String publish_remote_url = null +String publish_remote_username = null +String publish_remote_password = null +if (project.hasProperty("publishLocalArchiveRepoUrl")) publish_local_url = publishLocalArchiveRepoUrl +if (project.hasProperty("publishMvnRepoUrl")) { + publish_remote_url = publishMvnRepoUrl + publish_remote_username = publishMvnRepoUsername + publish_remote_password = publishMvnRepoPassword +} + group proj_group version proj_version -project.ext.archiveBaseName = proj_archive_name -project.ext.artifactId = proj_archive_name -mainClassName = "${proj_package}.ServerMain" +setArchivesBaseName proj_archive_name repositories { mavenCentral() @@ -74,6 +84,10 @@ dependencies { } +application { + mainClass = proj_application_main +} + test { useJUnitPlatform() } @@ -115,9 +129,7 @@ buildConfig { } shadowJar { - archiveBaseName.set("${project.ext.archiveBaseName}") - archiveVersion.set("${project.version}") - archiveClassifier.set("fat") + archiveClassifier.set "fat" } @SuppressWarnings("all") @@ -134,34 +146,34 @@ boolean isCleanBuild () { void listChanges (Status.Changes listing) { for (String file in listing.added) - println " add: ${file}" + print "\n add: ${file}" for (String file in listing.modified) - println " mod: ${file}" + print "\n mod: ${file}" for (String file in listing.removed) - println " del: ${file}" + print "\n del: ${file}" } publishing { repositories{ - maven { - name 'builds' - url publishLocalArchiveRepoUrl + if (publish_local_url != null) maven { + name 'archives' + url publish_local_url } - maven { + if (publish_remote_url != null) maven { name '-ws-' - url publishMvnRepoUrl + url publish_remote_url credentials { - username publishMvnRepoUsername - password publishMvnRepoPassword + username publish_remote_username + password publish_remote_password } } } publications { main (MavenPublication) { from components.java - groupId = project.group - artifactId = project.ext.artifactId - version = project.version + groupId = proj_group + artifactId = proj_archive_name + version = proj_version } } } diff --git a/gradle.properties b/gradle.properties index 01d5e1a..3db275d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,8 +4,8 @@ MORNY_ARCHIVE_NAME = morny-coeur VERSION = 1.0.0-alpha3 -USE_DELTA = true -VERSION_DELTA = 11061142 +USE_DELTA = false +VERSION_DELTA = CODENAME = beiping diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102..ae04661 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d8eb37206324da9f299c99c3ff05532cb9fabaeb Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Tue, 8 Nov 2022 22:37:46 +0800 Subject: [PATCH 06/40] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=B8=AD=20version=20delta=20=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=88=A4=E6=96=AD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 现在如果 use_delta 为 false 则输出 BuildConfig.VERSION_DELTA 为 null --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 35b05c0..60a856a 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ final String proj_application_main = "${proj_package}.ServerMain" final String proj_version_base = VERSION final String proj_version_delta = VERSION_DELTA -final String proj_version_use_delta = Boolean.parseBoolean(VERSION_DELTA) +final boolean proj_version_use_delta = Boolean.parseBoolean(USE_DELTA) String proj_version = proj_version_base if (proj_version_use_delta) proj_version += "-δ${proj_version_delta}" if (proj_git) proj_version += "+git.${proj_commit.substring(0, 8)}" + (proj_clean?"":".δ") @@ -120,7 +120,7 @@ buildConfig { packageName(proj_package) buildConfigField('String', 'VERSION', "\"${proj_version}\"") - buildConfigField('String', 'VERSION_DELTA', "\"${proj_version_delta}\"") + buildConfigField('String', 'VERSION_DELTA', proj_version_use_delta ? "\"${proj_version_delta}\"" : "null") buildConfigField('String', 'CODENAME', "\"${proj_version_codename}\"") buildConfigField('long', 'CODE_TIMESTAMP', "${proj_code_time}L") buildConfigField('String', 'COMMIT', proj_git ? "\"${proj_commit}\"" : "null") From 201c8bcd1a2fd48d0379abba95de5227c8df9e95 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Wed, 9 Nov 2022 01:41:01 +0800 Subject: [PATCH 07/40] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=20gi?= =?UTF-8?q?t=20=E9=93=BE=E6=8E=A5=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=8C=E5=A4=A7=E6=94=B9=E5=8A=A8=E7=89=88=E6=9C=AC=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=96=B9=E5=BC=8F=EF=BC=8C/version=20=E4=B8=8E=20/run?= =?UTF-8?q?time=20=E5=90=88=E5=B9=B6=E8=BF=9B=20/info?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 /version 和 /runtime 合并为了 /info 的子命令 - 旧的 /version 和 /runtime 命令仍然可用 - runtime 现在是 /info 的无参数行为 - 更新了各种位置的版本显示方式 - /version 现在通过组装的方式显示版本号的 BASE 与 DELTA 部分,并支持了可能的 git commit 显示,同时支持输出 commit 链接 - /runtime 现在通过组装的方式显示版本号的 BASE, DELTA, GIT 部分,其中也支持了 git commit 链接,同时 CODENAME 的显示方式规范为了 `version*CODENAME` 的格式 - ServerMain 的 -v 修改了显示格式,同时添加了 gitstat 字段显示 build 时的 git 信息 - ServerMain 的启动版本回显(仍使用完全体VERSION)将 md5hash 和 code-time 移到了新行 - dependencies update - java-telegram-bot-api: 5.6.0 -> 6.2.0 --- build.gradle | 5 + gradle.properties | 7 +- .../cc/sukazyo/cono/morny/MornySystem.java | 87 ++++++++++++++-- .../cc/sukazyo/cono/morny/ServerMain.java | 23 +++-- .../cono/morny/bot/command/MornyCommands.java | 42 ++++---- .../morny/bot/command/MornyInformations.java | 98 +++++++++++++++---- .../cono/morny/bot/event/OnCallMsgSend.java | 2 +- .../cono/morny/data/TelegramStickers.java | 23 ++++- .../cono/morny/util/BuildConfigField.java | 14 +++ 9 files changed, 249 insertions(+), 52 deletions(-) create mode 100644 src/main/java/cc/sukazyo/cono/morny/util/BuildConfigField.java diff --git a/build.gradle b/build.gradle index 60a856a..21ab1a4 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,9 @@ plugins { } final boolean proj_git = grgit!=null +final String proj_store = MORNY_CODE_STORE final String proj_commit = proj_git ? grgit.head().id : null +final String proj_commit_path = MORNY_COMMIT_PATH final boolean proj_clean = isCleanBuild() if (!proj_git) print "[MornyBuild] git repository not available for current working space! git version tag will be disabled." @@ -120,11 +122,14 @@ buildConfig { packageName(proj_package) buildConfigField('String', 'VERSION', "\"${proj_version}\"") + buildConfigField('String', 'VERSION_BASE', "\"${proj_version_base}\"") buildConfigField('String', 'VERSION_DELTA', proj_version_use_delta ? "\"${proj_version_delta}\"" : "null") buildConfigField('String', 'CODENAME', "\"${proj_version_codename}\"") buildConfigField('long', 'CODE_TIMESTAMP', "${proj_code_time}L") buildConfigField('String', 'COMMIT', proj_git ? "\"${proj_commit}\"" : "null") buildConfigField('boolean', 'CLEAN_BUILD', "${proj_clean}") + buildConfigField('String', 'CODE_STORE', proj_store==""?"null":"\"${proj_store}\"") + buildConfigField('String', 'COMMIT_PATH', proj_commit_path==""?"null":"\"${proj_commit_path}\"") } diff --git a/gradle.properties b/gradle.properties index 3db275d..ec523fe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,10 @@ MORNY_ARCHIVE_NAME = morny-coeur -VERSION = 1.0.0-alpha3 +MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono +MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s + +VERSION = 1.0.0-alpha4 USE_DELTA = false VERSION_DELTA = @@ -15,6 +18,6 @@ libSpotbugsVersion = 4.7.3 libMessivaVersion = 0.1.0.1 -libJavaTelegramBotApiVersion = 5.6.0 +libJavaTelegramBotApiVersion = 6.2.0 libJunitVersion = 5.9.0 diff --git a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java index a62290e..e15a81e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java @@ -1,8 +1,10 @@ package cc.sukazyo.cono.morny; +import cc.sukazyo.cono.morny.util.BuildConfigField; import cc.sukazyo.cono.morny.util.FileUtils; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.net.URISyntaxException; import java.security.NoSuchAlgorithmException; @@ -13,20 +15,93 @@ import java.security.NoSuchAlgorithmException; public class MornySystem { /** - * 程序的语义化版本号
      - * 会由 gradle 任务 {@code updateVersionCode} 更新 + * 程序的语义化版本号. + *

      + * 这个版本号是完整(verbose)构建的——它包含了以下的 {@link #VERSION_BASE}, {@link #VERSION_DELTA}, + * 和简写的 {@link BuildConfig#COMMIT} 字段. + * @since 1.0.0-alpha4 */ - public static final String VERSION = BuildConfig.VERSION; + @BuildConfigField @Nonnull public static final String VERSION = BuildConfig.VERSION; + /** + * 程序的基础版本号. + *

      + * 它只包含了版本号中的主要信息:例如 {@code 0.8.0.5}, {@code 1.0.0-alpha-3}, + * 而不会有用于精确定义的 {@link #VERSION_DELTA} 字段和作为附加使用的 {@link BuildConfig#COMMIT git commit 信息} + * @since 1.0.0-alpha4 + */ + @BuildConfigField @Nonnull public static final String VERSION_BASE = BuildConfig.VERSION_BASE; + /** + * 程序的版本 delta. + * —— 设计上用于在一个基版本当中分出不同构建的版本. + *

      + * {@link null} 作为值,表示这个字段没有被使用. + *

      + * 版本 delta 会以 {@code -δversion-delta} 的形式附着在 {@link #VERSION_BASE} 之后. + *

      + * 目前并不多被使用. + * @since 1.0.0-alpha4 + */ + @BuildConfigField @Nullable public static final String VERSION_DELTA = BuildConfig.VERSION_DELTA; /** - * Morny Coeur 当前的版本代号.
      + * Morny Coeur 当前的版本代号. + *

      * 一个单个单词,一般作为一个大版本的名称,只在重大更新改变
      * 格式保持为仅由小写字母和数字组成
      * 有时也可能是复合词或特殊的词句
      *
      - * 会由 gradle 任务 {@code updateVersionCode} 更新 */ - public static final String CODENAME = BuildConfig.CODENAME; + @BuildConfigField @Nonnull public static final String CODENAME = BuildConfig.CODENAME; + + /** + * Coeur 的代码仓库的链接. 它应该链接到当前程序的源码主页. + *

      + * {@link null} 表示这个属性在构建时未被设置(或没有源码主页) + * @since 1.0.0-alpha4 + */ + @BuildConfigField @Nullable public static final String CODE_STORE = BuildConfig.CODE_STORE; + /** + * Coeur 的 git commit 链接. + *

      + * 它应该是一个可以通过 {@link String#format(String, Object...)} 要求格式的链接模板,带有一个 {@link String} 类型的槽位 —— + * 通过 String.format(COMMIT_PATH, {@link BuildConfig#COMMIT}) 即可取得当前当前程序所基于的 commit 的链接。 + * @since 1.0.0-alpha4 + */ + @BuildConfigField @Nullable public static final String COMMIT_PATH = BuildConfig.COMMIT_PATH; + + /** @see #VERSION_DELTA */ + @BuildConfigField + public static boolean isUseDelta () { return VERSION_DELTA != null; } + + /** @see BuildConfig#COMMIT */ + @BuildConfigField + @SuppressWarnings("ConstantConditions") + public static boolean isGitBuild () { return BuildConfig.COMMIT != null; } + + /** @see BuildConfig#COMMIT */ + @BuildConfigField + public static boolean isCleanBuild () { return BuildConfig.CLEAN_BUILD; } + + /** + * 获取程序的当前构建所基于的 git commit 的链接. + *

      + * 如果 {@link #COMMIT_PATH}(一般表示没有公开储存库) + * 或是 {@link BuildConfig#COMMIT}(一般表示程序的构建环境没有使用 git) + * 任何一个不可用,则此方法也不可用。 + * + * @return 当前构建的 git commit 链接,为空则表示不可用。 + * @see #COMMIT_PATH + * @since 1.0.0-alpha4 + */ + @Nullable + @BuildConfigField + @SuppressWarnings("ConstantConditions") + public static String currentCodePath () { + if (COMMIT_PATH == null || !isGitBuild()) return null; + return String.format(COMMIT_PATH, BuildConfig.COMMIT); + } + + /** * 获取程序 jar 文件的 md5-hash 值
      diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index 6c720f8..d7c4b3a 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -184,14 +184,24 @@ public class ServerMain { logger.info(String.format(""" Morny Cono Version - version : - %s %s + Morny %s + %s%s - md5hash : %s + - gitstat : + %s - co.time : %d %s [UTC]""", - MornySystem.VERSION, MornySystem.CODENAME.toUpperCase(), + MornySystem.CODENAME.toUpperCase(), + MornySystem.VERSION_BASE, + MornySystem.isUseDelta() ? "-δ"+MornySystem.VERSION_DELTA : "", MornySystem.getJarMd5(), + MornySystem.isGitBuild() ? (String.format( + "on commit %s\n %s", + MornySystem.isCleanBuild() ? "- clean-build" : "<δ/non-clean-build>", + BuildConfig.COMMIT + )) : "", BuildConfig.CODE_TIMESTAMP, CommonFormat.formatDate(BuildConfig.CODE_TIMESTAMP, 0) )); @@ -204,11 +214,12 @@ public class ServerMain { logger.info(String.format(""" ServerMain.java Loaded >>> - - version %s (%s)(%d) - - Morny %s""", + - version %s + - Morny %s + - <%s> [%d]""", MornySystem.VERSION, - MornySystem.getJarMd5(), BuildConfig.CODE_TIMESTAMP, - MornySystem.CODENAME.toUpperCase() + MornySystem.CODENAME.toUpperCase(), + MornySystem.getJarMd5(), BuildConfig.CODE_TIMESTAMP )); //# diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index 8b6f124..f5401fc 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -21,10 +21,7 @@ import javax.annotation.Nullable; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static cc.sukazyo.cono.morny.Log.logger; import static cc.sukazyo.cono.morny.util.CommonFormat.formatDate; @@ -226,28 +223,33 @@ public class MornyCommands { } } - private static class Version implements ITelegramCommand { + private static class Version implements ISimpleCommand { @Nonnull @Override public String getName () { return "version"; } @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "检查 Bot 版本信息"; } + @Nonnull @Deprecated public String getParamRule () { return ""; } + @Nonnull @Deprecated public String getDescription () { return "检查 Bot 版本信息"; } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandVersionExec(event); } } - private static void onCommandVersionExec (@Nonnull Update event) { + public static void onCommandVersionExec (@Nonnull Update event) { MornyCoeur.extra().exec(new SendMessage( event.message().chat().id(), String.format( """ version: - Morny %s - - %s + - %s%s%s core md5_hash: - %s coding timestamp: - %d - %s [UTC]""", escapeHtml(MornySystem.CODENAME.toUpperCase()), - escapeHtml(MornySystem.VERSION), + escapeHtml(MornySystem.VERSION_BASE), + MornySystem.isUseDelta() ? String.format("-δ%s", escapeHtml(Objects.requireNonNull(MornySystem.VERSION_DELTA))) : "", + MornySystem.isGitBuild()?"\n- git "+ (MornySystem.currentCodePath()==null ? + String.format("%s", BuildConfig.COMMIT.substring(0, 8)) : + String.format("%s", MornySystem.currentCodePath(), BuildConfig.COMMIT.substring(0, 8)) + ) + (MornySystem.isCleanBuild() ? "" : ".δ") : "", escapeHtml(MornySystem.getJarMd5()), BuildConfig.CODE_TIMESTAMP, escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)) @@ -255,17 +257,17 @@ public class MornyCommands { ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); } - private static class MornyRuntime implements ITelegramCommand { + private static class MornyRuntime implements ISimpleCommand { @Nonnull @Override public String getName () { return "runtime"; } @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; } + @Nonnull @Deprecated public String getParamRule () { return ""; } + @Nonnull @Deprecated public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandRuntimeExec(event); } } /** * @since 0.4.1.2 */ - private static void onCommandRuntimeExec (@Nonnull Update event) { + public static void onCommandRuntimeExec (@Nonnull Update event) { String hostname; try { hostname = InetAddress.getLocalHost().getHostName(); @@ -286,7 +288,7 @@ public class MornyCommands { - %d / %d MB - %d cores coeur version: - - %s (%s) + - %s%s%s*%s - %s - %s [UTC] - [%d] @@ -307,8 +309,14 @@ public class MornyCommands { Runtime.getRuntime().maxMemory() / 1024 / 1024, Runtime.getRuntime().availableProcessors(), // version - escapeHtml(MornySystem.VERSION), - escapeHtml(MornySystem.CODENAME), + escapeHtml(MornySystem.VERSION_BASE), + MornySystem.isUseDelta() ? String.format("-δ%s", escapeHtml(Objects.requireNonNull(MornySystem.VERSION_DELTA))) : "", + MornySystem.isGitBuild()?String.format("-git.%s%s", MornySystem.currentCodePath()==null ? ( + String.format("%s", BuildConfig.COMMIT.substring(0, 8)) + ) : ( + String.format("%s", MornySystem.currentCodePath(), BuildConfig.COMMIT.substring(0, 8)) + ), MornySystem.isCleanBuild()?"":".δ"):"", + escapeHtml(MornySystem.CODENAME.toUpperCase()), escapeHtml(MornySystem.getJarMd5()), escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)), BuildConfig.CODE_TIMESTAMP, diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java index 9d815bb..271f644 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java @@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.bot.command; import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.data.TelegramStickers; +import cc.sukazyo.cono.morny.util.tgapi.ExtraAction; import cc.sukazyo.cono.morny.util.tgapi.InputCommand; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.SendSticker; @@ -11,36 +12,97 @@ import javax.annotation.Nullable; public class MornyInformations implements ITelegramCommand { - private static final String ACT_STICKER = "stickers"; + private static final String SUB_STICKER = "stickers"; + private static final String SUB_RUNTIME = "runtime"; + private static final String SUB_VERSION = "version"; + private static final String SUB_VERSION_2 = "v"; @Nonnull @Override public String getName () { return "info"; } @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[(stickers)|(stickers.)sticker_id]"; } - @Nonnull @Override public String getDescription () { return "输出 Morny 当前版本的一些预定义信息"; } + @Nonnull @Override public String getParamRule () { return "[subcommand]"; } + @Nonnull @Override public String getDescription () { return "输出当前 Morny 的各种信息"; } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - if (!command.hasArgs() || command.getArgs().length > 1) { - MornyCoeur.extra().exec(new SendSticker(event.message().chat().id(), TelegramStickers.ID_404).replyToMessageId(event.message().messageId())); - } + if (!command.hasArgs()) echoRuntime(command, event); final String action = command.getArgs()[0]; - if (action.startsWith("stickers")) { - if (action.equals("stickers")) - TelegramStickers.echoAllStickers(MornyCoeur.extra(), event.message().chat().id(), event.message().messageId()); - else { - TelegramStickers.echoStickerByID( - action.substring((ACT_STICKER+".").length()), - MornyCoeur.extra(), event.message().chat().id(), event.message().messageId() - ); - } - return; + if (action.startsWith(SUB_STICKER)) { + echoStickers(command, event); + } else if (action.equals(SUB_RUNTIME)) { + echoRuntime(command, event); + } else if (action.equals(SUB_VERSION) || action.equals(SUB_VERSION_2)) { + echoVersion(command, event); + } else { + echo404(event); } - MornyCoeur.extra().exec(new SendSticker(event.message().chat().id(), TelegramStickers.ID_404).replyToMessageId(event.message().messageId())); - + } + + /** + * /info 子命令 {@value #SUB_STICKER} + */ + public void echoStickers (@Nonnull InputCommand command, @Nonnull Update event) { + final long echoTo = event.message().chat().id(); + final int replyToMessage = event.message().messageId(); + String id = null; + if (command.getArgs()[0].equals(SUB_STICKER)) { + if (command.getArgs().length == 1) { + id = ""; + } else if (command.getArgs().length == 2) { + id = command.getArgs()[1]; + } + } else if (command.getArgs().length == 1) { + if (command.getArgs()[0].startsWith(SUB_STICKER+".") || command.getArgs()[0].startsWith(SUB_STICKER+"#")) { + id = command.getArgs()[0].substring(SUB_STICKER.length()+1); + } + } + if (id == null) { echo404(event); return; } + echoStickers(id, echoTo, replyToMessage); + } + + /** + * 向 telegram 输出一个或全部 sticker + * @param id + * sticker 在 {@link TelegramStickers} 中的字段名。 + * 使用 {@link ""}(空字符串)(不是{@link null}) 表示输出全部 sticker + * @param chatId 目标 chat id + * @param messageId 要回复的消息 id,特殊值跟随上游逻辑 + * @see TelegramStickers#echoStickerByID(String, ExtraAction, long, int) + * @see TelegramStickers#echoAllStickers(ExtraAction, long, int) + */ + public static void echoStickers (@Nonnull String id, long chatId, int messageId) { + if ("".equals(id)) TelegramStickers.echoAllStickers(MornyCoeur.extra(), chatId, messageId); + else TelegramStickers.echoStickerByID(id, MornyCoeur.extra(), chatId, messageId); + } + + /** + * /info 子命令 {@value #SUB_RUNTIME} + * @since 1.0.0-alpha4 + */ + public static void echoRuntime (@Nonnull InputCommand command, @Nonnull Update event) { + if (command.getArgs().length == 1) + MornyCommands.onCommandRuntimeExec(event); + else echo404(event); + } + + /** + * /info 子命令 {@value #SUB_VERSION} + * @since 1.0.0-alpha4 + */ + public static void echoVersion (@Nonnull InputCommand command, @Nonnull Update event) { + if (command.getArgs().length == 1) + MornyCommands.onCommandVersionExec(event); + else echo404(event); + } + + private static void echo404 (@Nonnull Update event) { + MornyCoeur.extra().exec(new SendSticker( + event.message().chat().id(), + TelegramStickers.ID_404 + ).replyToMessageId(event.message().messageId())); } } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java index d02beae..0b362f9 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java @@ -37,7 +37,7 @@ public class OnCallMsgSend extends EventListener { ) { } @Override - public boolean onMessage(Update update) { + public boolean onMessage(@Nonnull Update update) { // 执行体检查 if (update.message().chat().type() != Chat.Type.Private) return false; diff --git a/src/main/java/cc/sukazyo/cono/morny/data/TelegramStickers.java b/src/main/java/cc/sukazyo/cono/morny/data/TelegramStickers.java index fb3f01c..d23d1fe 100644 --- a/src/main/java/cc/sukazyo/cono/morny/data/TelegramStickers.java +++ b/src/main/java/cc/sukazyo/cono/morny/data/TelegramStickers.java @@ -5,6 +5,7 @@ import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.request.SendSticker; import com.pengrad.telegrambot.response.SendResponse; +import javax.annotation.Nonnull; import java.lang.reflect.Field; /** @@ -24,7 +25,14 @@ public class TelegramStickers { public static final String ID_PROGYNOVA = "CAACAgUAAxkBAAICm2KEuL7UQqNP7vSPCg2DHJIND6UsAAKLAwACH4WSBszIo722aQ3jJAQ"; public static final String ID_NETWORK_ERR = "CAACAgEAAxkBAAID0WNJgNEkD726KW4vZeFlw0FlVVyNAAIXJgACePzGBb50o7O1RbxoKgQ"; - public static void echoAllStickers (ExtraAction actionObject, long sentChat, int replyToMessageId) { + /** + * 向 telegram 输出当前的 {@link TelegramStickers} 中的所有 stickers. + * @param actionObject 要使用的 telegram account 包装实例 + * @param sentChat 目标 telegram chat id + * @param replyToMessageId 输出时回复指定的消息的 id。使用 {@link -1} 表示不回复消息 + * @since 0.8.0.6 + */ + public static void echoAllStickers (@Nonnull ExtraAction actionObject, long sentChat, int replyToMessageId) { for (Field object : TelegramStickers.class.getFields()) { if (object.getType()==String.class && object.getName().startsWith("ID_")) { @@ -45,7 +53,18 @@ public class TelegramStickers { } - public static void echoStickerByID (String stickerFieldID, ExtraAction actionObject, long sentChat, int replyToMessageId) { + /** + * 向 telegram 输出当前的 {@link TelegramStickers} 中的某个特定 sticker. + * @param stickerFieldID 要输出的 sticker 在 {@link TelegramStickers} 当中的字段名 + * @param actionObject 要使用的 telegram account 包装实例 + * @param sentChat 目标 telegram chat id + * @param replyToMessageId 输出时回复指定的消息的 id。使用 {@link -1} 表示不回复消息 + * @since 0.8.0.6 + */ + public static void echoStickerByID ( + @Nonnull String stickerFieldID, + @Nonnull ExtraAction actionObject, long sentChat, int replyToMessageId + ) { try { // normally get the sticker and echo Field sticker = TelegramStickers.class.getField(stickerFieldID); diff --git a/src/main/java/cc/sukazyo/cono/morny/util/BuildConfigField.java b/src/main/java/cc/sukazyo/cono/morny/util/BuildConfigField.java new file mode 100644 index 0000000..c584a8b --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/util/BuildConfigField.java @@ -0,0 +1,14 @@ +package cc.sukazyo.cono.morny.util; + + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * 这个注解表示当前字段是由 gradle 任务 {@code generateBuildConfig} 自动生成的. + * @since 1.0.0-alpha4 + */ +@Documented +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface BuildConfigField {} From 48a8fd9daa61ac75a4bf50247fdbbbb7ca28b0c3 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Wed, 9 Nov 2022 01:48:03 +0800 Subject: [PATCH 08/40] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20/info=20=E6=97=A0?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E7=9A=84=E9=94=99=E8=AF=AF=E5=8F=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/bot/command/MornyInformations.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index ec523fe..c8939ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-alpha4 +VERSION = 1.0.0-alpha4.1 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java index 271f644..5558f6e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java @@ -25,7 +25,10 @@ public class MornyInformations implements ITelegramCommand { @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - if (!command.hasArgs()) echoRuntime(command, event); + if (!command.hasArgs()) { + MornyCommands.onCommandRuntimeExec(event); + return; + } final String action = command.getArgs()[0]; From 837ec178d358e70838a5e916461ec37a1862566a Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Wed, 9 Nov 2022 12:03:17 +0800 Subject: [PATCH 09/40] =?UTF-8?q?=E5=B0=86=E5=B8=A6=E6=9C=89=20git=20?= =?UTF-8?q?=E7=9A=84=20version=20=E6=9B=B4=E5=90=8D=E4=B8=BA=20version-ful?= =?UTF-8?q?l=EF=BC=8C=E5=8E=9F=E5=85=88=E7=9A=84=20version=20=E5=8E=BB?= =?UTF-8?q?=E9=99=A4=20git=20=E7=AD=89=E9=99=84=E5=8A=A0=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=EF=BC=8C=E4=BD=9C=E4=B8=BA=20maven=20=E5=8C=85=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 ++++---- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/MornySystem.java | 19 +++++++++++++++++-- .../cc/sukazyo/cono/morny/ServerMain.java | 2 +- .../java/cc/sukazyo/cono/morny/MornyCLI.java | 2 +- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 21ab1a4..1629e2c 100644 --- a/build.gradle +++ b/build.gradle @@ -43,9 +43,8 @@ final String proj_application_main = "${proj_package}.ServerMain" final String proj_version_base = VERSION final String proj_version_delta = VERSION_DELTA final boolean proj_version_use_delta = Boolean.parseBoolean(USE_DELTA) -String proj_version = proj_version_base -if (proj_version_use_delta) proj_version += "-δ${proj_version_delta}" -if (proj_git) proj_version += "+git.${proj_commit.substring(0, 8)}" + (proj_clean?"":".δ") +final String proj_version = proj_version_base + (proj_version_use_delta ? "-δ${proj_version_delta}" : "") +final String proj_version_full = proj_version + (proj_git ? "+git.${proj_commit.substring(0, 8)}" + (proj_clean?"":".δ") : "") final String proj_version_codename = CODENAME final long proj_code_time = proj_clean ? grgit.head().dateTime.toInstant().toEpochMilli() : System.currentTimeMillis() @@ -64,7 +63,7 @@ if (project.hasProperty("publishMvnRepoUrl")) { } group proj_group -version proj_version +version proj_version_full setArchivesBaseName proj_archive_name repositories { @@ -122,6 +121,7 @@ buildConfig { packageName(proj_package) buildConfigField('String', 'VERSION', "\"${proj_version}\"") + buildConfigField('String', 'VERSION_FULL', "\"${proj_version_full}\"") buildConfigField('String', 'VERSION_BASE', "\"${proj_version_base}\"") buildConfigField('String', 'VERSION_DELTA', proj_version_use_delta ? "\"${proj_version_delta}\"" : "null") buildConfigField('String', 'CODENAME', "\"${proj_version_codename}\"") diff --git a/gradle.properties b/gradle.properties index c8939ed..cdee59b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-alpha4.1 +VERSION = 1.0.0-alpha4.2 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java index e15a81e..c00fc86 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java @@ -17,11 +17,23 @@ public class MornySystem { /** * 程序的语义化版本号. *

      - * 这个版本号是完整(verbose)构建的——它包含了以下的 {@link #VERSION_BASE}, {@link #VERSION_DELTA}, - * 和简写的 {@link BuildConfig#COMMIT} 字段. + * 这个版本号包含了以下的 {@link #VERSION_BASE}, {@link #VERSION_DELTA} 字段, + * 但不包含作为附加属性的构建时的{@link BuildConfig#COMMIT git 状态}属性 + *

      + * 这个格式的版本号也是在 maven 包仓库中所使用的版本号 * @since 1.0.0-alpha4 */ @BuildConfigField @Nonnull public static final String VERSION = BuildConfig.VERSION; + /** + * 程序的完整语义化版本号. + *

      + * 包含了全部的 {@link #VERSION_BASE}, {@link #VERSION_DELTA}, 以及{@link BuildConfig#COMMIT git 状态}属性。 + * 虽然仍旧不包含{@link #CODENAME}属性 + *

      + * 这个格式的版本号也是 gradle 构建配置使用的版本号,也在普通打包时生成文件时使用 + * @since 1.0.0-alpha4.2 + */ + @BuildConfigField @Nonnull public static final String VERSION_FULL = BuildConfig.VERSION_FULL; /** * 程序的基础版本号. *

      @@ -37,6 +49,9 @@ public class MornySystem { * {@link null} 作为值,表示这个字段没有被使用. *

      * 版本 delta 会以 {@code -δversion-delta} 的形式附着在 {@link #VERSION_BASE} 之后. + * 两者合并后的版本号格式即为 {@link #VERSION} + *

      + * 在发行版本中一般不应该被使用. *

      * 目前并不多被使用. * @since 1.0.0-alpha4 diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index d7c4b3a..d07d498 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -217,7 +217,7 @@ public class ServerMain { - version %s - Morny %s - <%s> [%d]""", - MornySystem.VERSION, + MornySystem.VERSION_FULL, MornySystem.CODENAME.toUpperCase(), MornySystem.getJarMd5(), BuildConfig.CODE_TIMESTAMP )); diff --git a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java index c26fca6..ef788b2 100644 --- a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java +++ b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java @@ -9,7 +9,7 @@ public class MornyCLI { public static void main (String[] args) { Scanner line = new Scanner(System.in); - System.out.print("$ java -jar morny-coeur-"+BuildConfig.VERSION+".jar " ); + System.out.print("$ java -jar morny-coeur-"+MornySystem.VERSION_FULL+".jar " ); String x = line.nextLine(); ServerMain.main(UniversalCommand.format(x)); From 618d77746305c19d2d69f6c9644f933a03f6bfaa Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Wed, 9 Nov 2022 14:03:53 +0800 Subject: [PATCH 10/40] fix gradlew exec perm --- gradlew | 0 gradlew.bat | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew mode change 100644 => 100755 gradlew.bat diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/gradlew.bat b/gradlew.bat old mode 100644 new mode 100755 From 01b4eea917c7c2caa6505fe700549bc4ff52994b Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Thu, 10 Nov 2022 23:06:52 +0800 Subject: [PATCH 11/40] =?UTF-8?q?=E4=B8=BAerror=E5=92=8Cexit/save=20403=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=86=20Morny=20Report=EF=BC=8CtrustedCha?= =?UTF-8?q?t=3D=20-1=EF=BC=8Ctypo=20=E4=BB=A5=E5=8F=8A=E6=96=87=E6=A1=88?= =?UTF-8?q?=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化了 coeur 中 exception 的 stackTrace 的输出方式 - 大幅度修改了 EventListenerManager 中报错的逻辑使其更加正常了许多 - 添加了 reportToChat 以及 --report-to 选项用在 MornyReport 中 - 为 coeur 中的错误报告添加了 MornyReport.exception - 为 save/exit 两个需要权限的命令添加了 MornyReport.unauthenticatedAction - 添加了一个方法可以检查 coeur(telegram_bot) 是否已完成初始化以存取 coeur 的内容 --- gradle.properties | 2 +- src/main/java/cc/sukazyo/cono/morny/Log.java | 9 +++ .../cc/sukazyo/cono/morny/MornyCoeur.java | 10 ++- .../cc/sukazyo/cono/morny/MornyConfig.java | 16 +++++ .../cc/sukazyo/cono/morny/MornySystem.java | 4 +- .../cc/sukazyo/cono/morny/MornyTrusted.java | 1 + .../cc/sukazyo/cono/morny/ServerMain.java | 7 ++ .../morny/bot/api/EventListenerManager.java | 33 ++++----- .../cono/morny/bot/command/Encryptor.java | 3 + .../cono/morny/bot/command/MornyCommands.java | 3 + .../cono/morny/bot/command/Testing.java | 2 +- .../cono/morny/bot/command/私わね.java | 13 ++-- .../cono/morny/daemon/MedicationTimer.java | 4 +- .../cono/morny/daemon/MornyReport.java | 67 +++++++++++++++++++ .../cono/morny/daemon/TrackerDataManager.java | 7 +- 15 files changed, 149 insertions(+), 32 deletions(-) create mode 100644 src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java diff --git a/gradle.properties b/gradle.properties index cdee59b..9d43267 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-alpha4.2 +VERSION = 1.0.0-alpha5 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/Log.java b/src/main/java/cc/sukazyo/cono/morny/Log.java index b58c825..f4d1ea4 100644 --- a/src/main/java/cc/sukazyo/cono/morny/Log.java +++ b/src/main/java/cc/sukazyo/cono/morny/Log.java @@ -3,6 +3,9 @@ package cc.sukazyo.cono.morny; import cc.sukazyo.messiva.Logger; import cc.sukazyo.messiva.appender.ConsoleAppender; +import java.io.PrintWriter; +import java.io.StringWriter; + /** * Morny 的 log 管理器 */ @@ -15,4 +18,10 @@ public class Log { */ public static final Logger logger = new Logger(new ConsoleAppender()); + public static String exceptionLog (Exception e) { + final StringWriter stackTrace = new StringWriter(); + e.printStackTrace(new PrintWriter(stackTrace)); + return stackTrace.toString(); + } + } diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java b/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java index 452d0e4..d887b99 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java @@ -223,7 +223,7 @@ public class MornyCoeur { logger.info("Succeed login to @" + remote.username()); return new LogInResult(account, remote.username(), remote.id()); } catch (Exception e) { - e.printStackTrace(System.out); + logger.error(Log.exceptionLog(e)); logger.error("login failed."); } } @@ -239,6 +239,14 @@ public class MornyCoeur { logger.info("done all save action."); } + /** + * 检查 Coeur 是否已经完成初始化. + * @since 1.0.0-alpha5 + */ + public static boolean available() { + return INSTANCE != null; + } + /** * 获取登录成功后的 telegram bot 对象 * diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java index 004302c..0e02f9f 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java @@ -88,6 +88,16 @@ public class MornyConfig { public final boolean commandLoginRefresh; public final boolean commandLogoutClear; + /* ======================================= * + * system: morny report * + * ======================================= */ + + /** + * 控制 Morny Coeur 系统的报告的报告对象. + * @since 1.0.0-alpha5 + */ + public final long reportToChat; + /* ======================================= * * function: dinner query tool * * ======================================= */ @@ -95,6 +105,10 @@ public class MornyConfig { @Nonnull public final Set dinnerTrustedReaders; public final long dinnerChatId; + /* ======================================= * + * End Configs | ConfigBuilder * + * ======================================= */ + public MornyConfig (@Nonnull Prototype prototype) throws CheckFailure { this.telegramBotApiServer = prototype.telegramBotApiServer; this.telegramBotApiServer4File = prototype.telegramBotApiServer4File; @@ -110,6 +124,7 @@ public class MornyConfig { this.commandLogoutClear = prototype.commandLogoutClear; this.dinnerTrustedReaders = prototype.dinnerTrustedReaders; this.dinnerChatId = prototype.dinnerChatId; + this.reportToChat = prototype.reportToChat; } public static class CheckFailure extends Exception { @@ -131,6 +146,7 @@ public class MornyConfig { public boolean commandLogoutClear = false; @Nonnull public Set dinnerTrustedReaders = new HashSet<>(); public long dinnerChatId = -1001707106392L; + public long reportToChat = -1001650050443L; } diff --git a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java index c00fc86..a4e2933 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java @@ -1,5 +1,6 @@ package cc.sukazyo.cono.morny; +import cc.sukazyo.cono.morny.daemon.MornyReport; import cc.sukazyo.cono.morny.util.BuildConfigField; import cc.sukazyo.cono.morny.util.FileUtils; @@ -135,7 +136,8 @@ public class MornySystem { } catch (IOException | URISyntaxException e) { return ""; } catch (NoSuchAlgorithmException e) { - e.printStackTrace(System.out); + Log.logger.error(Log.exceptionLog(e)); + MornyReport.exception(e, ""); return ""; } } diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java b/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java index ae2b3bb..7a43fa0 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java @@ -27,6 +27,7 @@ public class MornyTrusted { */ public boolean isTrusted (long userId) { if (userId == instance.config.trustedMaster) return true; + if (instance.config.trustedChat == -1) return false; return MornyCoeur.extra().isUserInGroup(userId, instance.config.trustedChat, Status.administrator); } diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index d07d498..6707185 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -49,6 +49,9 @@ public class ServerMain { * {@code --api-files} 单独设定 {@link MornyCoeur#getAccount() bot client} 使用的 telegram bot file api server * *

    • + * {@code --report-to} 设定 {@link cc.sukazyo.cono.morny.daemon.MornyReport} 的运行报告要发送到的 telegram 频道 + *
    • + *
    • * {@code --no-hello} 不在主程序启动时输出用于欢迎消息的字符画。 * 与 {@code --only-hello} 参数不兼容 —— 会导致程序完全没有任何输出 *
    • @@ -158,6 +161,10 @@ public class ServerMain { config.telegramBotApiServer4File = args[i]; continue; } + case "--report-to" -> { + config.reportToChat = Long.parseLong(args[i]); + continue; + } } } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java b/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java index 18d0004..671dfd3 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java @@ -1,5 +1,7 @@ package cc.sukazyo.cono.morny.bot.api; +import cc.sukazyo.cono.morny.Log; +import cc.sukazyo.cono.morny.daemon.MornyReport; import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; import com.google.gson.GsonBuilder; import com.pengrad.telegrambot.model.Update; @@ -32,27 +34,20 @@ public class EventListenerManager { if (exec.apply(x)) return; - } catch (EventRuntimeException e) { - - final StringBuilder errorMessage = new StringBuilder(); - errorMessage.append("Event runtime breaks: " + e.getMessage()).append('\n'); - errorMessage.append("at " + e.getStackTrace()[0].toString()).append('\n'); - errorMessage.append("at " + e.getStackTrace()[1].toString()).append('\n'); - errorMessage.append("at " + e.getStackTrace()[2].toString()).append('\n'); - errorMessage.append("at " + e.getStackTrace()[3].toString()).append('\n'); - if (e instanceof EventRuntimeException.ActionFailed) { - errorMessage.append(( - "\"telegram request track\": " + - new GsonBuilder().setPrettyPrinting().create().toJson(((EventRuntimeException.ActionFailed)e).getResponse()) - ).indent(4)).append('\n'); - } - - logger.error(errorMessage.toString()); - } catch (Exception e) { - logger.error("Event Error!"); - e.printStackTrace(System.out); + final StringBuilder errorMessage = new StringBuilder(); + errorMessage.append("Event throws unexpected exception:\n"); + errorMessage.append(Log.exceptionLog(e).indent(4)); + if (e instanceof EventRuntimeException.ActionFailed) { + errorMessage.append("\ntg-api action: response track: "); + errorMessage.append(new GsonBuilder().setPrettyPrinting().create().toJson( + ((EventRuntimeException.ActionFailed)e).getResponse() + ).indent(4)).append('\n'); + } + logger.error(errorMessage.toString()); + + MornyReport.exception(e, "on event running"); } } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java index e770b9f..5ceb213 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java @@ -1,6 +1,7 @@ package cc.sukazyo.cono.morny.bot.command; import cc.sukazyo.cono.morny.MornyCoeur; +import cc.sukazyo.cono.morny.daemon.MornyReport; import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.util.CommonConvert; import cc.sukazyo.cono.morny.util.CommonEncrypt; @@ -88,6 +89,7 @@ public class Encryptor implements ITelegramCommand { )).file()); } catch (IOException e) { logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage()); + MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI"); MornyCoeur.extra().exec(new SendSticker( event.message().chat().id(), TelegramStickers.ID_NETWORK_ERR @@ -110,6 +112,7 @@ public class Encryptor implements ITelegramCommand { )).file()); } catch (IOException e) { logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage()); + MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI"); MornyCoeur.extra().exec(new SendSticker( event.message().chat().id(), TelegramStickers.ID_NETWORK_ERR diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index f5401fc..5925046 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.command; import cc.sukazyo.cono.morny.BuildConfig; import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.MornySystem; +import cc.sukazyo.cono.morny.daemon.MornyReport; import cc.sukazyo.cono.morny.data.MornyJrrp; import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.util.tgapi.InputCommand; @@ -220,6 +221,7 @@ public class MornyCommands { ).replyToMessageId(event.message().messageId()) ); logger.info("403 exited tag from user " + TGToString.as(event.message().from()).toStringLogTag()); + MornyReport.unauthenticatedAction("/exit", event.message().from()); } } @@ -375,6 +377,7 @@ public class MornyCommands { ).replyToMessageId(event.message().messageId()) ); logger.info("403 call save tag from user " + TGToString.as(event.message().from()).toStringLogTag()); + MornyReport.unauthenticatedAction("/save", event.message().from()); } } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java index d1bfb80..8039ce7 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java @@ -28,7 +28,7 @@ public class Testing implements ISimpleCommand { MornyCoeur.extra().exec(new SendMessage( event.message().chat().id(), - "Just a TEST command." + "Just a TEST command." ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java index 0c92c84..b776046 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java @@ -22,12 +22,13 @@ public class 私わね implements ISimpleCommand { public void execute (@Nonnull InputCommand command, @Nonnull Update event) { if (ThreadLocalRandom.current().nextInt(521) == 0) { // 可以接入未来的心情系统(如果有的话) - final String text = switch (ThreadLocalRandom.current().nextInt(11)) { - case 0,7,8,9,10 -> "才不是"; - case 1,2,3,6 -> "才不是!"; - case 4,5 -> "才不是.."; - default -> throw new IllegalStateException("Unexpected random value in 私わね command."); - }; +// final String text = switch (ThreadLocalRandom.current().nextInt(11)) { +// case 0,7,8,9,10 -> "才不是"; +// case 1,2,3,6 -> "才不是!"; +// case 4,5 -> "才不是.."; +// default -> throw new IllegalStateException("Unexpected random value in 私わね command."); +// }; + final String text = "/打假"; MornyCoeur.extra().exec(new SendMessage( event.message().chat().id(), text diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java index 813640b..5faba4a 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java @@ -12,6 +12,7 @@ import com.pengrad.telegrambot.response.SendResponse; import java.util.ArrayList; import java.util.List; +import static cc.sukazyo.cono.morny.Log.exceptionLog; import static cc.sukazyo.cono.morny.Log.logger; public class MedicationTimer extends Thread { @@ -40,7 +41,8 @@ public class MedicationTimer extends Thread { logger.info("MedicationTimer was interrupted, will be exit now"); } catch (Exception e) { logger.error("Unexpected error occurred"); - e.printStackTrace(System.out); + logger.error(exceptionLog(e)); + MornyReport.exception(e); } } logger.info("MedicationTimer stopped"); diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java new file mode 100644 index 0000000..a6d37e2 --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java @@ -0,0 +1,67 @@ +package cc.sukazyo.cono.morny.daemon; + +import cc.sukazyo.cono.morny.Log; +import cc.sukazyo.cono.morny.MornyCoeur; +import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; +import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; +import com.google.gson.GsonBuilder; +import com.pengrad.telegrambot.model.User; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.BaseRequest; +import com.pengrad.telegrambot.request.SendMessage; +import com.pengrad.telegrambot.response.BaseResponse; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; + +public class MornyReport { + + private static , R extends BaseResponse> void executeReport (@Nonnull T report) { + if (!MornyCoeur.available()) return; + try { + MornyCoeur.extra().exec(report); + } catch (EventRuntimeException.ActionFailed e) { + Log.logger.warn("cannot execute report to telegram:"); + Log.logger.warn(Log.exceptionLog(e)); + } + } + + public static void exception (@Nonnull Exception e, @Nullable String description) { + if (!MornyCoeur.available()) return; + executeReport(new SendMessage( + MornyCoeur.config().reportToChat, + String.format(""" + ▌Coeur Unexpected Exception + %s +
      %s
      %s + """, + description == null ? "" : escapeHtml(description)+"\n", + escapeHtml(Log.exceptionLog(e)), + e instanceof EventRuntimeException.ActionFailed ? (String.format( + "\n\ntg-api error:\n
      %s
      ", + new GsonBuilder().setPrettyPrinting().create().toJson(((EventRuntimeException.ActionFailed)e).getResponse())) + ) : "" + ) + ).parseMode(ParseMode.HTML)); + } + + public static void exception (@Nonnull Exception e) { exception(e, null); } + + public static void unauthenticatedAction (@Nonnull String action, @Nonnull User user) { + if (!MornyCoeur.available()) return; + executeReport(new SendMessage( + MornyCoeur.config().reportToChat, + String.format(""" + ▌User unauthenticated action + action: %s + by user %s + """, + escapeHtml(action), + TGToString.as(user).fullnameRefHtml() + ) + ).parseMode(ParseMode.HTML)); + } + +} diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java b/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java index d0f8ab0..264a69a 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java @@ -10,6 +10,7 @@ import java.util.HashMap; import java.util.TreeSet; import java.util.concurrent.locks.ReentrantLock; +import static cc.sukazyo.cono.morny.Log.exceptionLog; import static cc.sukazyo.cono.morny.Log.logger; public class TrackerDataManager { @@ -113,8 +114,10 @@ public class TrackerDataManager { )); } catch (Exception e) { - logger.error(String.format("exception in write tracker data: %d/%d/%d", chat, user, timestamp)); - e.printStackTrace(System.out); + final String message = String.format("exception in write tracker data: %d/%d/%d", chat, user, timestamp); + logger.error(message); + logger.error(exceptionLog(e)); + MornyReport.exception(e, message); } } From 3689962cb3fd876bcffe6f6a923ceafd2e97e646 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Fri, 11 Nov 2022 16:37:24 +0800 Subject: [PATCH 12/40] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=20morny=20log?= =?UTF-8?q?in/exit=20=E7=9A=84=E6=8A=A5=E5=91=8A=EF=BC=8C*msg=20=E7=8E=B0?= =?UTF-8?q?=E5=9C=A8=E6=94=AF=E6=8C=81=E4=BA=86=E5=8F=AA=E5=8C=85=E5=90=AB?= =?UTF-8?q?=E5=8F=91=E9=80=81=20id=20=E4=B8=8D=E5=8C=85=E5=90=AB=E5=8F=91?= =?UTF-8?q?=E9=80=81=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 MornyCoeur 添加了 whileExitReason (exit/getExitReason) 接口,用于安全退出时指定推出原因 - MornyConfig 添加了一个 Sensitive 注解用于标明字段值属于敏感字段不应该被打印 - --- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/MornyCoeur.java | 12 ++- .../cc/sukazyo/cono/morny/MornyConfig.java | 11 ++- .../cono/morny/bot/command/MornyCommands.java | 2 +- .../cono/morny/bot/event/OnCallMsgSend.java | 13 +-- .../cono/morny/daemon/MornyDaemons.java | 4 + .../cono/morny/daemon/MornyReport.java | 91 ++++++++++++++++++- 7 files changed, 123 insertions(+), 12 deletions(-) diff --git a/gradle.properties b/gradle.properties index 9d43267..d8d160f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-alpha5 +VERSION = 1.0.0-alpha6 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java b/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java index d887b99..49f8655 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java @@ -60,6 +60,8 @@ public class MornyCoeur { */ public static final long coeurStartTimestamp = ServerMain.systemStartupTime; + private Object whileExitReason = null; + private record LogInResult(TelegramBot account, String username, long userid) { } /** @@ -152,7 +154,6 @@ public class MornyCoeur { * 用于退出时进行缓存的任务处理等进行安全退出 */ private void exitCleanup () { - logger.info("clean:save tracker data."); MornyDaemons.stop(); if (config.commandLogoutClear) { commandManager.automaticRemoveList(); @@ -304,4 +305,13 @@ public class MornyCoeur { public static long getUserid () { return INSTANCE.userid; } + public static void exit (int status, Object reason) { + INSTANCE.whileExitReason = reason; + System.exit(status); + } + + public static Object getExitReason () { + return INSTANCE.whileExitReason; + } + } diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java index 0e02f9f..be61e15 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java @@ -2,11 +2,20 @@ package cc.sukazyo.cono.morny; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.lang.annotation.*; import java.util.HashSet; import java.util.Set; public class MornyConfig { + /** + * 表示一个字段的值属于敏感数据,不应该被执行打印等操作。 + */ + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Target({ElementType.FIELD, ElementType.METHOD}) + public @interface Sensitive {} + /* ======================================= * * Config props Names Definition * * ======================================= */ @@ -37,7 +46,7 @@ public class MornyConfig { *

      * 这个值必须设定。 */ - @Nonnull public final String telegramBotKey; + @Nonnull @Sensitive public final String telegramBotKey; /** * morny 所使用的 bot 的 username. *

      diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index 5925046..9ad3087 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -213,7 +213,7 @@ public class MornyCommands { ).replyToMessageId(event.message().messageId()) ); logger.info("Morny exited by user " + TGToString.as(event.message().from()).toStringLogTag()); - System.exit(0); + MornyCoeur.exit(0, event.message().from()); } else { MornyCoeur.extra().exec(new SendSticker( event.message().chat().id(), diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java index 0b362f9..b4eaaec 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java @@ -27,12 +27,12 @@ import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; public class OnCallMsgSend extends EventListener { - private static final Pattern REGEX_MSG_SENDREQ_DATA_HEAD = Pattern.compile("^\\*msg([\\d-]+)(\\*\\S+)?\\n([\\s\\S]+)$"); + private static final Pattern REGEX_MSG_SENDREQ_DATA_HEAD = Pattern.compile("^\\*msg(-?\\d+)(\\*\\S+)?(?:\\n([\\s\\S]+))?$"); private record MessageToSend ( - String message, - MessageEntity[] entities, - ParseMode parseMode, + @Nullable String message, + @Nullable MessageEntity[] entities, + @Nullable ParseMode parseMode, long targetId ) { } @@ -62,7 +62,7 @@ public class OnCallMsgSend extends EventListener { // 发送体处理 if (update.message().replyToMessage() == null) return answer404(update); msgsendReqBody = parseRequest(update.message().replyToMessage()); - if (msgsendReqBody == null) return answer404(update); + if (msgsendReqBody == null || msgsendReqBody.message == null) return answer404(update); // 执行发送任务 SendResponse sendResponse = MornyCoeur.getAccount().execute(parseMessageToSend(msgsendReqBody)); if (!sendResponse.isOk()) { // 发送失败 @@ -150,7 +150,8 @@ public class OnCallMsgSend extends EventListener { ).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); } // 发送文本测试 - SendResponse testSendResp = MornyCoeur.getAccount().execute( + if (msgsendReqBody.message == null) return true; + final SendResponse testSendResp = MornyCoeur.getAccount().execute( parseMessageToSend(msgsendReqBody, update.message().chat().id()).replyToMessageId(update.message().messageId()) ); if (!testSendResp.isOk()) { diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyDaemons.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyDaemons.java index d40ba47..710a07a 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyDaemons.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyDaemons.java @@ -1,5 +1,7 @@ package cc.sukazyo.cono.morny.daemon; +import cc.sukazyo.cono.morny.MornyCoeur; + import static cc.sukazyo.cono.morny.Log.logger; public class MornyDaemons { @@ -10,6 +12,7 @@ public class MornyDaemons { logger.info("ALL Morny Daemons starting..."); // TrackerDataManager.init(); medicationTimerInstance.start(); + MornyReport.onMornyLogIn(); logger.info("Morny Daemons started."); } @@ -23,6 +26,7 @@ public class MornyDaemons { // TrackerDataManager.trackingLock.lock(); try { medicationTimerInstance.join(); } catch (InterruptedException e) { e.printStackTrace(System.out); } + MornyReport.onMornyExit(MornyCoeur.getExitReason()); logger.info("ALL Morny Daemons STOPPED."); } diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java index a6d37e2..c79b223 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java @@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.daemon; import cc.sukazyo.cono.morny.Log; import cc.sukazyo.cono.morny.MornyCoeur; +import cc.sukazyo.cono.morny.MornyConfig; import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; import com.google.gson.GsonBuilder; @@ -14,6 +15,9 @@ import com.pengrad.telegrambot.response.BaseResponse; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.lang.reflect.Field; + +import static cc.sukazyo.cono.morny.Log.logger; import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; public class MornyReport { @@ -23,8 +27,10 @@ public class MornyReport { try { MornyCoeur.extra().exec(report); } catch (EventRuntimeException.ActionFailed e) { - Log.logger.warn("cannot execute report to telegram:"); - Log.logger.warn(Log.exceptionLog(e)); + logger.warn("cannot execute report to telegram:"); + logger.warn(Log.exceptionLog(e).indent(4)); + logger.warn("tg-api response:"); + logger.warn(e.getResponse().toString().indent(4)); } } @@ -64,4 +70,85 @@ public class MornyReport { ).parseMode(ParseMode.HTML)); } + /** + * morny 登陆时的报告发送,包含已登录的账号 id 以及启动配置。 + * @since 1.0.0-alpha6 + */ + public static void onMornyLogIn () { + executeReport(new SendMessage( + MornyCoeur.config().reportToChat, + String.format(""" + ▌Morny Logged in + as user @%s + + as config fields: + %s + """, + MornyCoeur.getUsername(), + sectionConfigFields(MornyCoeur.config()) + ) + ).parseMode(ParseMode.HTML)); + } + + /** + * 返回一个 config 字段与值的列表,可以作为 telegram html 格式输出 + * @since 1.0.0-alpha6 + */ + private static String sectionConfigFields (@Nonnull MornyConfig config) { + final StringBuilder echo = new StringBuilder(); + for (Field field : config.getClass().getFields()) { + echo.append("- ").append(field.getName()).append(" "); + try { + if (field.isAnnotationPresent(MornyConfig.Sensitive.class)) { + echo.append(": sensitive_field"); + } else { + final Object fieldValue = field.get(config); + echo.append("= "); + if (fieldValue == null) + echo.append("null"); + else echo.append("").append(escapeHtml(fieldValue.toString())).append(""); + } + } catch (IllegalAccessException | IllegalArgumentException | NullPointerException e) { + echo.append(": ").append(escapeHtml("")).append(""); + logger.error("error while reading config field " + field.getName()); + logger.error(Log.exceptionLog(e)); + exception(e, "error while reading config field " + field.getName()); + } + echo.append("\n"); + } + return echo.substring(0, echo.length()-1); + } + + /** + * morny 关闭/登出时发送的报告. + *

      + * 基于 java 的程序关闭钩子,因此仍然无法在意外宕机的情况下发送报告. + * @param causedBy + * 关闭的原因。 + * 可以使用 {@link User Telegram 用户对象} 表示由一个用户执行了关闭, + * 传入其它数据将使用 {@code #toString} 输出其内容。 + * 传入 {@link null} 则表示不表明原因。 + */ + static void onMornyExit (@Nullable Object causedBy) { + if (!MornyCoeur.available()) return; + String causedTag = null; + if (causedBy != null) { + if (causedBy instanceof User) + causedTag = TGToString.as((User)causedBy).fullnameRefHtml(); + else + causedTag = "" + escapeHtml(causedBy.toString()) + ""; + } + executeReport(new SendMessage( + MornyCoeur.config().reportToChat, + String.format(""" + ▌Morny Exited + from user @%s + %s + """, + MornyCoeur.getUsername(), + causedBy == null ? "with UNKNOWN reason" : "\nby " + causedTag + ) + ).parseMode(ParseMode.HTML)); + } + } From fbbfe73ac12cb72807c447b85109cb438d332785 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Fri, 11 Nov 2022 18:32:04 +0800 Subject: [PATCH 13/40] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20"meow-set"?= =?UTF-8?q?=20=E9=83=A8=E5=88=86=E7=9A=84=E6=96=87=E6=A1=88=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E4=B8=BA=E5=85=B6=E6=B7=BB=E5=8A=A0=E4=BA=86=E7=89=B9?= =?UTF-8?q?=E5=BC=82=E5=8C=96=E5=9B=9E=E5=A4=8D=E7=9A=84=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=85=B6=E4=BB=A5=E5=8F=8A=20--repo?= =?UTF-8?q?rt-to=20=E7=9A=84=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复了上次提交的 --report-to 参数没有被正确处理问题 - 创建了一个新的补丁事件,修复了 meow-set 无法使用的问题 --- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/ServerMain.java | 1 + .../cono/morny/bot/command/MornyCommands.java | 10 +++-- .../sukazyo/cono/morny/bot/command/喵呜.java | 40 +++++++++++-------- .../cono/morny/bot/event/EventListeners.java | 2 + .../morny/bot/event/OnUniMeowTrigger.java | 36 +++++++++++++++++ 6 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java diff --git a/gradle.properties b/gradle.properties index d8d160f..50b2c53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-alpha6 +VERSION = 1.0.0-beta1 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index 6707185..e9237ee 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -162,6 +162,7 @@ public class ServerMain { continue; } case "--report-to" -> { + i++; config.reportToChat = Long.parseLong(args[i]); continue; } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index 9ad3087..e50db1b 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.command; import cc.sukazyo.cono.morny.BuildConfig; import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.MornySystem; +import cc.sukazyo.cono.morny.bot.event.OnUniMeowTrigger; import cc.sukazyo.cono.morny.daemon.MornyReport; import cc.sukazyo.cono.morny.data.MornyJrrp; import cc.sukazyo.cono.morny.data.TelegramStickers; @@ -83,12 +84,15 @@ public class MornyCommands { // 统一注册这些奇怪的东西&.& register( + new 私わね(), + new 喵呜.Progynova() + ); + // special: 注册出于兼容使用的特别 event 的数据 + OnUniMeowTrigger.register( new 喵呜.抱抱(), new 喵呜.揉揉(), new 喵呜.蹭蹭(), - new 喵呜.贴贴(), - new 私わね(), - new 喵呜.Progynova() + new 喵呜.贴贴() ); } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java index 2bdbb7d..692ce2a 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.command; import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.util.tgapi.InputCommand; +import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.SendMessage; @@ -11,6 +12,16 @@ import com.pengrad.telegrambot.request.SendSticker; import javax.annotation.Nonnull; import javax.annotation.Nullable; +/** + * WARNING that {@link cc.sukazyo.cono.morny.bot.event.OnTelegramCommand} + * 并不能够处理非 english word 字符之外的命令. + *

      + * 出于这个限制,以下几个命令目前都无法使用 + * @see 抱抱 + * @see 揉揉 + * @see 蹭蹭 + * @see 贴贴 + */ @SuppressWarnings("NonAsciiCharacters") public class 喵呜 { @@ -18,10 +29,7 @@ public class 喵呜 { @Nonnull @Override public String getName () { return "抱抱"; } @Nullable @Override public String[] getAliases () { return new String[0]; } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "抱抱——" - )); + replyingSet(event, "抱抱", "抱抱"); } } @@ -29,10 +37,7 @@ public class 喵呜 { @Nonnull @Override public String getName () { return "揉揉"; } @Nullable @Override public String[] getAliases () { return new String[0]; } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "蹭蹭w" - )); + replyingSet(event, "蹭蹭", "摸摸"); } } @@ -40,10 +45,7 @@ public class 喵呜 { @Nonnull @Override public String getName () { return "蹭蹭"; } @Nullable @Override public String[] getAliases () { return new String[0]; } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "喵呜~-" - )); + replyingSet(event, "揉揉", "蹭蹭"); } } @@ -51,13 +53,19 @@ public class 喵呜 { @Nonnull @Override public String getName () { return "贴贴"; } @Nullable @Override public String[] getAliases () { return new String[0]; } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "(贴贴喵呜&.&)" - ).parseMode(ParseMode.HTML)); + replyingSet(event, "贴贴", "贴贴"); } } + private static void replyingSet (@Nonnull Update event, @Nonnull String whileRec, @Nonnull String whileNew) { + final boolean isNew = event.message().replyToMessage() == null; + final Message target = isNew ? event.message() : event.message().replyToMessage(); + MornyCoeur.extra().exec(new SendMessage( + event.message().chat().id(), + isNew ? whileNew : whileRec + ).replyToMessageId(target.messageId()).parseMode(ParseMode.HTML)); + } + public static class Progynova implements ITelegramCommand { @Nonnull @Override public String getName () { return "install"; } @Nullable @Override public String[] getAliases () { return new String[0]; } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java index 7b165f9..389b037 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java @@ -16,6 +16,7 @@ public class EventListeners { public static final OnCallMsgSend CALL_MSG_SEND = new OnCallMsgSend(); public static final OnMedicationNotifyApply MEDICATION_NOTIFY_APPLY = new OnMedicationNotifyApply(); public static final OnRandomlyTriggered RANDOMLY_TRIGGERED = new OnRandomlyTriggered(); + public static final OnUniMeowTrigger UNI_MEOW_TRIGGER = new OnUniMeowTrigger(); public static void registerAllListeners () { EventListenerManager.addListener( @@ -24,6 +25,7 @@ public class EventListeners { /* write functional event behind here */ // KUOHUANHUAN_NEED_SLEEP, COMMANDS_LISTENER, + UNI_MEOW_TRIGGER, RANDOMLY_TRIGGERED, USER_RANDOMS, USER_SLASH_ACTION, diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java new file mode 100644 index 0000000..59e8be1 --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java @@ -0,0 +1,36 @@ +package cc.sukazyo.cono.morny.bot.event; + +import cc.sukazyo.cono.morny.bot.api.EventListener; +import cc.sukazyo.cono.morny.bot.command.ISimpleCommand; +import cc.sukazyo.cono.morny.util.tgapi.InputCommand; +import com.pengrad.telegrambot.model.Update; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +public class OnUniMeowTrigger extends EventListener { + + private static final Map triggers = new HashMap<>(); + + public static void register (ISimpleCommand... list) { + for (ISimpleCommand cmd : list) + triggers.put(cmd.getName(), cmd); + } + + @Override + public boolean onMessage (@Nonnull Update event) { + if (event.message().text() == null) return false; + AtomicBoolean ok = new AtomicBoolean(false); + triggers.forEach((name, command) -> { + name = "/" + name; + if (name.equals(event.message().text())) { + command.execute(new InputCommand(name), event); + ok.set(true); + } + }); + return ok.get(); + } + +} From c4a709491d2ed635d9371ae9ed09d22309c207e1 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 12 Nov 2022 13:42:23 +0800 Subject: [PATCH 14/40] =?UTF-8?q?=E4=B8=BA=20loginReport=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86=E7=89=88=E6=9C=AC=E5=8F=B7=EF=BC=8C=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20version/runtime=20=E7=9A=84=E5=86=85=E9=83=A8?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改了 system 的显示方式将 os名称/版本/架构 移为了一行 - 将 version/runtime 的执行体移动到了 MornyInformations 当中 - MornyInformations 中对一些复杂的 version/runtime 部件为其独立出了一个方法 --- gradle.properties | 2 +- .../cono/morny/bot/command/MornyCommands.java | 100 +----------- .../morny/bot/command/MornyInformations.java | 147 ++++++++++++++++-- .../cono/morny/daemon/MornyReport.java | 9 +- 4 files changed, 144 insertions(+), 114 deletions(-) diff --git a/gradle.properties b/gradle.properties index 50b2c53..c087a4f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-beta1 +VERSION = 1.0.0-beta2 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index e50db1b..5e14712 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -1,8 +1,6 @@ package cc.sukazyo.cono.morny.bot.command; -import cc.sukazyo.cono.morny.BuildConfig; import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.MornySystem; import cc.sukazyo.cono.morny.bot.event.OnUniMeowTrigger; import cc.sukazyo.cono.morny.daemon.MornyReport; import cc.sukazyo.cono.morny.data.MornyJrrp; @@ -21,13 +19,9 @@ import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.util.*; import static cc.sukazyo.cono.morny.Log.logger; -import static cc.sukazyo.cono.morny.util.CommonFormat.formatDate; -import static cc.sukazyo.cono.morny.util.CommonFormat.formatDuration; import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; public class MornyCommands { @@ -234,33 +228,7 @@ public class MornyCommands { @Nullable @Override public String[] getAliases () { return null; } @Nonnull @Deprecated public String getParamRule () { return ""; } @Nonnull @Deprecated public String getDescription () { return "检查 Bot 版本信息"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandVersionExec(event); } - } - public static void onCommandVersionExec (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format( - """ - version: - - Morny %s - - %s%s%s - core md5_hash: - - %s - coding timestamp: - - %d - - %s [UTC]""", - escapeHtml(MornySystem.CODENAME.toUpperCase()), - escapeHtml(MornySystem.VERSION_BASE), - MornySystem.isUseDelta() ? String.format("-δ%s", escapeHtml(Objects.requireNonNull(MornySystem.VERSION_DELTA))) : "", - MornySystem.isGitBuild()?"\n- git "+ (MornySystem.currentCodePath()==null ? - String.format("%s", BuildConfig.COMMIT.substring(0, 8)) : - String.format("%s", MornySystem.currentCodePath(), BuildConfig.COMMIT.substring(0, 8)) - ) + (MornySystem.isCleanBuild() ? "" : ".δ") : "", - escapeHtml(MornySystem.getJarMd5()), - BuildConfig.CODE_TIMESTAMP, - escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)) - ) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); + @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformations.echoVersion(event); } } private static class MornyRuntime implements ISimpleCommand { @@ -268,71 +236,7 @@ public class MornyCommands { @Nullable @Override public String[] getAliases () { return null; } @Nonnull @Deprecated public String getParamRule () { return ""; } @Nonnull @Deprecated public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandRuntimeExec(event); } - } - /** - * @since 0.4.1.2 - */ - public static void onCommandRuntimeExec (@Nonnull Update event) { - String hostname; - try { - hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - hostname = ""; - } - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format(""" - system: - - %s - - %s - - %s - java runtime: - - %s - - %s - vm memory: - - %d / %d MB - - %d cores - coeur version: - - %s%s%s*%s - - %s - - %s [UTC] - - [%d] - continuous: - - %s - - [%d] - - %s [UTC] - - [%d]""", - // system - escapeHtml(hostname), - escapeHtml(String.format("%s (%s)", System.getProperty("os.name"), System.getProperty("os.arch"))), - escapeHtml(System.getProperty("os.version")), - // java - escapeHtml(System.getProperty("java.vm.vendor")+"."+System.getProperty("java.vm.name")), - escapeHtml(System.getProperty("java.vm.version")), - // memory - Runtime.getRuntime().totalMemory() / 1024 / 1024, - Runtime.getRuntime().maxMemory() / 1024 / 1024, - Runtime.getRuntime().availableProcessors(), - // version - escapeHtml(MornySystem.VERSION_BASE), - MornySystem.isUseDelta() ? String.format("-δ%s", escapeHtml(Objects.requireNonNull(MornySystem.VERSION_DELTA))) : "", - MornySystem.isGitBuild()?String.format("-git.%s%s", MornySystem.currentCodePath()==null ? ( - String.format("%s", BuildConfig.COMMIT.substring(0, 8)) - ) : ( - String.format("%s", MornySystem.currentCodePath(), BuildConfig.COMMIT.substring(0, 8)) - ), MornySystem.isCleanBuild()?"":".δ"):"", - escapeHtml(MornySystem.CODENAME.toUpperCase()), - escapeHtml(MornySystem.getJarMd5()), - escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)), - BuildConfig.CODE_TIMESTAMP, - // continuous - escapeHtml(formatDuration(System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp)), - System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp, - escapeHtml(formatDate(MornyCoeur.coeurStartTimestamp, 0)), - MornyCoeur.coeurStartTimestamp - ) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); + @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformations.echoRuntime(event); } } private static class Jrrp implements ITelegramCommand { diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java index 5558f6e..e9a6ad2 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java @@ -1,14 +1,25 @@ package cc.sukazyo.cono.morny.bot.command; +import cc.sukazyo.cono.morny.BuildConfig; import cc.sukazyo.cono.morny.MornyCoeur; +import cc.sukazyo.cono.morny.MornySystem; import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.util.tgapi.ExtraAction; import cc.sukazyo.cono.morny.util.tgapi.InputCommand; import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.request.SendSticker; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Objects; + +import static cc.sukazyo.cono.morny.util.CommonFormat.formatDate; +import static cc.sukazyo.cono.morny.util.CommonFormat.formatDuration; +import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; public class MornyInformations implements ITelegramCommand { @@ -26,7 +37,7 @@ public class MornyInformations implements ITelegramCommand { public void execute (@Nonnull InputCommand command, @Nonnull Update event) { if (!command.hasArgs()) { - MornyCommands.onCommandRuntimeExec(event); + echoRuntime(event); return; } @@ -35,9 +46,9 @@ public class MornyInformations implements ITelegramCommand { if (action.startsWith(SUB_STICKER)) { echoStickers(command, event); } else if (action.equals(SUB_RUNTIME)) { - echoRuntime(command, event); + echoRuntime(event); } else if (action.equals(SUB_VERSION) || action.equals(SUB_VERSION_2)) { - echoVersion(command, event); + echoVersion(event); } else { echo404(event); } @@ -85,20 +96,134 @@ public class MornyInformations implements ITelegramCommand { * /info 子命令 {@value #SUB_RUNTIME} * @since 1.0.0-alpha4 */ - public static void echoRuntime (@Nonnull InputCommand command, @Nonnull Update event) { - if (command.getArgs().length == 1) - MornyCommands.onCommandRuntimeExec(event); - else echo404(event); + public static void echoRuntime (@Nonnull Update event) { + MornyCoeur.extra().exec(new SendMessage( + event.message().chat().id(), + String.format(""" + system: + - %s + - %s (%s) %s + java runtime: + - %s + - %s + vm memory: + - %d / %d MB + - %d cores + coeur version: + - %s + - %s + - %s [UTC] + - [%d] + continuous: + - %s + - [%d] + - %s [UTC] + - [%d]""", + // system + escapeHtml(getRuntimeHostName()==null ? "" : getRuntimeHostName()), + escapeHtml(System.getProperty("os.name")), + escapeHtml(System.getProperty("os.arch")), + escapeHtml(System.getProperty("os.version")), + // java + escapeHtml(System.getProperty("java.vm.vendor")+"."+System.getProperty("java.vm.name")), + escapeHtml(System.getProperty("java.vm.version")), + // memory + Runtime.getRuntime().totalMemory() / 1024 / 1024, + Runtime.getRuntime().maxMemory() / 1024 / 1024, + Runtime.getRuntime().availableProcessors(), + // version + getVersionAllFullTagHtml(), + escapeHtml(MornySystem.getJarMd5()), + escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)), + BuildConfig.CODE_TIMESTAMP, + // continuous + escapeHtml(formatDuration(System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp)), + System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp, + escapeHtml(formatDate(MornyCoeur.coeurStartTimestamp, 0)), + MornyCoeur.coeurStartTimestamp + ) + ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); } /** * /info 子命令 {@value #SUB_VERSION} * @since 1.0.0-alpha4 */ - public static void echoVersion (@Nonnull InputCommand command, @Nonnull Update event) { - if (command.getArgs().length == 1) - MornyCommands.onCommandVersionExec(event); - else echo404(event); + public static void echoVersion (@Nonnull Update event) { + MornyCoeur.extra().exec(new SendMessage( + event.message().chat().id(), + String.format( + """ + version: + - Morny %s + - %s%s%s + core md5_hash: + - %s + coding timestamp: + - %d + - %s [UTC]""", + escapeHtml(MornySystem.CODENAME.toUpperCase()), + escapeHtml(MornySystem.VERSION_BASE), + MornySystem.isUseDelta() ? String.format("-δ%s", escapeHtml(Objects.requireNonNull(MornySystem.VERSION_DELTA))) : "", + MornySystem.isGitBuild() ? "\n- git "+getVersionGitTagHtml() : "", + escapeHtml(MornySystem.getJarMd5()), + BuildConfig.CODE_TIMESTAMP, + escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)) + ) + ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); + } + + /** + * 取得 {@link MornySystem} 的 git commit 相关版本信息的 HTML 格式化标签. + * @return 格式类似于 {@code 28e8c82a.δ} 的以 HTML 方式格式化的版本号组件。 + * 其中 {@code .δ} 对应着 {@link MornySystem#isCleanBuild}; + * commit tag 字段如果支援 {@link MornySystem#currentCodePath} 则会以链接形式解析,否则则为 code 格式 + * 为了对 telegram api html 格式兼容所以不支援嵌套链接与code标签。 + * 如果 {@link MornySystem#isGitBuild} 为 {@link false},则方法会返回 {@link ""} + * @since 1.0.0-beta2 + */ + @Nonnull + public static String getVersionGitTagHtml () { + if (!MornySystem.isGitBuild()) return ""; + final StringBuilder g = new StringBuilder(); + final String cp = MornySystem.currentCodePath(); + if (cp == null) g.append(String.format("%s", BuildConfig.COMMIT.substring(0, 8))); + else g.append(String.format("%s", MornySystem.currentCodePath(), BuildConfig.COMMIT.substring(0, 8))); + if (!MornySystem.isCleanBuild()) g.append(".δ"); + return g.toString(); + } + + /** + * 取得完整 Morny 版本的 HTML 格式化标签. + *

      + * 相比于 {@link MornySystem#VERSION_FULL},这个版本号还包含了 {@link MornySystem#CODENAME 版本 codename}。 + * 各个部分也被以 HTML 的格式进行了格式化以可以更好的在富文本中插入使用. + * @return 基于 HTML 标签进行了格式化了的类似于 + * {@link MornySystem#VERSION_BASE 5.38.2-alpha1}{@link MornySystem#isUseDelta() -δ}{@link MornySystem#VERSION_DELTA tt}{@link MornySystem#isGitBuild() +git.}{@link #getVersionGitTagHtml() 28e8c82a.δ}*{@link MornySystem#CODENAME TOKYO} + * 的版本号。 + * @since 1.0.0-beta2 + */ + @Nonnull + public static String getVersionAllFullTagHtml () { + final StringBuilder v = new StringBuilder(); + v.append("").append(MornySystem.VERSION_BASE).append(""); + if (MornySystem.isUseDelta()) v.append("-δ").append(MornySystem.VERSION_DELTA).append(""); + if (MornySystem.isGitBuild()) v.append("+git.").append(getVersionGitTagHtml()); + v.append("*").append(MornySystem.CODENAME.toUpperCase()).append(""); + return v.toString(); + } + + /** + * 获取 coeur 运行时的宿主机的主机名 + * @return coeur 宿主机主机名,或者 {@link null} 表示获取失败 + */ + @Nullable + public static String getRuntimeHostName () { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + return null; + } } private static void echo404 (@Nonnull Update event) { diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java index c79b223..b4f7ae5 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java @@ -1,8 +1,7 @@ package cc.sukazyo.cono.morny.daemon; -import cc.sukazyo.cono.morny.Log; -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.MornyConfig; +import cc.sukazyo.cono.morny.*; +import cc.sukazyo.cono.morny.bot.command.MornyInformations; import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; import com.google.gson.GsonBuilder; @@ -74,16 +73,18 @@ public class MornyReport { * morny 登陆时的报告发送,包含已登录的账号 id 以及启动配置。 * @since 1.0.0-alpha6 */ - public static void onMornyLogIn () { + static void onMornyLogIn () { executeReport(new SendMessage( MornyCoeur.config().reportToChat, String.format(""" ▌Morny Logged in + -v %s as user @%s as config fields: %s """, + MornyInformations.getVersionAllFullTagHtml(), MornyCoeur.getUsername(), sectionConfigFields(MornyCoeur.config()) ) From a83930efd0fb4cacb9bb965a58d48a11643a4e15 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 12 Nov 2022 17:40:51 +0800 Subject: [PATCH 15/40] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9C=AA=E9=A2=84?= =?UTF-8?q?=E8=A7=81=20--report-to=20=E8=AE=BE=E7=BD=AE=E4=B8=BA=20-1=20?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/daemon/MornyReport.java | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index 22b2091..798df0e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC1 +VERSION = 1.0.0-RC1.1 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java index b4f7ae5..0aa990e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java @@ -21,8 +21,12 @@ import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; public class MornyReport { + private static boolean unsupported () { + return !MornyCoeur.available() || MornyCoeur.config().reportToChat == -1; + } + private static , R extends BaseResponse> void executeReport (@Nonnull T report) { - if (!MornyCoeur.available()) return; + if (unsupported()) return; try { MornyCoeur.extra().exec(report); } catch (EventRuntimeException.ActionFailed e) { @@ -34,7 +38,7 @@ public class MornyReport { } public static void exception (@Nonnull Exception e, @Nullable String description) { - if (!MornyCoeur.available()) return; + if (unsupported()) return; executeReport(new SendMessage( MornyCoeur.config().reportToChat, String.format(""" @@ -55,7 +59,7 @@ public class MornyReport { public static void exception (@Nonnull Exception e) { exception(e, null); } public static void unauthenticatedAction (@Nonnull String action, @Nonnull User user) { - if (!MornyCoeur.available()) return; + if (unsupported()) return; executeReport(new SendMessage( MornyCoeur.config().reportToChat, String.format(""" @@ -131,7 +135,7 @@ public class MornyReport { * 传入 {@link null} 则表示不表明原因。 */ static void onMornyExit (@Nullable Object causedBy) { - if (!MornyCoeur.available()) return; + if (unsupported()) return; String causedTag = null; if (causedBy != null) { if (causedBy instanceof User) From 2c7c4a2a6bb4853375055a770755d119c00f02d4 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 13 Nov 2022 13:22:17 +0800 Subject: [PATCH 16/40] =?UTF-8?q?=E5=8F=98=E6=9B=B4=E5=90=83=E8=8D=AF?= =?UTF-8?q?=E6=8F=90=E9=86=92=E4=B8=BA=2012:00=20=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=94=B9=E4=BA=86=E5=90=83=E8=8D=AF=E6=8F=90?= =?UTF-8?q?=E9=86=92=E7=9A=84=E6=97=B6=E9=97=B4=E8=AE=A1=E7=AE=97=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E4=B8=BA=E5=90=83=E8=8D=AF=E6=8F=90=E9=86=92?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E9=97=B4=E8=AE=A1=E7=AE=97=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E4=B8=80=E4=B8=AA=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将吃药提醒的时间计算变更为基于 LocalDateTime 的计算 - 为其添加了 USE_TIME_ZONE 和 NOTIFY_AT_HOUR 两个参数 --- gradle.properties | 2 +- .../cono/morny/daemon/MedicationTimer.java | 18 ++++++++++-- .../morny/daemon/TestMedicationTimer.java | 29 +++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java diff --git a/gradle.properties b/gradle.properties index 798df0e..305ce56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC1.1 +VERSION = 1.0.0-RC2 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java index 5faba4a..0c95a39 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java @@ -9,14 +9,19 @@ import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.response.SendResponse; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; +import java.util.Set; import static cc.sukazyo.cono.morny.Log.exceptionLog; import static cc.sukazyo.cono.morny.Log.logger; public class MedicationTimer extends Thread { + public static final ZoneOffset USE_TIME_ZONE = ZoneOffset.ofHours(8); + public static final Set NOTIFY_AT_HOUR = Set.of(12); public static final long NOTIFY_CHAT = -1001729016815L; public static final String NOTIFY_MESSAGE = "\uD83C\uDF65⏲"; private static final String DAEMON_THREAD_NAME = "TIMER_Medication"; @@ -69,12 +74,19 @@ public class MedicationTimer extends Thread { lastNotify = LAST_NOTIFY_ID_NULL; } - private static long calcNextRoutineTimestamp () { - return ((System.currentTimeMillis()+8*60*60*1000) / (12*60*60*1000) + 1) * 12*60*60*1000 - 8*60*60*1000; + public static long calcNextRoutineTimestamp (long baseTimeMillis, ZoneOffset useTimeZone, Set atHours) { + LocalDateTime time = LocalDateTime.ofEpochSecond( + baseTimeMillis/1000, (int)baseTimeMillis%1000*1000*1000, + useTimeZone + ).withMinute(0).withSecond(0).withNano(0); + do { + time = time.plusHours(1); + } while (!atHours.contains(time.getHour())); + return time.withMinute(0).withSecond(0).withNano(0).toInstant(useTimeZone).toEpochMilli(); } private void waitToNextRoutine () throws InterruptedException { - sleep(calcNextRoutineTimestamp() - System.currentTimeMillis()); + sleep(calcNextRoutineTimestamp(System.currentTimeMillis(), USE_TIME_ZONE, NOTIFY_AT_HOUR) - System.currentTimeMillis()); } } diff --git a/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java b/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java new file mode 100644 index 0000000..cb74065 --- /dev/null +++ b/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java @@ -0,0 +1,29 @@ +package cc.sukazyo.cono.morny.daemon; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Set; + +public class TestMedicationTimer { + + @ParameterizedTest + @CsvSource(textBlock = """ + 2022-11-13T13:14:35+08, +08, 2022-11-14T12:00:00+08 + 2022-11-13T13:14:35+02, +02, 2022-11-14T12:00:00+02 + 2022-11-13T08:14:35+08, +08, 2022-11-13T12:00:00+08 + 2022-11-13T00:14:35+08, +08, 2022-11-13T12:00:00+08 + 2022-11-13T12:00:00+00, +00, 2022-11-14T12:00:00+00 + """) + void testCalcNextRoutineTimestamp (ZonedDateTime base, ZoneOffset zoneHour, ZonedDateTime expected) { + final Set at = Set.of(12); + Assertions.assertEquals( + expected.toEpochSecond()*1000, + MedicationTimer.calcNextRoutineTimestamp(base.toEpochSecond()*1000, zoneHour, at) + ); + } + +} From 2ffab30ef10fd41b1b46e51fac41b4a093bdbe85 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Thu, 17 Nov 2022 13:54:48 +0800 Subject: [PATCH 17/40] fix error while converting millis to nanos in MedicationTimer --- gradle.properties | 2 +- src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 305ce56..1ab81d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC2 +VERSION = 1.0.0-RC2.1 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java index 0c95a39..dec57fa 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java @@ -76,7 +76,7 @@ public class MedicationTimer extends Thread { public static long calcNextRoutineTimestamp (long baseTimeMillis, ZoneOffset useTimeZone, Set atHours) { LocalDateTime time = LocalDateTime.ofEpochSecond( - baseTimeMillis/1000, (int)baseTimeMillis%1000*1000*1000, + baseTimeMillis/1000, (int)baseTimeMillis%1000*1000, useTimeZone ).withMinute(0).withSecond(0).withNano(0); do { From 8e28bbbce16994e8e4fd0aa131da79bd32eb2346 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Fri, 18 Nov 2022 17:49:39 +0800 Subject: [PATCH 18/40] add -medc/medt/medtz params and configs for customize MedicationTimer status, fix millis/nanos conv problem again - add configs for medication timer - medicationNotifyToChat - default is -1001729016815L(Annie medication notify chat) - add param --medication-notify-chat / -medc to set it, accept a long number. - medicationTimerUseTimezone - default is ZoneOffset.UTC(means +00:00) - add param --medication-notify-timezone / -medtz to set it, accept a number that means timezone hours - medicationNotifyAt - default is a empty Set - add param --medication-notify-times / -medt to set it, accept a hour(0~23) list seperated by "," - now medication timer will exit if the notify-times is empty - changed the Test data-source for MedicationTimer#calcNextRoutineTimestamp --- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/MornyConfig.java | 21 ++++++++++++++++- .../cc/sukazyo/cono/morny/ServerMain.java | 18 +++++++++++++++ .../bot/event/OnMedicationNotifyApply.java | 4 ++-- .../cono/morny/daemon/MedicationTimer.java | 18 ++++++++++----- .../morny/daemon/TestMedicationTimer.java | 23 +++++++++++-------- 6 files changed, 67 insertions(+), 19 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1ab81d9..d18bae9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC2.1 +VERSION = 1.0.0-RC3 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java index be61e15..bdc8893 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.lang.annotation.*; +import java.time.ZoneOffset; import java.util.HashSet; import java.util.Set; @@ -114,6 +115,16 @@ public class MornyConfig { @Nonnull public final Set dinnerTrustedReaders; public final long dinnerChatId; + /* ======================================= * + * function: medication timer * + * ======================================= */ + + public final long medicationNotifyToChat; + + @Nonnull public final ZoneOffset medicationTimerUseTimezone; + + @Nonnull public final Set medicationNotifyAt; + /* ======================================= * * End Configs | ConfigBuilder * * ======================================= */ @@ -134,11 +145,16 @@ public class MornyConfig { this.dinnerTrustedReaders = prototype.dinnerTrustedReaders; this.dinnerChatId = prototype.dinnerChatId; this.reportToChat = prototype.reportToChat; + this.medicationNotifyToChat = prototype.medicationNotifyToChat; + this.medicationTimerUseTimezone = prototype.medicationTimerUseTimezone; + prototype.medicationNotifyAt.forEach(i -> { if (i < 0 || i > 23) throw new CheckFailure.UnavailableTimeInMedicationNotifyAt(); }); + this.medicationNotifyAt = prototype.medicationNotifyAt; } - public static class CheckFailure extends Exception { + public static class CheckFailure extends RuntimeException { public static class NullTelegramBotKey extends CheckFailure {} public static class UnsetEventOutdatedTimestamp extends CheckFailure {} + public static class UnavailableTimeInMedicationNotifyAt extends CheckFailure {} } public static class Prototype { @@ -156,6 +172,9 @@ public class MornyConfig { @Nonnull public Set dinnerTrustedReaders = new HashSet<>(); public long dinnerChatId = -1001707106392L; public long reportToChat = -1001650050443L; + public long medicationNotifyToChat = -1001729016815L; + @Nonnull public ZoneOffset medicationTimerUseTimezone = ZoneOffset.UTC; + @Nonnull public Set medicationNotifyAt = new HashSet<>(); } diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index e9237ee..ba8e79e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -4,6 +4,8 @@ import cc.sukazyo.cono.morny.util.CommonFormat; import javax.annotation.Nonnull; +import java.time.ZoneOffset; + import static cc.sukazyo.cono.morny.Log.logger; /** @@ -166,6 +168,22 @@ public class ServerMain { config.reportToChat = Long.parseLong(args[i]); continue; } + case "--medication-notify-chat", "-medc" -> { + i++; + config.medicationNotifyToChat = Long.parseLong(args[i]); + continue; + } + case "--medication-notify-timezone", "-medtz" -> { + i++; + config.medicationTimerUseTimezone = ZoneOffset.ofHours(Integer.parseInt(args[i])); + continue; + } + case "--medication-notify-times", "-medt" -> { + i++; + for (String u : args[i].split(",")) + config.medicationNotifyAt.add(Integer.parseInt(u)); + continue; + } } } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java index a5bd835..7217635 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java @@ -1,7 +1,7 @@ package cc.sukazyo.cono.morny.bot.event; +import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.daemon.MedicationTimer; import cc.sukazyo.cono.morny.daemon.MornyDaemons; import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.Update; @@ -20,7 +20,7 @@ public class OnMedicationNotifyApply extends EventListener { } private boolean editedMessageProcess (Message edited) { - if (edited.chat().id() != MedicationTimer.NOTIFY_CHAT) return false; + if (edited.chat().id() != MornyCoeur.config().medicationNotifyToChat) return false; MornyDaemons.medicationTimerInstance.refreshNotificationWrite(edited); return true; } diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java index dec57fa..6760b4e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java @@ -20,15 +20,16 @@ import static cc.sukazyo.cono.morny.Log.logger; public class MedicationTimer extends Thread { - public static final ZoneOffset USE_TIME_ZONE = ZoneOffset.ofHours(8); - public static final Set NOTIFY_AT_HOUR = Set.of(12); - public static final long NOTIFY_CHAT = -1001729016815L; + private final ZoneOffset USE_TIME_ZONE = MornyCoeur.config().medicationTimerUseTimezone; + private final Set NOTIFY_AT_HOUR = MornyCoeur.config().medicationNotifyAt; + private final long NOTIFY_CHAT = MornyCoeur.config().medicationNotifyToChat; public static final String NOTIFY_MESSAGE = "\uD83C\uDF65⏲"; private static final String DAEMON_THREAD_NAME = "TIMER_Medication"; private static final long LAST_NOTIFY_ID_NULL = -1L; private long lastNotify = LAST_NOTIFY_ID_NULL; + public static class NoNotifyTimeTag extends Throwable { private NoNotifyTimeTag(){} } MedicationTimer () { super(DAEMON_THREAD_NAME); @@ -44,6 +45,9 @@ public class MedicationTimer extends Thread { } catch (InterruptedException e) { interrupt(); logger.info("MedicationTimer was interrupted, will be exit now"); + } catch (NoNotifyTimeTag ignored) { + logger.warn("Notify Time not Set and the MedicationTimer will not working!\nMedicationTimer will be exit now."); + interrupt(); } catch (Exception e) { logger.error("Unexpected error occurred"); logger.error(exceptionLog(e)); @@ -74,9 +78,11 @@ public class MedicationTimer extends Thread { lastNotify = LAST_NOTIFY_ID_NULL; } - public static long calcNextRoutineTimestamp (long baseTimeMillis, ZoneOffset useTimeZone, Set atHours) { + public static long calcNextRoutineTimestamp (long baseTimeMillis, ZoneOffset useTimeZone, Set atHours) + throws NoNotifyTimeTag { + if (atHours.isEmpty()) throw new NoNotifyTimeTag(); LocalDateTime time = LocalDateTime.ofEpochSecond( - baseTimeMillis/1000, (int)baseTimeMillis%1000*1000, + baseTimeMillis/1000, (int)(baseTimeMillis%1000)*1000*1000, useTimeZone ).withMinute(0).withSecond(0).withNano(0); do { @@ -85,7 +91,7 @@ public class MedicationTimer extends Thread { return time.withMinute(0).withSecond(0).withNano(0).toInstant(useTimeZone).toEpochMilli(); } - private void waitToNextRoutine () throws InterruptedException { + private void waitToNextRoutine () throws InterruptedException, NoNotifyTimeTag { sleep(calcNextRoutineTimestamp(System.currentTimeMillis(), USE_TIME_ZONE, NOTIFY_AT_HOUR) - System.currentTimeMillis()); } diff --git a/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java b/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java index cb74065..58600ce 100644 --- a/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java +++ b/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java @@ -12,18 +12,23 @@ public class TestMedicationTimer { @ParameterizedTest @CsvSource(textBlock = """ - 2022-11-13T13:14:35+08, +08, 2022-11-14T12:00:00+08 - 2022-11-13T13:14:35+02, +02, 2022-11-14T12:00:00+02 - 2022-11-13T08:14:35+08, +08, 2022-11-13T12:00:00+08 - 2022-11-13T00:14:35+08, +08, 2022-11-13T12:00:00+08 - 2022-11-13T12:00:00+00, +00, 2022-11-14T12:00:00+00 + 2022-11-13T13:14:35.000+08, +08, 2022-11-13T19:00:00+08 + 2022-11-13T13:14:35.174+02, +02, 2022-11-13T19:00:00+02 + 1998-02-01T08:14:35.871+08, +08, 1998-02-01T19:00:00+08 + 2022-11-13T00:00:00.000-01, -01, 2022-11-13T07:00:00-01 + 2022-11-21T19:00:00.000+00, +00, 2022-11-21T21:00:00+00 + 2022-12-31T21:00:00.000+00, +00, 2023-01-01T07:00:00+00 + 2125-11-18T23:45:27.062+00, +00, 2125-11-19T07:00:00+00 """) - void testCalcNextRoutineTimestamp (ZonedDateTime base, ZoneOffset zoneHour, ZonedDateTime expected) { - final Set at = Set.of(12); + void testCalcNextRoutineTimestamp (ZonedDateTime base, ZoneOffset zoneHour, ZonedDateTime expected) + throws MedicationTimer.NoNotifyTimeTag { + final Set at = Set.of(7, 19, 21); + System.out.println("base.toInstant().toEpochMilli() = " + base.toInstant().toEpochMilli()); Assertions.assertEquals( - expected.toEpochSecond()*1000, - MedicationTimer.calcNextRoutineTimestamp(base.toEpochSecond()*1000, zoneHour, at) + expected.toInstant().toEpochMilli(), + MedicationTimer.calcNextRoutineTimestamp(base.toInstant().toEpochMilli(), zoneHour, at) ); + System.out.println(" ok"); } } From 17c29036b3038b548b6bec13ef07734595f08175 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 19 Nov 2022 22:48:20 +0800 Subject: [PATCH 19/40] add a function "if something?" to OnUserRandoms - and now uses @Deprecated not @SuppressWarnings("unused") to tag those unused event. --- gradle.properties | 2 +- .../cono/morny/bot/event/EventListeners.java | 4 +-- .../morny/bot/event/OnActivityRecord.java | 1 + .../bot/event/OnKuohuanhuanNeedSleep.java | 1 + .../cono/morny/bot/event/OnUserRandoms.java | 25 ++++++------------- 5 files changed, 12 insertions(+), 21 deletions(-) diff --git a/gradle.properties b/gradle.properties index d18bae9..aee182c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3 +VERSION = 1.0.0-RC3.1 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java index 389b037..6f6708b 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java @@ -5,13 +5,13 @@ import cc.sukazyo.cono.morny.bot.api.EventListenerManager; public class EventListeners { public static final OnTelegramCommand COMMANDS_LISTENER = new OnTelegramCommand(); - @SuppressWarnings("unused") public static final OnActivityRecord ACTIVITY_RECORDER = new OnActivityRecord(); +// public static final OnActivityRecord ACTIVITY_RECORDER = new OnActivityRecord(); public static final OnUserSlashAction USER_SLASH_ACTION = new OnUserSlashAction(); public static final OnUpdateTimestampOffsetLock UPDATE_TIMESTAMP_OFFSET_LOCK = new OnUpdateTimestampOffsetLock(); public static final OnInlineQueries INLINE_QUERY = new OnInlineQueries(); public static final OnCallMe CALL_ME = new OnCallMe(); public static final OnEventHackHandle EVENT_HACK_HANDLE = new OnEventHackHandle(); - @SuppressWarnings("unused") static final OnKuohuanhuanNeedSleep KUOHUANHUAN_NEED_SLEEP = new OnKuohuanhuanNeedSleep(); +// static final OnKuohuanhuanNeedSleep KUOHUANHUAN_NEED_SLEEP = new OnKuohuanhuanNeedSleep(); public static final OnUserRandoms USER_RANDOMS = new OnUserRandoms(); public static final OnCallMsgSend CALL_MSG_SEND = new OnCallMsgSend(); public static final OnMedicationNotifyApply MEDICATION_NOTIFY_APPLY = new OnMedicationNotifyApply(); diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java index 6b26436..8ad0af4 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java @@ -7,6 +7,7 @@ import com.pengrad.telegrambot.model.Update; import javax.annotation.Nonnull; +@Deprecated public class OnActivityRecord extends EventListener { @Override diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java index 41091aa..2a51a67 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java @@ -10,6 +10,7 @@ import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Locale; +@Deprecated public class OnKuohuanhuanNeedSleep extends EventListener { @Override diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java index 898bdac..79d8a28 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java @@ -2,7 +2,6 @@ package cc.sukazyo.cono.morny.bot.event; import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.util.UniversalCommand; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.SendMessage; import org.jetbrains.annotations.NotNull; @@ -13,8 +12,8 @@ import java.util.regex.Pattern; public class OnUserRandoms extends EventListener { - private static final Pattern USER_OR_CN_QUERY = Pattern.compile("(.+)还是(.+)"); - private static final Pattern USER_OR_EN_QUERY = Pattern.compile("(.+)or(.+)"); + private static final Pattern USER_OR_QUERY = Pattern.compile("(.+)(?:还是|or)(.+)"); + private static final Pattern USER_IF_QUERY = Pattern.compile("(.+)[吗?|?]+$"); @Override public boolean onMessage (@NotNull Update update) { @@ -22,24 +21,14 @@ public class OnUserRandoms extends EventListener { if (update.message().text() == null) return false; if (!update.message().text().startsWith("/")) return false; - final String[] preProcess = UniversalCommand.format(update.message().text()); - if (preProcess.length > 1) return false; - final String query = preProcess[0]; - - // ----- START CODE BLOCK COMMENT ----- - // 这里实现思路和代码优化有至少一半是 copilot 和 IDEA 提供的 - // 实现思路都可以从人类手里抢一半贡献太恐怖了aba + final String query = update.message().text().substring(1); String result = null; - final Matcher matcher; - if (query.contains("还是")) { - matcher = USER_OR_CN_QUERY.matcher(query); - } else { - matcher = USER_OR_EN_QUERY.matcher(query); - } - if (matcher.find()) { + Matcher matcher; + if ((matcher = USER_OR_QUERY.matcher(query)).find()) { result = ThreadLocalRandom.current().nextBoolean() ? matcher.group(1) : matcher.group(2); + } else if ((matcher = USER_IF_QUERY.matcher(query)).matches()) { + result = (ThreadLocalRandom.current().nextBoolean()?"":"不") + matcher.group(1); } - // ----- STOP CODE BLOCK COMMENT ----- if (result == null) return false; MornyCoeur.extra().exec(new SendMessage( From 0e3a9d3f9becb57a56e315c713686654da309db0 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 19 Nov 2022 23:27:45 +0800 Subject: [PATCH 20/40] add function to randomly reply "?" message, added a CommonRandom tool, replace all jb.NotNull to javax.Nonnull --- gradle.properties | 2 +- .../cono/morny/bot/command/Ip186Query.java | 5 ++- .../cono/morny/bot/command/MornyCommands.java | 3 +- .../cono/morny/bot/command/私わね.java | 5 +-- .../cono/morny/bot/event/EventListeners.java | 2 ++ .../bot/event/OnMedicationNotifyApply.java | 7 ++-- .../morny/bot/event/OnQuestionMarkReply.java | 33 +++++++++++++++++++ .../cono/morny/bot/event/OnUserRandoms.java | 11 ++++--- .../sukazyo/cono/morny/util/CommonRandom.java | 22 +++++++++++++ 9 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java create mode 100644 src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java diff --git a/gradle.properties b/gradle.properties index aee182c..ecb6ab9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3.1 +VERSION = 1.0.0-RC3.2 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java index 212305a..4d5b4f3 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java @@ -7,7 +7,6 @@ import cc.sukazyo.cono.morny.util.tgapi.InputCommand; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.SendMessage; -import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -29,7 +28,7 @@ public class Ip186Query { @Nullable @Override public String[] getAliases () { return new String[0]; } @Nonnull @Override public String getParamRule () { return "[ip]"; } @Nonnull @Override public String getDescription () { return "通过 https://ip.186526.xyz 查询 ip 资料"; } - @Override public void execute (@NotNull InputCommand command, @NotNull Update event) { exec(event, command); } + @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { exec(event, command); } } public static class Whois implements ITelegramCommand { @@ -37,7 +36,7 @@ public class Ip186Query { @Nullable @Override public String[] getAliases () { return new String[0]; } @Nonnull @Override public String getParamRule () { return "[domain]"; } @Nonnull @Override public String getDescription () { return "通过 https://ip.186526.xyz 查询域名资料"; } - @Override public void execute (@NotNull InputCommand command, @NotNull Update event) { exec(event, command); } + @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { exec(event, command); } } private static void exec (@Nonnull Update event, @Nonnull InputCommand command) { diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index 5e14712..c3f78bb 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -14,7 +14,6 @@ import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.request.SendSticker; import com.pengrad.telegrambot.request.SetMyCommands; -import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -201,7 +200,7 @@ public class MornyCommands { private static class ExitAlias implements ISimpleCommand { @Nonnull @Override public String getName () { return "quit"; } @Nullable @Override public String[] getAliases () { return new String[]{"stop"}; } - @Override public void execute (@NotNull InputCommand command, @NotNull Update event) { onCommandExitExec(event); } + @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandExitExec(event); } } private static void onCommandExitExec (@Nonnull Update event) { if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) { diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java index b776046..6006c0d 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java @@ -7,7 +7,8 @@ import com.pengrad.telegrambot.request.SendMessage; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.concurrent.ThreadLocalRandom; + +import static cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue; @SuppressWarnings("NonAsciiCharacters") public class 私わね implements ISimpleCommand { @@ -20,7 +21,7 @@ public class 私わね implements ISimpleCommand { @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - if (ThreadLocalRandom.current().nextInt(521) == 0) { + if (probabilityTrue(521)) { // 可以接入未来的心情系统(如果有的话) // final String text = switch (ThreadLocalRandom.current().nextInt(11)) { // case 0,7,8,9,10 -> "才不是"; diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java index 6f6708b..76c9256 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java @@ -17,6 +17,7 @@ public class EventListeners { public static final OnMedicationNotifyApply MEDICATION_NOTIFY_APPLY = new OnMedicationNotifyApply(); public static final OnRandomlyTriggered RANDOMLY_TRIGGERED = new OnRandomlyTriggered(); public static final OnUniMeowTrigger UNI_MEOW_TRIGGER = new OnUniMeowTrigger(); + public static final OnQuestionMarkReply QUESTION_MARK_REPLY = new OnQuestionMarkReply(); public static void registerAllListeners () { EventListenerManager.addListener( @@ -28,6 +29,7 @@ public class EventListeners { UNI_MEOW_TRIGGER, RANDOMLY_TRIGGERED, USER_RANDOMS, + QUESTION_MARK_REPLY, USER_SLASH_ACTION, INLINE_QUERY, CALL_ME, diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java index 7217635..59ec01c 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java @@ -5,17 +5,18 @@ import cc.sukazyo.cono.morny.bot.api.EventListener; import cc.sukazyo.cono.morny.daemon.MornyDaemons; import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.Update; -import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; public class OnMedicationNotifyApply extends EventListener { @Override - public boolean onEditedChannelPost (@NotNull Update update) { + public boolean onEditedChannelPost (@Nonnull Update update) { return editedMessageProcess(update.editedChannelPost()); } @Override - public boolean onEditedMessage (@NotNull Update update) { + public boolean onEditedMessage (@Nonnull Update update) { return editedMessageProcess(update.editedMessage()); } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java new file mode 100644 index 0000000..00f1a90 --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java @@ -0,0 +1,33 @@ +package cc.sukazyo.cono.morny.bot.event; + +import cc.sukazyo.cono.morny.MornyCoeur; +import cc.sukazyo.cono.morny.bot.api.EventListener; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.SendMessage; + +import javax.annotation.Nonnull; +import java.util.Set; + +import static cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue; + +public class OnQuestionMarkReply extends EventListener { + + public static final Set QUESTION_MARKS = Set.of("?", "?", "¿", "⁈", "⁇", "‽", "❔", "❓"); + + @Override + public boolean onMessage (@Nonnull Update update) { + + if (update.message().text() == null) return false; + + if (QUESTION_MARKS.contains(update.message().text()) && probabilityTrue(8)) { + MornyCoeur.extra().exec(new SendMessage( + update.message().chat().id(), update.message().text() + ).replyToMessageId(update.message().messageId())); + return true; + } + + return false; + + } + +} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java index 79d8a28..b1deec4 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java @@ -4,19 +4,20 @@ import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.bot.api.EventListener; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.request.SendMessage; -import org.jetbrains.annotations.NotNull; -import java.util.concurrent.ThreadLocalRandom; +import javax.annotation.Nonnull; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static cc.sukazyo.cono.morny.util.CommonRandom.iif; + public class OnUserRandoms extends EventListener { private static final Pattern USER_OR_QUERY = Pattern.compile("(.+)(?:还是|or)(.+)"); private static final Pattern USER_IF_QUERY = Pattern.compile("(.+)[吗?|?]+$"); @Override - public boolean onMessage (@NotNull Update update) { + public boolean onMessage (@Nonnull Update update) { if (update.message().text() == null) return false; if (!update.message().text().startsWith("/")) return false; @@ -25,9 +26,9 @@ public class OnUserRandoms extends EventListener { String result = null; Matcher matcher; if ((matcher = USER_OR_QUERY.matcher(query)).find()) { - result = ThreadLocalRandom.current().nextBoolean() ? matcher.group(1) : matcher.group(2); + result = iif() ? matcher.group(1) : matcher.group(2); } else if ((matcher = USER_IF_QUERY.matcher(query)).matches()) { - result = (ThreadLocalRandom.current().nextBoolean()?"":"不") + matcher.group(1); + result = (iif()?"":"不") + matcher.group(1); } if (result == null) return false; diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java new file mode 100644 index 0000000..4e90fff --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java @@ -0,0 +1,22 @@ +package cc.sukazyo.cono.morny.util; + +import javax.annotation.Nonnegative; +import java.util.concurrent.ThreadLocalRandom; + +public class CommonRandom { + + public static boolean probabilityTrue (@Nonnegative int probability, @Nonnegative int base) { + if (probability < 1) throw new IllegalArgumentException("the probability must be a positive value!"); + if (base < 1) throw new IllegalArgumentException("the probability base must be a positive value!"); + return probability > ThreadLocalRandom.current().nextInt(base); + } + + public static boolean probabilityTrue (@Nonnegative int probabilityIn) { + return (probabilityTrue(1, probabilityIn)); + } + + public static boolean iif () { + return ThreadLocalRandom.current().nextBoolean(); + } + +} From 031a7990700e30b9ff43e460fe5fdd48b530ce2b Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 20 Nov 2022 12:45:20 +0800 Subject: [PATCH 21/40] support QuestionMarkReply to reply more than 1 mark char message. add javadoc --- gradle.properties | 2 +- .../morny/bot/event/OnQuestionMarkReply.java | 19 ++++++++++------ .../sukazyo/cono/morny/util/CommonRandom.java | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/gradle.properties b/gradle.properties index ecb6ab9..f3bdb84 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3.2 +VERSION = 1.0.0-RC3.3 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java index 00f1a90..8e51eeb 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java @@ -12,21 +12,26 @@ import static cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue; public class OnQuestionMarkReply extends EventListener { - public static final Set QUESTION_MARKS = Set.of("?", "?", "¿", "⁈", "⁇", "‽", "❔", "❓"); + /** + * 一个 unicode 的问号字符列表. 不仅有半角全角问号,也包含了变体问号,和叹号结合的问好以及 uni-emoji 问号。 + * @since 1.0.0-RC3.2 + */ + public static final Set QUESTION_MARKS = Set.of('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓'); @Override public boolean onMessage (@Nonnull Update update) { if (update.message().text() == null) return false; - if (QUESTION_MARKS.contains(update.message().text()) && probabilityTrue(8)) { - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), update.message().text() - ).replyToMessageId(update.message().messageId())); - return true; + if (!probabilityTrue(8)) return false; + for (char c : update.message().text().toCharArray()) { + if (!QUESTION_MARKS.contains(c)) return false; } - return false; + MornyCoeur.extra().exec(new SendMessage( + update.message().chat().id(), update.message().text() + ).replyToMessageId(update.message().messageId())); + return true; } diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java index 4e90fff..22c9812 100644 --- a/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java +++ b/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java @@ -5,16 +5,38 @@ import java.util.concurrent.ThreadLocalRandom; public class CommonRandom { + /** + * 通过 {@link ThreadLocalRandom} 以指定的一定几率返回 true. + * @param probability 一个正整数,决定在样本空间中有多大的可能性为 true。 + * @param base 一个正整数,决定样本空间有多大。 + * @return 有 {@code base} 分之 {@code probability} 的几率,返回值为 {@link true}. + * 如果 {@code probability} 大于 {@code base},也就是为 true 的可能性大于 100%,则会永远为 true。 + * @throws IllegalArgumentException + * 当参数 base 或是 probability 不为正整数时 + * @since 1.0.0-RC3.2 + */ public static boolean probabilityTrue (@Nonnegative int probability, @Nonnegative int base) { if (probability < 1) throw new IllegalArgumentException("the probability must be a positive value!"); if (base < 1) throw new IllegalArgumentException("the probability base must be a positive value!"); return probability > ThreadLocalRandom.current().nextInt(base); } + /** + * 以一定几率返回 true. + * @return {@code probabilityIn} 分之 {@link 1} 的几率为 {@link true}. + * @see #probabilityTrue(int, int) + * @since 1.0.0-RC3.2 + */ public static boolean probabilityTrue (@Nonnegative int probabilityIn) { return (probabilityTrue(1, probabilityIn)); } + /** + * 通过 {@link ThreadLocalRandom} 实现的随机 boolean 取值. + * @return 随机的 {@link true} 或 {@link false},各占(近似)一半可能性. + * @see ThreadLocalRandom#nextBoolean() + * @since 1.0.0-RC3.2 + */ public static boolean iif () { return ThreadLocalRandom.current().nextBoolean(); } From f990df70ea2eab61a2198feae06b93b7c9ee08a2 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 24 Dec 2022 23:18:49 +0800 Subject: [PATCH 22/40] /nbnhhsh will send 404 while no content, add some javadoc --- gradle.properties | 2 +- src/main/java/cc/sukazyo/cono/morny/Log.java | 6 ++++++ .../cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java | 14 ++++++++++++-- .../cono/morny/bot/command/package-info.java | 7 +++++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/package-info.java diff --git a/gradle.properties b/gradle.properties index f3bdb84..c80d4ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3.3 +VERSION = 1.0.0-RC3.4 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/Log.java b/src/main/java/cc/sukazyo/cono/morny/Log.java index f4d1ea4..b9e0e83 100644 --- a/src/main/java/cc/sukazyo/cono/morny/Log.java +++ b/src/main/java/cc/sukazyo/cono/morny/Log.java @@ -18,6 +18,12 @@ public class Log { */ public static final Logger logger = new Logger(new ConsoleAppender()); + /** + * 获取异常的堆栈信息. + * @param e 异常体 + * @return {@link String} 格式的异常的堆栈报告信息. + * @see 1.0.0-alpha5 + */ public static String exceptionLog (Exception e) { final StringWriter stackTrace = new StringWriter(); e.printStackTrace(new PrintWriter(stackTrace)); diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java index 22e87cc..20adb23 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java @@ -1,5 +1,6 @@ package cc.sukazyo.cono.morny.bot.command; +import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.util.tgapi.InputCommand; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.ParseMode; @@ -7,6 +8,7 @@ import com.pengrad.telegrambot.request.SendMessage; import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.data.NbnhhshQuery; +import com.pengrad.telegrambot.request.SendSticker; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -24,13 +26,17 @@ public class Nbnhhsh implements ITelegramCommand { @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { + class TagNoContent extends Exception {} try { - String queryTarget = ""; + String queryTarget; if (event.message().replyToMessage() != null && event.message().replyToMessage().text() != null) queryTarget = event.message().replyToMessage().text(); - if (command.hasArgs()) + else if (command.hasArgs()) queryTarget = stringsConnecting(command.getArgs(), " ", 0, command.getArgs().length-1); + else { + throw new TagNoContent(); + } NbnhhshQuery.GuessResult response = NbnhhshQuery.sendGuess(queryTarget); @@ -58,6 +64,10 @@ public class Nbnhhsh implements ITelegramCommand { message.toString() ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); + } catch (TagNoContent tag) { + MornyCoeur.extra().exec(new SendSticker( + event.message().chat().id(), TelegramStickers.ID_404 + ).replyToMessageId(event.message().messageId())); } catch (Exception e) { MornyCoeur.extra().exec(new SendMessage( event.message().chat().id(), diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/package-info.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/package-info.java new file mode 100644 index 0000000..a2d0379 --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/package-info.java @@ -0,0 +1,7 @@ +/** + * 一系列的 telegram bot 命令的声明. + *

      + * 命令将在 {@link cc.sukazyo.cono.morny.bot.command.MornyCommands} 当中实例化并注册管理,并通过事件 + * {@link cc.sukazyo.cono.morny.bot.event.OnTelegramCommand} 调用. + */ +package cc.sukazyo.cono.morny.bot.command; From 9c03a59512162b0f7a3226bebf0b8ac726878edc Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Fri, 7 Apr 2023 16:55:09 +0800 Subject: [PATCH 23/40] fix input av/bv may be too long or too small to process --- _book | 1 - build.gradle | 4 +-- gradle.properties | 2 +- .../morny/bot/command/GetUsernameAndId.java | 6 ++++ .../morny/bot/query/ShareToolBilibili.java | 3 +- .../cc/sukazyo/cono/morny/util/BiliTool.java | 29 +++++++++++++++++-- 6 files changed, 37 insertions(+), 8 deletions(-) delete mode 160000 _book diff --git a/_book b/_book deleted file mode 160000 index 33e051f..0000000 --- a/_book +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 33e051fee5acb5118d6c023039ff9c1db6aa0c60 diff --git a/build.gradle b/build.gradle index 1629e2c..7c34aa5 100644 --- a/build.gradle +++ b/build.gradle @@ -102,11 +102,11 @@ java { } -tasks.withType(JavaCompile) { +tasks.withType(JavaCompile).configureEach { options.encoding = proj_file_encoding.name() } -tasks.withType(Javadoc) { +tasks.withType(Javadoc).configureEach { options.encoding = proj_file_encoding.name() options.docEncoding = proj_file_encoding.name() options.charSet = proj_file_encoding.name() diff --git a/gradle.properties b/gradle.properties index c80d4ad..cdc89ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3.4 +VERSION = 1.0.0-RC3.5 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java index bcc2bcf..553b14f 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java @@ -25,16 +25,20 @@ public class GetUsernameAndId implements ITelegramCommand { final String[] args = command.getArgs(); + // 不支持大于一个参数 if (args.length > 1) { MornyCoeur.extra().exec(new SendMessage( event.message().chat().id(), "[Unavailable] Too much arguments." ).replyToMessageId(event.message().messageId())); return; } + // 发送者自己的 id long userId = event.message().from().id(); + // 如果有回复某个人,则使用被回复人的 id if (event.message().replyToMessage()!= null) { userId = event.message().replyToMessage().from().id(); } + // 如果有指定 id,则使用指定的 id if (args.length > 0) { try { userId = Long.parseLong(args[0]); @@ -47,6 +51,7 @@ public class GetUsernameAndId implements ITelegramCommand { } } + // 重新获取用户对象 final GetChatMemberResponse response = MornyCoeur.getAccount().execute( new GetChatMember(event.message().chat().id(), userId) ); @@ -59,6 +64,7 @@ public class GetUsernameAndId implements ITelegramCommand { return; } + // 获取并发送用户信息 final User user = response.chatMember().user(); if (user.id() == 136817688) { diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java index a5e1ced..68652bd 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java @@ -13,7 +13,6 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -//import static cc.sukazyo.cono.morny.Log.logger; import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; public class ShareToolBilibili implements ITelegramQuery { @@ -22,7 +21,7 @@ public class ShareToolBilibili implements ITelegramQuery { public static final String TITLE_BILI_BV = "[bilibili] Share video / BV"; public static final String ID_PREFIX_BILI_AV = "[morny/share/bili/av]"; public static final String ID_PREFIX_BILI_BV = "[morny/share/bili/bv]"; - public static final Pattern REGEX_BILI_VIDEO = Pattern.compile("^(?:(?:https?://)?(?:www\\.)?bilibili\\.com(?:/s)?/video/((?:av|AV)(\\d+)|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]+))/?(\\?(?:p=(\\d+))?.*)?|(?:av|AV)(\\d+)|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]+))$"); + public static final Pattern REGEX_BILI_VIDEO = Pattern.compile("^(?:(?:https?://)?(?:www\\.)?bilibili\\.com(?:/s)?/video/((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))/?(\\?(?:p=(\\d+))?.*)?|(?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$"); private static final String SHARE_FORMAT_HTML = "%s"; diff --git a/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java b/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java index ef36993..a7df8bc 100644 --- a/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java +++ b/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java @@ -16,6 +16,22 @@ public class BiliTool { private static final char[] BV_TEMPLATE = "1 4 1 7 ".toCharArray(); private static final int[] BV_TEMPLATE_FILTER = new int[]{9, 8, 1, 6, 2, 4}; + public static class IllegalFormatException extends RuntimeException { + + private IllegalFormatException (String bv, String reason) { + super("`%s` is not a valid 10 digits base58 BV id: %s".formatted(bv, reason)); + } + + private IllegalFormatException (String bv, int length) { + this(bv, "length is %d.".formatted(length)); + } + + private IllegalFormatException (String bv, char c, int location) { + this(bv, "char `%s` is not in base58 char table (in position %d)".formatted(c, location)); + } + + } + /** * Convert a Bilibili AV video id format to BV id format. *

      @@ -27,17 +43,26 @@ public class BiliTool { *

      * for now , the BV id has 10 digits. * the method available while the av-id < 2^27, while it theoretically available when the av-id < 2^30. + *

      + * this method allows input only 10 digits base58 BV id, if the input is not formatted by this method, it will throw + * an Exception. * * @see mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」? * * @param bv the BV id, a string in (a special) base58 number format, without "BV" prefix. * @return the AV id corresponding to this bv id in Bilibili, formatted as a number. + * @throws IllegalFormatException if the input BV id is not the 10 digits base58 String. */ @Nonnegative - public static long toAv (@Nonnull String bv) { + public static long toAv (@Nonnull String bv) throws IllegalFormatException { long av = 0; + if (bv.length() != 10) + throw new IllegalFormatException(bv, bv.length()); for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) { - av += BV_TABLE_REVERSED.get(bv.charAt(BV_TEMPLATE_FILTER[i])) * Math.pow(TABLE_INT,i); + final Integer tableToken = BV_TABLE_REVERSED.get(bv.charAt(BV_TEMPLATE_FILTER[i])); + if (tableToken == null) + throw new IllegalFormatException(bv, bv.charAt(BV_TEMPLATE_FILTER[i]), BV_TEMPLATE_FILTER[i]); + av += tableToken * Math.pow(TABLE_INT,i); } return (av-V_CONV_ADD)^V_CONV_XOR; } From e153d9e47f7e2d4aba4f738e1fdb7413129cf791 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 8 Apr 2023 13:10:36 +0800 Subject: [PATCH 24/40] support log level setting; remove git submodule _book; bug fix. - update dependencies - messiva: 0.1.0.1 -> 0.1.1 - add param -d/--dbg/--debug to enable the Debug/Trace log output. - fix UniversalCommand occurs ArrayOutOfBounds while the quotes not closed. --- .gitmodules | 3 -- gradle.properties | 4 +-- src/main/java/cc/sukazyo/cono/morny/Log.java | 28 +++++++++++++++++-- .../cc/sukazyo/cono/morny/ServerMain.java | 17 ++++++++++- .../morny/bot/query/ShareToolBilibili.java | 21 +++++++------- .../cono/morny/util/UniversalCommand.java | 8 ++++-- 6 files changed, 60 insertions(+), 21 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index aa5e2ed..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "_book"] - path = _book - url = https://storage.sukazyo.cc/Eyre_S/morny-book.git diff --git a/gradle.properties b/gradle.properties index cdc89ae..e748f83 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3.5 +VERSION = 1.0.0-RC3.6 USE_DELTA = false VERSION_DELTA = @@ -16,7 +16,7 @@ CODENAME = beiping libSpotbugsVersion = 4.7.3 -libMessivaVersion = 0.1.0.1 +libMessivaVersion = 0.1.1 libJavaTelegramBotApiVersion = 6.2.0 diff --git a/src/main/java/cc/sukazyo/cono/morny/Log.java b/src/main/java/cc/sukazyo/cono/morny/Log.java index b9e0e83..4c3dadd 100644 --- a/src/main/java/cc/sukazyo/cono/morny/Log.java +++ b/src/main/java/cc/sukazyo/cono/morny/Log.java @@ -1,6 +1,8 @@ package cc.sukazyo.cono.morny; -import cc.sukazyo.messiva.Logger; +import cc.sukazyo.messiva.formatter.SimpleFormatter; +import cc.sukazyo.messiva.log.LogLevel; +import cc.sukazyo.messiva.logger.Logger; import cc.sukazyo.messiva.appender.ConsoleAppender; import java.io.PrintWriter; @@ -16,7 +18,29 @@ public class Log { * messiva 更新 * @since 0.4.1.1 */ - public static final Logger logger = new Logger(new ConsoleAppender()); + public static final Logger logger = new Logger(new ConsoleAppender(new SimpleFormatter())).minLevel(LogLevel.INFO); + + /** + * Is the Debug mode enabled. + * + * @return if the minimal log level is equal or lower than DEBUG level. + */ + public static boolean debug () { + return logger.levelSetting.minLevel().level <= LogLevel.DEBUG.level; + } + + /** + * Switch the Debug log output enabled. + *

      + * if enable the debug log output, all the Log regardless of LogLevel will be output. + * As default, if the debug log output is disabled, Logger will ignore the Logs level lower than INFO. + * + * @param debug switch enable the debug log output as true, or disable it as false. + */ + public static void debug (boolean debug) { + if (debug) logger.minLevel(LogLevel.ALL); + else logger.minLevel(LogLevel.INFO); + } /** * 获取异常的堆栈信息. diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index ba8e79e..113879b 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -5,6 +5,8 @@ import cc.sukazyo.cono.morny.util.CommonFormat; import javax.annotation.Nonnull; import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; import static cc.sukazyo.cono.morny.Log.logger; @@ -93,11 +95,17 @@ public class ServerMain { config.eventOutdatedTimestamp = systemStartupTime; + List unknownArgs = new ArrayList<>(); + for (int i = 0; i < args.length; i++) { if (args[i].startsWith("-")) { switch (args[i]) { + case "-d", "--dbg", "--debug" -> { + Log.debug(true); + continue; + } case "--outdated-block", "-ob" -> { config.eventIgnoreOutdated = true; continue; @@ -188,10 +196,17 @@ public class ServerMain { } - logger.warn("Can't understand arg to some meaning :\n " + args[i]); + unknownArgs.add(args[i]); } + unknownArgs.forEach(arg -> logger.warn("Can't understand arg to some meaning :\n " + arg)); + + if (Log.debug()) + logger.warn("Debug log output enabled.\n It may lower your performance, make sure that you are not in production environment."); + + logger.debug("Debug log output enabled."); + String propToken = null; String propTokenKey = null; for (String iKey : MornyConfig.PROP_TOKEN_KEY) { diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java index 68652bd..da6c2fa 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static cc.sukazyo.cono.morny.Log.logger; import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; public class ShareToolBilibili implements ITelegramQuery { @@ -32,25 +33,25 @@ public class ShareToolBilibili implements ITelegramQuery { final Matcher regex = REGEX_BILI_VIDEO.matcher(event.inlineQuery().query()); if (regex.matches()) { -// logger.debug(String.format( -// "====== ok\n1: %s\n2: %s\n3: %s\n4: %s\n5: %s\n6: %s\n7: %s", -// regex.group(1), regex.group(2), regex.group(3), regex.group(4), -// regex.group(5), regex.group(6), regex.group(7) -// )); + logger.debug(String.format( + "====== Share Tool Bilibili Catch ok\n1: %s\n2: %s\n3: %s\n4: %s\n5: %s\n6: %s\n7: %s", + regex.group(1), regex.group(2), regex.group(3), regex.group(4), + regex.group(5), regex.group(6), regex.group(7) + )); // get video id from input, also get video part id String av = regex.group(2)==null ? regex.group(6)==null ? null : regex.group(6) : regex.group(2); String bv = regex.group(3)==null ? regex.group(7)==null ? null : regex.group(7) : regex.group(3); -// logger.trace(String.format("catch id av[%s] bv[%s]", av, bv)); + logger.trace(String.format("catch id av[%s] bv[%s]", av, bv)); final int part = regex.group(5)==null ? -1 : Integer.parseInt(regex.group(5)); -// logger.trace(String.format("catch part [%s]", part)); + logger.trace(String.format("catch part [%s]", part)); if (av == null) { assert bv != null; av = String.valueOf(BiliTool.toAv(bv)); -// logger.trace(String.format("converted bv[%s] to av[%s]", bv, av)); + logger.trace(String.format("converted bv[%s] to av[%s]", bv, av)); } else { bv = BiliTool.toBv(Long.parseLong(av)); -// logger.trace(String.format("converted av[%s] to bv[%s]", av, bv)); + logger.trace(String.format("converted av[%s] to bv[%s]", av, bv)); } // build standard share links final String linkPartParam = part==-1 ? "" : "?p="+part; @@ -58,7 +59,7 @@ public class ShareToolBilibili implements ITelegramQuery { final String linkBv = "https://www.bilibili.com/video/BV"+bv + linkPartParam; final String idAv = "av"+av; final String idBv = "BV"+bv; -// logger.trace("built all data."); + logger.trace("built all data."); // build share message element List> result = new ArrayList<>(); diff --git a/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java b/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java index f9e11b8..2bf2e8e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java +++ b/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java @@ -19,16 +19,18 @@ public class UniversalCommand { } else if (coma[i] == '"') { while (true) { i++; - if (coma[i] == '"') { + if (i >= coma.length) { break; - } else if (coma[i] == '\\' && (coma[i+1] == '"' || coma[i+1] == '\\')) { + } else if (coma[i] == '"') { + break; + } else if (coma[i] == '\\' && i+1 < coma.length && (coma[i+1] == '"' || coma[i+1] == '\\')) { i++; tmp.append(coma[i]); } else { tmp.append(coma[i]); } } - } else if (coma[i] == '\\' && (coma[i+1] == ' ' || coma[i+1] == '"' || coma[i+1] == '\\')) { + } else if (coma[i] == '\\' && i+1 < coma.length && (coma[i+1] == ' ' || coma[i+1] == '"' || coma[i+1] == '\\')) { i++; tmp.append(coma[i]); } else { From 518bde9404b312d1fd4f825edfbefe2067b7c33b Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 8 Apr 2023 15:01:12 +0800 Subject: [PATCH 25/40] put Welcome Message echo first --- gradle.properties | 2 +- .../java/cc/sukazyo/cono/morny/ServerMain.java | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/gradle.properties b/gradle.properties index e748f83..710c744 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3.6 +VERSION = 1.0.0-RC3.7 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index 113879b..87d988e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -200,6 +200,14 @@ public class ServerMain { } + //# + //# 启动信息输出 + //# 启动相关参数的检查和处理 + //# + + if (showWelcome) logger.info(MornyHello.MORNY_PREVIEW_IMAGE_ASCII); + if (welcomeEchoMode) return; + unknownArgs.forEach(arg -> logger.warn("Can't understand arg to some meaning :\n " + arg)); if (Log.debug()) @@ -216,10 +224,6 @@ public class ServerMain { } } - //# - //# 启动相关参数的检查和处理 - //# - if (versionEchoMode) { logger.info(String.format(""" @@ -250,9 +254,6 @@ public class ServerMain { } - if (showWelcome) logger.info(MornyHello.MORNY_PREVIEW_IMAGE_ASCII); - if (welcomeEchoMode) return; - logger.info(String.format(""" ServerMain.java Loaded >>> - version %s From 69a33933f50a7be73c572e26ea159b1b0ac96653 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Tue, 15 Aug 2023 14:10:13 +0800 Subject: [PATCH 26/40] add docker, update gradle wrapper - added Dockerfile and simple example docker-compose.yml - upgrade gradle wrapper: 7.5.1 -> 8.2.1 - tiny code refactor --- .dockerignore | 17 +++++++++ Dockerfile | 21 ++++++++++ build.gradle | 38 +++++++++++-------- docker-compose.yml | 4 ++ gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../cc/sukazyo/cono/morny/ServerMain.java | 21 +++++----- 7 files changed, 79 insertions(+), 26 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..df3d0f1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ + +# IDE +.idea/ +.vscode/ +.gradle/ +.settings/ +/src/test/java/test/* +/src/test/resources/test/* + +#build +/build/ +/bin/ +.project +lcoal.properties + +# debug dir +/run/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..055ebfc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM eclipse-temurin:20-jdk as build +LABEL authors="A.C.Sukazyo Eyre" + +COPY . /app/source/ +WORKDIR /app + +RUN cd ./source \ +&& ./gradlew shadowJar -PdockerBuild \ +&& cd .. \ +&& cp ./source/build/libs/morny-coeur-docker-build.jar ./morny-coeur.jar +#&& rm -r ./source \ +#&& rm -r /root/.gradle \ + + +FROM eclipse-temurin:20-jre + +COPY --from=build /app/morny-coeur.jar /app/morny-coeur.jar +WORKDIR /app + +ENTRYPOINT ["java", "-jar", "morny-coeur.jar"] +CMD ["-q", "-v"] diff --git a/build.gradle b/build.gradle index 7c34aa5..6ec4e7d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,36 +1,36 @@ -import org.ajoberstar.grgit.Status - -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets - plugins { id 'java' id 'java-library' id 'application' id 'maven-publish' - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' id 'com.github.gmazzo.buildconfig' version '3.1.0' - id 'org.ajoberstar.grgit' version '5.0.0' + id 'org.ajoberstar.grgit' version '5.2.0' } +import org.ajoberstar.grgit.Status + +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + final boolean proj_git = grgit!=null final String proj_store = MORNY_CODE_STORE final String proj_commit = proj_git ? grgit.head().id : null final String proj_commit_path = MORNY_COMMIT_PATH final boolean proj_clean = isCleanBuild() if (!proj_git) - print "[MornyBuild] git repository not available for current working space! git version tag will be disabled." + println "[MornyBuild] git repository not available for current working space! git version tag will be disabled." else if (isCleanBuild()) { - print "git: clean build at ${grgit.head().id}" + println "git: clean build at ${grgit.head().id}" } else { final Status status = grgit.status() - print "git: non-clean-build" + println "git: non-clean-build" if (!status.unstaged.allChanges.empty) { - print "\ngit: unstaged changes" + println "git: unstaged changes" listChanges(status.unstaged) } if (!status.staged.allChanges.empty) { - print "\ngit: staged changes" + println "git: staged changes" listChanges(status.staged) } } @@ -134,7 +134,15 @@ buildConfig { } shadowJar { + archiveClassifier.set "fat" + + if (project.hasProperty("dockerBuild")) { + println "shadow-jar: using docker build name" + archiveVersion.set "" + archiveClassifier.set "docker-build" + } + } @SuppressWarnings("all") @@ -151,11 +159,11 @@ boolean isCleanBuild () { void listChanges (Status.Changes listing) { for (String file in listing.added) - print "\n add: ${file}" + println " add: ${file}" for (String file in listing.modified) - print "\n mod: ${file}" + println " mod: ${file}" for (String file in listing.removed) - print "\n del: ${file}" + println " del: ${file}" } publishing { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4d8cce6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,4 @@ +services: + coeur-app: + build: . + command: -v diff --git a/gradle.properties b/gradle.properties index 710c744..9946e84 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3.7 +VERSION = 1.0.0-RC3.8 USE_DELTA = false VERSION_DELTA = diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661..84a0b92 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index 87d988e..9a0e931 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -97,6 +97,7 @@ public class ServerMain { List unknownArgs = new ArrayList<>(); + //# 从命令行参数设置启动参数 for (int i = 0; i < args.length; i++) { if (args[i].startsWith("-")) { @@ -200,6 +201,17 @@ public class ServerMain { } + //# 从环境变量设置启动参数 + String propToken = null; + String propTokenKey = null; + for (String iKey : MornyConfig.PROP_TOKEN_KEY) { + if (System.getenv(iKey) != null) { + propToken = System.getenv(iKey); + propTokenKey = iKey; + } + } + + //# //# 启动信息输出 //# 启动相关参数的检查和处理 @@ -215,15 +227,6 @@ public class ServerMain { logger.debug("Debug log output enabled."); - String propToken = null; - String propTokenKey = null; - for (String iKey : MornyConfig.PROP_TOKEN_KEY) { - if (System.getenv(iKey) != null) { - propToken = System.getenv(iKey); - propTokenKey = iKey; - } - } - if (versionEchoMode) { logger.info(String.format(""" From 7589e8661d0c0f368232878a7ec2ee76b046ec06 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 20 Aug 2023 23:11:10 +0800 Subject: [PATCH 27/40] added `--dinner-chat` startup param, code quality optimization. - added `--dinner-chat`/`-chd` startup param for setup dinner chat id - fixed some unclosed resource - TrackerDataManager changed method to get FileChannel (I don't know if it works) - MornyCLI scanner uses try-with-resources - declared directly used dependencies - okhttp: 4.11.0 - gson: 2.10.1 - some minor refactors - MornyCoeur.main rename to .init - added package `internal` and move @BuildConfigField to it - added new util class OkHttpPublic with MediaTypes in use - added javadoc for TrackerDataManager - and so on... --- build.gradle | 18 +++++--- gradle.properties | 13 ++++-- .../cc/sukazyo/cono/morny/MornyCoeur.java | 2 +- .../cc/sukazyo/cono/morny/MornyConfig.java | 4 +- .../cc/sukazyo/cono/morny/MornySystem.java | 2 +- .../cc/sukazyo/cono/morny/ServerMain.java | 11 +++-- .../cono/morny/bot/command/EventHack.java | 37 +++++++++------ .../morny/bot/event/OnRandomlyTriggered.java | 8 ++-- .../cono/morny/daemon/TrackerDataManager.java | 46 +++++++++++++++++-- .../sukazyo/cono/morny/data/NbnhhshQuery.java | 9 ++-- .../{util => internal}/BuildConfigField.java | 2 +- .../sukazyo/cono/morny/util/OkHttpPublic.java | 13 ++++++ .../java/cc/sukazyo/cono/morny/MornyCLI.java | 4 +- .../cono/morny/util/TestCommonConvert.java | 33 ++++++------- 14 files changed, 133 insertions(+), 69 deletions(-) rename src/main/java/cc/sukazyo/cono/morny/{util => internal}/BuildConfigField.java (89%) create mode 100644 src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java diff --git a/build.gradle b/build.gradle index 6ec4e7d..1ba1412 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id 'application' id 'maven-publish' id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'com.github.gmazzo.buildconfig' version '3.1.0' + id 'com.github.gmazzo.buildconfig' version '4.1.2' id 'org.ajoberstar.grgit' version '5.2.0' } @@ -73,15 +73,16 @@ repositories { dependencies { - compileOnlyApi "com.github.spotbugs:spotbugs-annotations:${libSpotbugsVersion}" + compileOnlyApi "com.github.spotbugs:spotbugs-annotations:${lib_spotbugs_v}" - api "cc.sukazyo:messiva:${libMessivaVersion}" + api "cc.sukazyo:messiva:${lib_messiva_v}" - implementation "com.github.pengrad:java-telegram-bot-api:${libJavaTelegramBotApiVersion}" + implementation "com.github.pengrad:java-telegram-bot-api:${lib_javatelegramapi_v}" + implementation "com.squareup.okhttp3:okhttp:${lib_okhttp_v}" + implementation "com.google.code.gson:gson:${lib_gson_v}" - testImplementation "org.junit.jupiter:junit-jupiter-api:${libJunitVersion}" - testImplementation "org.junit.jupiter:junit-jupiter-params:${libJunitVersion}" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${libJunitVersion}" + testImplementation platform("org.junit:junit-bom:${lib_junit_v}") + testImplementation "org.junit.jupiter:junit-jupiter" } @@ -91,6 +92,9 @@ application { test { useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } } java { diff --git a/gradle.properties b/gradle.properties index 9946e84..24b3c02 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3.8 +VERSION = 1.0.0-RC3.9 USE_DELTA = false VERSION_DELTA = @@ -14,10 +14,13 @@ CODENAME = beiping # dependencies -libSpotbugsVersion = 4.7.3 +lib_spotbugs_v = 4.7.3 -libMessivaVersion = 0.1.1 +lib_messiva_v = 0.1.1 -libJavaTelegramBotApiVersion = 6.2.0 +lib_javatelegramapi_v = 6.2.0 -libJunitVersion = 5.9.0 +lib_okhttp_v = 4.11.0 +lib_gson_v = 2.10.1 + +lib_junit_v = 5.10.0 diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java b/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java index 49f8655..d891a84 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java @@ -118,7 +118,7 @@ public class MornyCoeur { * @see #MornyCoeur 程序初始化方法 * @param config morny 实例的配置选项数据 */ - public static void main (MornyConfig config) { + public static void init (MornyConfig config) { if (INSTANCE == null) { logger.info("Coeur Starting"); diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java index bdc8893..a3d9907 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java @@ -169,12 +169,12 @@ public class MornyConfig { public long eventOutdatedTimestamp = -1; public boolean commandLoginRefresh = false; public boolean commandLogoutClear = false; - @Nonnull public Set dinnerTrustedReaders = new HashSet<>(); + @Nonnull public final Set dinnerTrustedReaders = new HashSet<>(); public long dinnerChatId = -1001707106392L; public long reportToChat = -1001650050443L; public long medicationNotifyToChat = -1001729016815L; @Nonnull public ZoneOffset medicationTimerUseTimezone = ZoneOffset.UTC; - @Nonnull public Set medicationNotifyAt = new HashSet<>(); + @Nonnull public final Set medicationNotifyAt = new HashSet<>(); } diff --git a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java index a4e2933..edd4719 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java @@ -1,7 +1,7 @@ package cc.sukazyo.cono.morny; import cc.sukazyo.cono.morny.daemon.MornyReport; -import cc.sukazyo.cono.morny.util.BuildConfigField; +import cc.sukazyo.cono.morny.internal.BuildConfigField; import cc.sukazyo.cono.morny.util.FileUtils; import javax.annotation.Nonnull; diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index 9a0e931..c437d96 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -79,7 +79,7 @@ public class ServerMain { * 自 {@code 0.5.0.4},旧的直接通过参数为 bot token & username 赋值的方式已被删除 * 使用参数所进行取值的 token 和 username 已被转移至 {@code --token} 和 {@code --username} 参数
      * - * @see MornyCoeur#main + * @see MornyCoeur#init * @since 0.4.0.0 * @param args 参数组 */ @@ -143,12 +143,17 @@ public class ServerMain { config.trustedChat = Long.parseLong(args[i]); continue; } - //noinspection SpellCheckingInspection + // noinspection SpellCheckingInspection case "--trusted-reader-dinner", "-trsd" -> { i++; config.dinnerTrustedReaders.add(Long.parseLong(args[i])); continue; } + case "--dinner-chat", "-chd" -> { + i++; + config.dinnerChatId = Long.parseLong(args[i]); + continue; + } case "--auto-cmd", "-cmd", "-c" -> { config.commandLoginRefresh = true; config.commandLogoutClear = true; @@ -278,7 +283,7 @@ public class ServerMain { Thread.currentThread().setName(THREAD_MORNY_INIT); try { - MornyCoeur.main(new MornyConfig(config)); + MornyCoeur.init(new MornyConfig(config)); } catch (MornyConfig.CheckFailure.NullTelegramBotKey ignore) { logger.info("Parameter required has no value:\n --token."); } catch (MornyConfig.CheckFailure e) { diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java index ddbe9e2..dbcfc81 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java @@ -37,50 +37,57 @@ public class EventHack implements ITelegramCommand { @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - boolean isOk = false; + enum Status { + OK, + FORBIDDEN_FOR_ANY + } + Status status; String x_mode = ""; if (command.hasArgs()) { x_mode = command.getArgs()[0]; } - switch (x_mode) { - case "any": + case "any" -> { if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) { OnEventHackHandle.registerHack( event.message().messageId(), event.message().from().id(), event.message().chat().id(), OnEventHackHandle.HackType.ANY - );isOk = true; + ); + status = Status.OK; + } else { + status = Status.FORBIDDEN_FOR_ANY; } - break; - case "group": + } + case "group" -> { OnEventHackHandle.registerHack( event.message().messageId(), event.message().from().id(), event.message().chat().id(), OnEventHackHandle.HackType.GROUP - );isOk = true; - break; - default: + ); + status = Status.OK; + } + default -> { OnEventHackHandle.registerHack( event.message().messageId(), event.message().from().id(), event.message().chat().id(), OnEventHackHandle.HackType.USER - );isOk = true; - break; + ); + status = Status.OK; + } } - if (isOk) { - MornyCoeur.extra().exec(new SendSticker( + switch (status) { + case OK -> MornyCoeur.extra().exec(new SendSticker( event.message().chat().id(), TelegramStickers.ID_WAITING ).replyToMessageId(event.message().messageId()) ); - } else { - MornyCoeur.extra().exec(new SendSticker( + case FORBIDDEN_FOR_ANY -> MornyCoeur.extra().exec(new SendSticker( event.message().chat().id(), TelegramStickers.ID_403 ).replyToMessageId(event.message().messageId()) diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java index d710459..5481a08 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java @@ -4,10 +4,10 @@ import cc.sukazyo.cono.morny.bot.api.EventListener; public class OnRandomlyTriggered extends EventListener { - /** - * function CODE_IK0XA1 - */ - // @Override +// /** +// * function CODE_IK0XA1 +// */ +// // @Override // public boolean onMessage (@Nonnull Update update) { // // if (update.message().text() == null) return false; diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java b/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java index 264a69a..f8a99fc 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java @@ -1,11 +1,11 @@ package cc.sukazyo.cono.morny.daemon; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; +import java.nio.file.StandardOpenOption; import java.util.HashMap; import java.util.TreeSet; import java.util.concurrent.locks.ReentrantLock; @@ -15,9 +15,11 @@ import static cc.sukazyo.cono.morny.Log.logger; public class TrackerDataManager { + /** {@link TrackerDaemon} 的锁。保证在程序中只有一个 TrackerDaemon 会运行。 */ public static final ReentrantLock trackingLock = new ReentrantLock(); - + /** {@link #record Tracker 缓存}的锁

      为保证对 Tracker 缓存的操作不会造成线程冲突,在操作缓存数据前应先取得此锁。 */ private static final ReentrantLock recordLock = new ReentrantLock(); + /** Tracker 数据的内存缓存

      进行数据操作前请先取得对应的{@link #recordLock 锁} */ private static HashMap>> record = new HashMap<>(); public static final TrackerDaemon DAEMON = new TrackerDaemon(); @@ -55,6 +57,15 @@ public class TrackerDataManager { } + /** + * 向 Tracker 缓存写入一条 tracker 数据. + *

      + * 这个方法对于 Tracker 缓存是原子化的。 + * + * @param chat tracker 所属的 Telegram Chat ID + * @param user tracker 所记录的 Telegram User ID + * @param timestamp tracker 被生成时的 UTC 时间戳 + */ public static void record (long chat, long user, long timestamp) { recordLock.lock(); if (!record.containsKey(chat)) record.put(chat, new HashMap<>()); @@ -65,16 +76,36 @@ public class TrackerDataManager { recordLock.unlock(); } + /** + * 开启 {@link TrackerDaemon}. + *

      + * 由于 Tracker 已废弃,这个方法已无作用。 + */ + @SuppressWarnings("unused") public static void init () { DAEMON.start(); } + /** + * 执行 Tracker 的保存逻辑. + * @see #reset() 弹出 Tracker 缓存 + * @see #save(HashMap) 执行硬盘写入操作 + */ public static void save () { logger.info("start writing tracker data."); save(reset()); logger.info("done writing tracker data."); } + /** + * 将 Tracker 的缓存数据弹出. + *

      + * 这个方法将返回现在 Tracker 的所有缓存数据,然后清除缓存。 + *

      + * 这个方法对于 Tracker 缓存是原子化的。 + * + * @return 当前 Tracker 所包含的内容 + */ private static HashMap>> reset () { recordLock.lock(); HashMap>> recordOld = record; @@ -83,6 +114,11 @@ public class TrackerDataManager { return recordOld; } + /** + * 将 Tracker 数据写入到硬盘. + * + * @param record 需要保存的 Tracker 数据集 + */ private static void save (HashMap>> record) { { @@ -109,10 +145,12 @@ public class TrackerDataManager { } assert channelCurrent != null; - channelCurrent.write(ByteBuffer.wrap( + final int result = channelCurrent.write(ByteBuffer.wrap( String.format("%d\n", timestamp).getBytes(StandardCharsets.UTF_8) )); + if (result == 0) logger.warn("writing tracker data %d/%d/%d: write only 0 bytes! is anything wrong?"); + } catch (Exception e) { final String message = String.format("exception in write tracker data: %d/%d/%d", chat, user, timestamp); logger.error(message); @@ -130,7 +168,7 @@ public class TrackerDataManager { if (!data.isDirectory()) if (!data.mkdirs()) throw new IOException("Cannot create file directory " + data.getPath()); File file = new File(data, String.valueOf(day)); if (!file.isFile()) if (!file.createNewFile()) throw new IOException("Cannot create file " + file.getPath()); - return new FileOutputStream(file, true).getChannel(); + return FileChannel.open(file.toPath(), StandardOpenOption.APPEND); } } diff --git a/src/main/java/cc/sukazyo/cono/morny/data/NbnhhshQuery.java b/src/main/java/cc/sukazyo/cono/morny/data/NbnhhshQuery.java index cd1be74..2ed6657 100644 --- a/src/main/java/cc/sukazyo/cono/morny/data/NbnhhshQuery.java +++ b/src/main/java/cc/sukazyo/cono/morny/data/NbnhhshQuery.java @@ -4,7 +4,7 @@ import java.io.IOException; import com.google.gson.Gson; -import okhttp3.MediaType; +import cc.sukazyo.cono.morny.util.OkHttpPublic.MediaTypes; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; @@ -23,21 +23,18 @@ public class NbnhhshQuery { public Word[] words; } - public record GuessReq (String text) { - } + public record GuessReq (String text) {} public static final String API_URL = "https://lab.magiconch.com/api/nbnhhsh/"; public static final String API_GUESS_METHOD = "guess/"; - public static final String API_GUESS_DATA_TEMPLATE = "{ \"text\": \"%s\" }"; private static final OkHttpClient httpClient = new OkHttpClient(); - public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); public static GuessResult sendGuess (String text) throws IOException { final String reqJsonText = new Gson().toJson(new GuessReq(text)); Request request = new Request.Builder() .url(API_URL + API_GUESS_METHOD) - .post(RequestBody.create(JSON, reqJsonText)) + .post(RequestBody.create(reqJsonText, MediaTypes.JSON)) .build(); try (Response response = httpClient.newCall(request).execute()) { final ResponseBody body = response.body(); diff --git a/src/main/java/cc/sukazyo/cono/morny/util/BuildConfigField.java b/src/main/java/cc/sukazyo/cono/morny/internal/BuildConfigField.java similarity index 89% rename from src/main/java/cc/sukazyo/cono/morny/util/BuildConfigField.java rename to src/main/java/cc/sukazyo/cono/morny/internal/BuildConfigField.java index c584a8b..f9931ce 100644 --- a/src/main/java/cc/sukazyo/cono/morny/util/BuildConfigField.java +++ b/src/main/java/cc/sukazyo/cono/morny/internal/BuildConfigField.java @@ -1,4 +1,4 @@ -package cc.sukazyo.cono.morny.util; +package cc.sukazyo.cono.morny.internal; import java.lang.annotation.Documented; diff --git a/src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java b/src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java new file mode 100644 index 0000000..fb6b32a --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java @@ -0,0 +1,13 @@ +package cc.sukazyo.cono.morny.util; + +import okhttp3.MediaType; + +public class OkHttpPublic { + + public static class MediaTypes { + + public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + + } + +} diff --git a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java index ef788b2..e634582 100644 --- a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java +++ b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java @@ -8,9 +8,9 @@ public class MornyCLI { public static void main (String[] args) { - Scanner line = new Scanner(System.in); System.out.print("$ java -jar morny-coeur-"+MornySystem.VERSION_FULL+".jar " ); - String x = line.nextLine(); + String x; + try (Scanner line = new Scanner(System.in)) { x = line.nextLine(); } ServerMain.main(UniversalCommand.format(x)); } diff --git a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java b/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java index 8fd9ed7..d35e55f 100644 --- a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java +++ b/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java @@ -1,16 +1,12 @@ package cc.sukazyo.cono.morny.util; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.stream.Stream; +import org.junit.jupiter.params.provider.EnumSource; import static cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex; import static cc.sukazyo.cono.morny.util.CommonConvert.byteToHex; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.params.provider.Arguments.arguments; public class TestCommonConvert { @@ -30,21 +26,22 @@ public class TestCommonConvert { assertEquals(expected, byteToHex(source)); } - public static Stream testByteArrayToHexProvider () { - return Stream.of( - arguments(new byte[]{0x00}, "00"), - arguments(new byte[]{(byte)0xff}, "ff"), - arguments(new byte[]{(byte)0xc3}, "c3"), - arguments(new byte[]{}, ""), - arguments(new byte[]{0x30,0x0a,0x00,0x04,(byte)0xb0,0x00}, "300a0004b000"), - arguments(new byte[]{0x00,0x00,0x0a,(byte)0xff,(byte)0xfc,(byte)0xab,(byte)0x00,0x04}, "00000afffcab0004"), - arguments(new byte[]{0x00,0x7c,0x11,0x28,(byte)0x88,(byte)0xa6,(byte)0xfc,0x30}, "007c112888a6fc30") - ); + public enum TestByteArrayToHexSource { + $1(new T(new byte[]{0x00}, "00")), + $2(new T(new byte[]{(byte)0xff}, "ff")), + $3(new T(new byte[]{(byte)0xc3}, "c3")), + $4(new T(new byte[]{}, "")), + $5(new T(new byte[]{0x30,0x0a,0x00,0x04,(byte)0xb0,0x00}, "300a0004b000")), + $6(new T(new byte[]{0x00,0x00,0x0a,(byte)0xff,(byte)0xfc,(byte)0xab,(byte)0x00,0x04}, "00000afffcab0004")), + $7(new T(new byte[]{0x00,0x7c,0x11,0x28,(byte)0x88,(byte)0xa6,(byte)0xfc,0x30}, "007c112888a6fc30")); + public record T (byte[] raw, String expected) {} + public final T value; + TestByteArrayToHexSource (T value) { this.value = value; } } @ParameterizedTest - @MethodSource("testByteArrayToHexProvider") - void testByteArrayToHex (byte[] raw, String expected) { - assertEquals(expected, byteArrayToHex(raw)); + @EnumSource + void testByteArrayToHex (TestByteArrayToHexSource source) { + assertEquals(source.value.expected, byteArrayToHex(source.value.raw)); } } From 213798dab70a630f3b1ef777d21dc54a1a2eb1d3 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 27 Aug 2023 13:00:36 +0800 Subject: [PATCH 28/40] added /info(main) and /start, added support for assets - add /info (without subcommand) - add about pic - add morny about links - add MornyAssets to manager & use assets - add TelegramImages to manager images that in use - with a AssetsFileImage - with IMG_ABOUT - add MornyAbout - changed MornyHello to MornyAbout - add about links (used in /info) - remove HelloOnStart --- build.gradle | 5 +- gradle.properties | 3 +- .../cc/sukazyo/cono/morny/MornyAbout.java | 31 +++++++ .../cc/sukazyo/cono/morny/MornyAssets.java | 19 ++++ .../cc/sukazyo/cono/morny/MornyHello.java | 70 --------------- .../cc/sukazyo/cono/morny/ServerMain.java | 4 +- .../cono/morny/bot/command/MornyCommands.java | 16 ++-- .../morny/bot/command/MornyInfoOnHello.java | 44 ++++++++++ ...nformations.java => MornyInformation.java} | 83 ++++++++++++++++-- .../cono/morny/daemon/MornyReport.java | 4 +- .../cono/morny/data/TelegramImages.java | 81 +++++++++++++++++ .../images/featured-image@0.5x.jpg | Bin 0 -> 293002 bytes .../assets_morny/texts/server-hello.txt | 55 ++++++++++++ 13 files changed, 324 insertions(+), 91 deletions(-) create mode 100644 src/main/java/cc/sukazyo/cono/morny/MornyAbout.java create mode 100644 src/main/java/cc/sukazyo/cono/morny/MornyAssets.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/MornyHello.java create mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.java rename src/main/java/cc/sukazyo/cono/morny/bot/command/{MornyInformations.java => MornyInformation.java} (77%) create mode 100644 src/main/java/cc/sukazyo/cono/morny/data/TelegramImages.java create mode 100644 src/main/resources/assets_morny/images/featured-image@0.5x.jpg create mode 100644 src/main/resources/assets_morny/texts/server-hello.txt diff --git a/build.gradle b/build.gradle index 1ba1412..aff5df9 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,6 @@ if (project.hasProperty("publishMvnRepoUrl")) { group proj_group version proj_version_full -setArchivesBaseName proj_archive_name repositories { mavenCentral() @@ -75,7 +74,8 @@ dependencies { compileOnlyApi "com.github.spotbugs:spotbugs-annotations:${lib_spotbugs_v}" - api "cc.sukazyo:messiva:${lib_messiva_v}" + implementation "cc.sukazyo:messiva:${lib_messiva_v}" + implementation "cc.sukazyo:resource-tools:${lib_resourcetools_v}" implementation "com.github.pengrad:java-telegram-bot-api:${lib_javatelegramapi_v}" implementation "com.squareup.okhttp3:okhttp:${lib_okhttp_v}" @@ -139,6 +139,7 @@ buildConfig { shadowJar { + archiveBaseName.set proj_archive_name archiveClassifier.set "fat" if (project.hasProperty("dockerBuild")) { diff --git a/gradle.properties b/gradle.properties index 24b3c02..2996049 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC3.9 +VERSION = 1.0.0-RC4 USE_DELTA = false VERSION_DELTA = @@ -17,6 +17,7 @@ CODENAME = beiping lib_spotbugs_v = 4.7.3 lib_messiva_v = 0.1.1 +lib_resourcetools_v = 0.2.2 lib_javatelegramapi_v = 6.2.0 diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyAbout.java b/src/main/java/cc/sukazyo/cono/morny/MornyAbout.java new file mode 100644 index 0000000..3933d9b --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/MornyAbout.java @@ -0,0 +1,31 @@ +package cc.sukazyo.cono.morny; + +import java.io.IOException; + +/** + * Some of the static information of Morny. + */ +public class MornyAbout { + + /** + * ASCII art of Morny Featured Image. + *

      + * used for Coeur starting welcome screen. + *

      + * stored at /assets_morny/texts/server-hello.txt + */ + public static final String MORNY_PREVIEW_IMAGE_ASCII; + static { + try { + MORNY_PREVIEW_IMAGE_ASCII = MornyAssets.pack.getResource("texts/server-hello.txt").readAsString(); + } catch (IOException e) { + throw new RuntimeException("Cannot read MORNY_PREVIEW_IMAGE_ASCII from assets pack", e); + } + } + + public static final String MORNY_SOURCECODE_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono"; + public static final String MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK = "https://storage.sukazyo.cc/Eyre_S/Coeur-Morny-Cono"; + public static final String MORNY_ISSUE_TRACKER_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono/issues"; + public static final String MORNY_USER_GUIDE_LINK = "https://book.sukazyo.cc/morny"; + +} diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyAssets.java b/src/main/java/cc/sukazyo/cono/morny/MornyAssets.java new file mode 100644 index 0000000..1fdfe7f --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/MornyAssets.java @@ -0,0 +1,19 @@ +package cc.sukazyo.cono.morny; + +import cc.sukazyo.restools.ResourcesPackage; + +/** + * Morny assets manager. + * + * @see #pack + * @since 1.0.0-RC4 + */ +public class MornyAssets { + + /** + * Instance mirror of the Morny assets, the assets root is /src/main/resources/assets_morny/. + * @since 1.0.0-RC4 + */ + public static final ResourcesPackage pack = new ResourcesPackage(MornyAssets.class, "assets_morny"); + +} diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyHello.java b/src/main/java/cc/sukazyo/cono/morny/MornyHello.java deleted file mode 100644 index 8a3725b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/MornyHello.java +++ /dev/null @@ -1,70 +0,0 @@ -package cc.sukazyo.cono.morny; - -/** - * {@link #MORNY_PREVIEW_IMAGE_ASCII} 静态数据存放类 - */ -@SuppressWarnings("all") -public class MornyHello { - - /** - * 系统的开屏欢迎语 ASCII 字符画字段 - */ - public static final String MORNY_PREVIEW_IMAGE_ASCII = """ - ttt///t/////fucj(\\tvnxtf{< .' .. .:i` . . ^!`l|-^i+,!_[:1/|{i?//\\//jf\\\\\\///\\\\\\\\//\\\\\\//////\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\//\\\\\\\\/\\\\\\\\/\\\\//\\\\\\///\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\fnncvvU0O00QCx!!". .. ` \s - tt//////////\\jzjrucnjt/?{j,,"' . .' .. .":. .;{: ' "`.,1(<."i?)\\(-}\\\\\\(((\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\///\\//////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|\\\\\\\\\\\\///\\\\///\\\\\\\\\\\\\\\\|\\\\\\\\\\\\|\\\\\\\\\\\\\\\\tvXvuXcxn/[Il)({_:.. ."` .,\s - //////////////////\\////|)/([}-_<+[]>.^^""[<'`^` .''""`'.`'`"i! ^!>l:' :<" !!.IiI`+l^^`i>_<`??)1;^{\\\\\\\\\\{|({({|/\\]I)\\\\()\\(]}|\\\\||\\|||\\/\\\\\\\\\\\\|||\\\\\\\\\\\\\\\\//\\\\\\\\/\\\\\\\\||\\\\\\\\\\\\\\\\//\\\\\\\\\\\\\\\\\\\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\//\\\\\\\\\\\\\\\\\\\\\\\\////\\\\\\\\\\\\\\\\\\//|{{?{|)[[-; - ttt/tt//////////////////{)(\\t(/tt/1~I}{-1\\_^])1_+[{|(?"<1~>>+!+[}11)}[(1}]};^1\\|~_1}{I:-1(I+)(|))|\\\\/////////\\\\////\\\\\\/////\\\\\\\\\\\\\\\\\\\\\\\\\\\\/\\//\\\\///\\//||\\////|)(//\\\\///){\\/\\(11|///({)//({[1\\\\\\\\\\\\\\\\\\\\|\\/\\\\\\/\\//////////\\\\\\\\\\\\\\\\\\//\\\\\\\\\\///////\\|\\\\\\\\//////\\\\///\\ - tttt/////////////\\///////\\||///////t//|(|)|}|\\/(\\\\(//(l_{{. ... ">+<^'I!: ^<(\\\\1}1//\\\\\\//////////\\\\///\\/\\///\\\\\\\\\\\\//\\\\//\\\\\\\\\\\\\\\\\\\\\\\\\\\\///\\(/\\{ - t////////////////////////////////////////////////////////\\/\\\\///////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\/\\\\\\\\\\\\||\\|\\\\\\\\\\|\\\\\\\\/\\\\|\\\\\\\\\\\\////((|///}!:,":,^`. .;' ' '^..':. ^!;. .^^ '^^`. '' ...I[{!>:^;_i:'~\\ttt/////tt//\\\\////////////\\\\\\\\/\\\\\\\\\\\\/\\\\\\\\/\\\\\\\\\\)}-+[+I??i - ttt////////////////////////////////////////////////////////\\\\//\\//\\\\/\\\\\\\\\\\\\\\\\\\\//\\\\\\\\\\\\||\\\\\\/\\//\\\\\\\\\\\\\\\\\\/\\|\\\\\\////\\1;``^;<>+!">__+I `' .. "'. .;" ;;. .:^ ``,,;'` .;]I ,-_-|\\////t////t///////\\/\\\\\\\\//\\\\\\\\\\\\//\\\\\\///////-II1ttt///tttt/////////\\/\\/////\\\\\\\\////t|+<}?!-]l<{[[1-+] - t//////////////////////////////////////\\/////////\\////////////////\\//////\\//tttttttttt//////////////////////////)_)t)|}1f/{<.^,^:~: . .. '''^:-|/> '-/}-_?\\/)-{?(//\\(\\tt////\\///\\\\\\\\\\\\\\///t1.;); .l~` '" - ///////////////////////////////////////////tt/t(|tt//]+{t\\{][|////\\//////////ttttt///t//t/////////////\\//////|//{[|f}!l>~++~<<\\//]l~?])tt//\\\\\\\\\\/\\\\///\\\\|?<_}["^!;I^;]:. . - ////////////////////////////////tttt/|{[1)]~!!+>!<_(/|[-<"i!l,]tt//ttt/t////ttt//t///ttttttt////////t//t//ttt){+. :?^ '. l_-!+l;;;|!!>~~il!lllllllllll!!lI:`'. .' :I;]_}>,?tf:.+fft)l+1//\\~`'I-(//\\/t/|/(-1[)/?>>II:' '.`';-'` \s - /////////////////////////t//()\\1_<>il^'''' ,!>;.,.'{tti `~tf(`'-(|fffftttttttttt/tttttttttttt///tttft//(t|]?-+!^ ."`. `. ;!I,. .?{il-\\_!~<>>!lII;IllIIIIIllllllllllI;;:,:,' '"^`(f{+{>' .<{t(I!}/||t> ^(//}>;:1\\]: "[:"` ^<: . II.'.. \s - ///////////////ttt//tt((-!+}"'^. I, ,?<:' ,:;!>~',!_~{}-1]`^!}_+\\ttttttt/tttttttfff/tt\\(||]-?+;,:"l" '..'.. ?]l:" -(lI;,~?~!IIIlllI:IIlI;IIIIlllllllIIIIIllII!; . . '^^;~), "~!}\\/t//\\\\/_. '". '_i !i''' \s - tt//t///ttt///(]<>l>][l"'.`,. ^.^. ii ;; ~>>>. .i~I'^^<}), .;|tfftttttttttttf\\]}t-!,,I` .^ '. !: . .",I;. ^,I<)/-l:;llllllI;lIll;;IIIIlllIllIlIIlIllI;><. ' .;}". '.:+](ft\\}(t/t{;<\\{l^>}!^l\\/{>1/t(lI:I!+<<". ':" \s - t//tttt|?+!I!:' '` .`. ...... `^ "<^.;`^"'`,!".,^^^.,?)!. [f/+>(/tttft\\tff|+^,!' '^: >[,++:`' .I^ . _?!:^. ;~{/?//)! __::. ':. '. \s - t/\\}[{]",il'`!-<-]:`'^` .. '' .^+:'. .^'"i:`^. ';`:<_|>'.?/t/!"<)ffftf)]]!'II.,l ^' ''. '";" .' `Il, ;]>]j_;lI;ll;!!llIII>~IIIIII;: ;,~.',.<:`, 'I_|\\; .i|/]^ ?(}\\/////\\i' '' ....'^ \s - tf1<}i `^. `I` .I?"'. . . ^' .^' .'` .". >}_.I|t{_(tf({~,~(); ')t};.><,. .. . .. . . .]}^{j1IlllIlI!1IlllII?{IIIIIlI;[1!I;IIllIIlIIllI<]_;+/t\\(|\\/1,' "` ... \s - )+::((:^' ll .,` . . ..'. ' :+'`{tj{,l: ^;"..;!"^.I?' '~; .` .'. . `1+ [x?-:lIlll!]r-IllI~~{~I>lllll[i\\--+;;I~IIIII!l;x] "I"-<<_> >i.' l{}:itf/}[/\\)(\\}))|(:^^..'. `". \s - >.._f|i.:l,;^^''__. .^' `' "+,`]1i`!1_. ^l: .". .` '1I +JIt!IIlll;]\\) >1}c(_i(!IllI_l;(f. ,_";~~+^ .. .;-i '+([i+: !//1](||/\\(?:^..^^ \s - i'"}_,.` ''^... '. `. ^<`_> .. +x??_~]:[|!,.ll` . {+ ;Y[^|,>~IlIIf\\ {/;I!\\ [[-'<+l-{ _??]f\\n]lllI[!;1v` `+"]-}]~" ..'`l, ''i-` l+?\\\\\\/t{!)t[:' .^^ \s - ;,:: :,^..;:. . i+..;^ `_ ]]<-?l``-]' I>]?+. ^-|\\\\_I?]{t/?` .... \s - ^(\\]I^~?;."!" . .. . ^<^ :( >t) _[il>|+:(U<1nYQ0Xx\\> . .~xcXXYzx(n?IllI}">xCI .:1]_-" . .^. `}>!}((1-^,+?" .. \s - 1+,~I.(l' . ... ~|r:;`.+I?\\};+t) "".-?;lI>(;]xn. '_>]!+. '^'`l:11l[|((+?: . \s - [-`.':;..""' lv|. .:_(;I!u> ^,",^. .;?I]?IlI}n[^ ')(I' `tlII>x1" <}1{)l "~+ |[II;\\[:~zl .i;. .... `"i\\\\}]..!' ^.... \s - -" ` ' . ":` .|+<<;!\\U)>^ '^`' ^"I?)c-;j/ <1I~;` .!}\\(: .;"`' \s - ' 'l, .' ~[><+;!f()nn|]!:' ..^:!+1fcjx}}v!_)})>|n` ~^ ^;"'` .<+I<)/||\\i'"<"'^ `. \s - II ^}_'+_!fI?_/-jJjUr\\\\ucJJz\\|J>}?-j{]^ni" .;](),.;-<`' .^ .^' \s - +[" +]{.`i;I; ::."!??l.^Ywj}<, (n, ,~_:` .,, ` `` ' '... ^+-l,]}]}\\j/!. . ` 'I<~`'{tl..^` \s - '' .,<{[>" i/i" `-[; ,<_[>^i_l,:^_! ',+l.. ^,:,,. ;~>l;^ l> ',;I^???~,'l".. .. \s - ;{?l. !+ .. .<1i '^' "}|{:-+-;?\\[)-] ^:l1-:. '' '`, . ';.`~^ '. ..^`. \s - 'i+;]}!,. <))\\!<|ji >((_}}?t)}\\\\v|]?jI!), "lf!l. ... .^ . ". \s - .+{>` l/z\\!,>""I+~_){]vQjut_~~>>>_-<]<-)f":l_v){\\/1}}}{t/\\0?z~. ^' `-l . . .. . \s - l\\ :_>>i:^+\\)_-]!:>-+l'...`^;1!^ 'l>})l\\n\\Qt?]?]})1{][[(XC>^ ... - ^-+^.i-((?!"`:>l<[~<]nQY+?????][{\\cmO||l . ''.. - '|: '^[{~)\\_+++))1{uxnvt(t){{[[u0\\1|({1()){-?|xfc: .. \s - .<-, ]-]]]})11)){{{{}{{)|{}}{1{111{11{{}}]_!"x\\]Xf \s - ,]<\\}][[[[[[[[[[[[[[[]][[[[[[[[][[[[[[]?-+!YC{z} .` - "_[}?]][[[[[[[[[[[[[[[[[[][[[[[[[[[[[[[[[]vn\\?. ^ - ^. ;{_(_??][[[[[[[[[[[[[[[[[|[[[[[[[[[[[[[[[[v_(]^ \s - .' '. :t>/?[[[[[[[[[[[[[[[[[[[]t\\][[[[[[[[[[][]?u;()_ .. .'. . . - `. .` :)!j_]][[[]]]]][[[[[[[[[[}j(/{[[[[[][[}1{~n!)ft . .. .. . .. . - ". ..' .` . "' .^":;~ti{\\1]][]]]??]]]]][]]??[){[}[[[[?+}]!^':``^``,`;I.!<>?>:??i:;-;,<_..^,Ii: 'l,i+```' ..'!; ''. ?~.'<+ .li!:,1?}[[[[[[[[[[[[[[[[[[[[)1}]t[:, "O" . .' . ^!' . ". i+' . \s - ^. . .':`'`~/1,-<-~^'^'^^,`.."i_>^. `1t]!,^I]l^;`,I::_?]?[!:;`.`"'`l!l<1f{~>;]\\1(]I>~l!l[<,,;`lI,~},^>!>l'...'": 'x' ]l>i .:I1[~]]]]}}}}}[[}}[}}}}}[[]]]??1}}[}~;:>vx. :;..:??,.' ^` I;>.";:"^' .. ^'.^"" ' .!},' . - ~!;:!".":i"^_/|]^li(\\1;;it{' .[\\fft+<}(/{}/)|f||'.^{/[)?!:(?+-,I+fjtil"'"+fj{i:',!!;!!^:.`r. r; !l'"i1?!i>~+_?][[[?-???]][[]?-+-]??+~<_{[_l?> ''^l;l`-}<`^.i>+l ``;I":+?!~-l ,>>l.'.;. ':!!(/!":,I - /t1ffft+{jff/ttff)];)?1(/tt\\/t/tfttttfftf/1\\|t\\|/?<_]_]{<_]/f({fffjttf/[i>1//|tft|" :<~:+}, ]>if" .:-~ >) ^`^l)f(_{/\\}-+1\\()t-{j/]!:^'l<]\\)+ ."_?I_{ - ffft)|)(t[_-{tjjrjrj/{(||}(rjj\\1)I<\\((ffj/rjffttjffftrjfffrtfff/f[1jjffffftt//)}tttff/ttt[<{rj}tf1?<:~{/j)>)fttf|?)tfffftt1_;+tf1-1|~i1, >;:} '1_ ;( .. .. `:"_1{}tjtvj)vjr/|jfff/<(tf)+1/)1j)~~-[j[l|[(/\\j{:-]]([}\\t - """; - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java index c437d96..208ed1b 100644 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java @@ -34,7 +34,7 @@ public class ServerMain { * {@code --version} 只输出版本信息,不运行主程序。此参数会导致其它所有参数失效(优先级最高) * *

    • - * {@code --only-hello} 只输出欢迎字符画({@link MornyHello}),不运行主程序。 + * {@code --only-hello} 只输出欢迎字符画({@link MornyAbout#MORNY_PREVIEW_IMAGE_ASCII}),不运行主程序。 * 不要同时使用 {@code --no-hello},原因见下。 *
    • *
    • @@ -222,7 +222,7 @@ public class ServerMain { //# 启动相关参数的检查和处理 //# - if (showWelcome) logger.info(MornyHello.MORNY_PREVIEW_IMAGE_ASCII); + if (showWelcome) logger.info(MornyAbout.MORNY_PREVIEW_IMAGE_ASCII); if (welcomeEchoMode) return; unknownArgs.forEach(arg -> logger.warn("Can't understand arg to some meaning :\n " + arg)); diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index c3f78bb..e84ab3e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -54,7 +54,8 @@ public class MornyCommands { register( new ON(), - new Hello(), new HelloOnStart(), + new Hello(), /* new {@link HelloOnStart}, */ + new MornyInfoOnHello(), new GetUsernameAndId(), new EventHack(), new Nbnhhsh(), @@ -62,7 +63,7 @@ public class MornyCommands { new Ip186Query.Whois(), new Encryptor(), new SaveData(), - new MornyInformations(), + new MornyInformation(), new Version(), new MornyRuntime(), new Jrrp(), @@ -138,7 +139,7 @@ public class MornyCommands { } private BotCommand formatTelegramCommandListLine (@Nonnull String commandName, @Nonnull String paramRule, @Nonnull String intro) { - return new BotCommand(commandName, "".equals(paramRule) ? (intro) : (paramRule+" - "+intro)); + return new BotCommand(commandName, paramRule.isEmpty() ? (intro) : (paramRule+" - "+intro)); } private boolean nonCommandExecutable (Update event, InputCommand command) { @@ -181,6 +182,11 @@ public class MornyCommands { @Nonnull @Override public String getDescription () { return "打招呼"; } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandHelloExec(event); } } + /** + * {@link Hello} on special command /start + * Deprecated due to new {@link MornyInfoOnHello} + */ + @Deprecated @SuppressWarnings("unused") private static class HelloOnStart implements ISimpleCommand { @Nonnull @Override public String getName () { return "start"; }@Nullable @Override public String[] getAliases () { return new String[0]; }@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandHelloExec(event); }} private static void onCommandHelloExec (@Nonnull Update event) { MornyCoeur.extra().exec(new SendSticker( @@ -227,7 +233,7 @@ public class MornyCommands { @Nullable @Override public String[] getAliases () { return null; } @Nonnull @Deprecated public String getParamRule () { return ""; } @Nonnull @Deprecated public String getDescription () { return "检查 Bot 版本信息"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformations.echoVersion(event); } + @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformation.echoVersion(event); } } private static class MornyRuntime implements ISimpleCommand { @@ -235,7 +241,7 @@ public class MornyCommands { @Nullable @Override public String[] getAliases () { return null; } @Nonnull @Deprecated public String getParamRule () { return ""; } @Nonnull @Deprecated public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformations.echoRuntime(event); } + @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformation.echoRuntime(event); } } private static class Jrrp implements ITelegramCommand { diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.java new file mode 100644 index 0000000..245bae8 --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.java @@ -0,0 +1,44 @@ +package cc.sukazyo.cono.morny.bot.command; + +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.ParseMode; +import com.pengrad.telegrambot.request.SendPhoto; + +import cc.sukazyo.cono.morny.MornyCoeur; +import cc.sukazyo.cono.morny.util.tgapi.InputCommand; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The implementation of Telegram special command `/start`. + * + * @see MornyInformation related class where some data comes from. + * + * @since 1.0.0-RC4 + */ +public class MornyInfoOnHello implements ISimpleCommand { + + @Nonnull @Override public String getName() { return "start"; } + @Nullable @Override public String[] getAliases() { return new String[0]; } + // @Override public String getParamRule() { return ""; } + // @Override public String getDescription() { return "" } + + @Override + public void execute(@Nonnull InputCommand command, @Nonnull Update event) { + MornyCoeur.extra().exec(new SendPhoto( + event.message().chat().id(), + MornyInformation.getAboutPic() + ).caption(""" + 欢迎使用 Morny Cono来自安妮的侍从小鼠。 + Morny 具有各种各样的功能。 + + ———————————————— + %s + ———————————————— + + (你可以随时通过 /info 重新获得这些信息)""".formatted(MornyInformation.getMornyAboutLinksHTML()) + ).parseMode(ParseMode.HTML)); + } + +} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformation.java similarity index 77% rename from src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java rename to src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformation.java index e9a6ad2..6a204e0 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java +++ b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformation.java @@ -1,14 +1,18 @@ package cc.sukazyo.cono.morny.bot.command; import cc.sukazyo.cono.morny.BuildConfig; +import cc.sukazyo.cono.morny.MornyAbout; import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.MornySystem; +import cc.sukazyo.cono.morny.data.TelegramImages; import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.util.tgapi.ExtraAction; import cc.sukazyo.cono.morny.util.tgapi.InputCommand; + import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.SendMessage; +import com.pengrad.telegrambot.request.SendPhoto; import com.pengrad.telegrambot.request.SendSticker; import javax.annotation.Nonnull; @@ -21,7 +25,7 @@ import static cc.sukazyo.cono.morny.util.CommonFormat.formatDate; import static cc.sukazyo.cono.morny.util.CommonFormat.formatDuration; import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; -public class MornyInformations implements ITelegramCommand { +public class MornyInformation implements ITelegramCommand { private static final String SUB_STICKER = "stickers"; private static final String SUB_RUNTIME = "runtime"; @@ -30,14 +34,14 @@ public class MornyInformations implements ITelegramCommand { @Nonnull @Override public String getName () { return "info"; } @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[subcommand]"; } + @Nonnull @Override public String getParamRule () { return "[(version|runtime|stickers[.IDs])]"; } @Nonnull @Override public String getDescription () { return "输出当前 Morny 的各种信息"; } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { if (!command.hasArgs()) { - echoRuntime(event); + echoInfo(event.message().chat().id(), event.message().messageId()); return; } @@ -56,7 +60,26 @@ public class MornyInformations implements ITelegramCommand { } /** - * /info 子命令 {@value #SUB_STICKER} + * Subcommand /info without params. + * + * @since 1.0.0-RC4 + */ + public void echoInfo (long chatId, int replayToMessage) { + MornyCoeur.extra().exec(new SendPhoto( + chatId, + getAboutPic() + ).caption(""" + Morny Cono + 来自安妮的侍从小鼠。 + ———————————————— + %s""".formatted(getMornyAboutLinksHTML()) + ).parseMode(ParseMode.HTML).replyToMessageId(replayToMessage)); + } + + /** + * subcommand /info stickers + * + * @see #SUB_STICKER */ public void echoStickers (@Nonnull InputCommand command, @Nonnull Update event) { final long echoTo = event.message().chat().id(); @@ -78,22 +101,28 @@ public class MornyInformations implements ITelegramCommand { } /** - * 向 telegram 输出一个或全部 sticker + * 向 telegram 输出一个或全部 sticker. + * * @param id * sticker 在 {@link TelegramStickers} 中的字段名。 * 使用 {@link ""}(空字符串)(不是{@link null}) 表示输出全部 sticker * @param chatId 目标 chat id - * @param messageId 要回复的消息 id,特殊值跟随上游逻辑 + * @param messageId 要回复的消息 id,依据 {@link TelegramStickers#echoStickerByID(String, ExtraAction, long, int) 上游} + * 逻辑,使用 {@link -1} 表示不回复消息。 + * * @see TelegramStickers#echoStickerByID(String, ExtraAction, long, int) * @see TelegramStickers#echoAllStickers(ExtraAction, long, int) */ public static void echoStickers (@Nonnull String id, long chatId, int messageId) { - if ("".equals(id)) TelegramStickers.echoAllStickers(MornyCoeur.extra(), chatId, messageId); + if (id.isEmpty()) TelegramStickers.echoAllStickers(MornyCoeur.extra(), chatId, messageId); else TelegramStickers.echoStickerByID(id, MornyCoeur.extra(), chatId, messageId); } /** - * /info 子命令 {@value #SUB_RUNTIME} + * Subcommand /info runtime. + * + * @see #SUB_RUNTIME + * * @since 1.0.0-alpha4 */ public static void echoRuntime (@Nonnull Update event) { @@ -146,7 +175,11 @@ public class MornyInformations implements ITelegramCommand { } /** - * /info 子命令 {@value #SUB_VERSION} + * Subcommand /info version or /info v. + * + * @see #SUB_VERSION + * @see #SUB_VERSION_2 + * * @since 1.0.0-alpha4 */ public static void echoVersion (@Nonnull Update event) { @@ -175,11 +208,13 @@ public class MornyInformations implements ITelegramCommand { /** * 取得 {@link MornySystem} 的 git commit 相关版本信息的 HTML 格式化标签. + * * @return 格式类似于 {@code 28e8c82a.δ} 的以 HTML 方式格式化的版本号组件。 * 其中 {@code .δ} 对应着 {@link MornySystem#isCleanBuild}; * commit tag 字段如果支援 {@link MornySystem#currentCodePath} 则会以链接形式解析,否则则为 code 格式 * 为了对 telegram api html 格式兼容所以不支援嵌套链接与code标签。 * 如果 {@link MornySystem#isGitBuild} 为 {@link false},则方法会返回 {@link ""} + * * @since 1.0.0-beta2 */ @Nonnull @@ -226,6 +261,18 @@ public class MornyInformations implements ITelegramCommand { } } + /** + * Get the about-pic (intro picture or featured image) of Morny. + * + * @return the Telegram file binary data of the about-pic. + * @throws IllegalStateException {@link TelegramImages.AssetsFileImage#get() get() image data} may + * throws {@link IllegalStateException} while read error. + */ + @Nonnull + public static byte[] getAboutPic () { + return TelegramImages.IMG_ABOUT.get(); + } + private static void echo404 (@Nonnull Update event) { MornyCoeur.extra().exec(new SendSticker( event.message().chat().id(), @@ -233,4 +280,22 @@ public class MornyInformations implements ITelegramCommand { ).replyToMessageId(event.message().messageId())); } + /** + * The formatted about links of Morny Cono and Morny Coeur. + *

      + * With the Telegram HTML formatting, used in /info and /start. + * Provided the end user the links that can find resources about Morny. + */ + @Nonnull + public static String getMornyAboutLinksHTML () { + return """ + source code | backup + 反馈 / issue tracker + 使用说明书 / user guide & docs""".formatted( + MornyAbout.MORNY_SOURCECODE_LINK, MornyAbout.MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK, + MornyAbout.MORNY_ISSUE_TRACKER_LINK, + MornyAbout.MORNY_USER_GUIDE_LINK + ); + } + } diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java index 0aa990e..11a646c 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java @@ -1,7 +1,7 @@ package cc.sukazyo.cono.morny.daemon; import cc.sukazyo.cono.morny.*; -import cc.sukazyo.cono.morny.bot.command.MornyInformations; +import cc.sukazyo.cono.morny.bot.command.MornyInformation; import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; import com.google.gson.GsonBuilder; @@ -88,7 +88,7 @@ public class MornyReport { as config fields: %s """, - MornyInformations.getVersionAllFullTagHtml(), + MornyInformation.getVersionAllFullTagHtml(), MornyCoeur.getUsername(), sectionConfigFields(MornyCoeur.config()) ) diff --git a/src/main/java/cc/sukazyo/cono/morny/data/TelegramImages.java b/src/main/java/cc/sukazyo/cono/morny/data/TelegramImages.java new file mode 100644 index 0000000..f433c65 --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/data/TelegramImages.java @@ -0,0 +1,81 @@ +package cc.sukazyo.cono.morny.data; + +import cc.sukazyo.cono.morny.MornyAssets; +import cc.sukazyo.cono.morny.daemon.MornyReport; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; + +import static cc.sukazyo.cono.morny.Log.exceptionLog; +import static cc.sukazyo.cono.morny.Log.logger; + +/** + * The images of morny will use. + * + * @since 1.0.0-RC4 + */ +public class TelegramImages { + + /** + * Image that stored in the {@link MornyAssets#pack}. + *

      + * It has a final {@link #assetsPath} record that is its store location, + * and has a {@link #cache} of the binary data. + * + * @since 1.0.0-RC4 + */ + public static class AssetsFileImage { + + /** the path where the image is stored in {@link MornyAssets#pack}. */ + @Nonnull private final String assetsPath; + /** the binary data cache of the image.

      {@link null} means it hasn't been cached. */ + @Nullable private byte[] cache = null; + + /** + * An {@link AssetsFileImage}. + * + * @param path the image path relative to {@link MornyAssets#pack}'s root. + */ + AssetsFileImage (@Nonnull String path) { + this.assetsPath = path; + } + + /** + * Get the binary data. + *

      + * Will read the {@link #cache} firstly. If read {@link null}, + * then it will try {@link #read load the file}, and read again. + * + * @return The binary data of the image. + * @throws IllegalStateException While the {@link #read()} failed to read data and + * the result {@link #cache} is still null + */ + @Nonnull public byte[] get() { + if (cache == null) read(); + if (cache == null) throw new IllegalStateException("Failed get assets file image."); + return cache; + } + + /** + * Load the file from {@link MornyAssets#pack}, and stored the binary data to {@link #cache}. + *

      + * If failed, it will output the exception to the log and the {@link MornyReport}, + * and remains the cache's current data. + */ + private void read() { + try (InputStream stream = MornyAssets.pack.getResource(assetsPath).read()) { + this.cache = stream.readAllBytes(); + } catch (IOException e) { + logger.error("Cannot read resource file"); + logger.error(exceptionLog(e)); + MornyReport.exception(e, "Cannot read resource file"); + } + } + + } + + public static final AssetsFileImage IMG_ABOUT = new AssetsFileImage("images/featured-image@0.5x.jpg"); + +} diff --git a/src/main/resources/assets_morny/images/featured-image@0.5x.jpg b/src/main/resources/assets_morny/images/featured-image@0.5x.jpg new file mode 100644 index 0000000000000000000000000000000000000000..444cf8f3eb9a0b4db97822c12bb9d553e0f94326 GIT binary patch literal 293002 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%0A@k9#X@Z*S32+3Ou$(GT!_MP?o@ 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Úp1L(H#i#i$IY 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

      {awJj!;0~D_ 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:.. ."` ., +//////////////////\////|)/([}-_<+[]>.^^""[<'`^` .''""`'.`'`"i! ^!>l:' :<" !!.IiI`+l^^`i>_<`??)1;^{\\\\\{|({({|/\]I)\\()\(]}|\\||\|||\/\\\\\\|||\\\\\\\\//\\\\/\\\\||\\\\\\\\//\\\\\\\\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\//\\\\\\\\\\\\////\\\\\\\\\//|{{?{|)[[-; +ttt/tt//////////////////{)(\t(/tt/1~I}{-1\_^])1_+[{|(?"<1~>>+!+[}11)}[(1}]};^1\|~_1}{I:-1(I+)(|))|\\/////////\\////\\\/////\\\\\\\\\\\\\\/\//\\///\//||\////|)(//\\///){\/\(11|///({)//({[1\\\\\\\\\\|\/\\\/\//////////\\\\\\\\\//\\\\\///////\|\\\\//////\\///\ +tttt/////////////\///////\||///////t//|(|)|}|\/(\\(//(l_{{. ... ">+<^'I!: ^<(\\1}1//\\\//////////\\///\/\///\\\\\\//\\//\\\\\\\\\\\\\\///\(/\{ +t////////////////////////////////////////////////////////\/\\///////\\\\\\\\\\\\\\\/\\\\\\||\|\\\\\|\\\\/\\|\\\\\\////((|///}!:,":,^`. .;' ' '^..':. ^!;. .^^ '^^`. '' ...I[{!>:^;_i:'~\ttt/////tt//\\////////////\\\\/\\\\\\/\\\\/\\\\\)}-+[+I??i +ttt////////////////////////////////////////////////////////\\//\//\\/\\\\\\\\\\//\\\\\\||\\\/\//\\\\\\\\\/\|\\\////\1;``^;<>+!">__+I `' .. "'. .;" ;;. .:^ ``,,;'` .;]I ,-_-|\////t////t///////\/\\\\//\\\\\\//\\\///////-II1ttt///tttt/////////\/\/////\\\\////t|+<}?!-]l<{[[1-+] +t//////////////////////////////////////\/////////\////////////////\//////\//tttttttttt//////////////////////////)_)t)|}1f/{<.^,^:~: . .. '''^:-|/> '-/}-_?\/)-{?(//\(\tt////\///\\\\\\\///t1.;); .l~` '" +///////////////////////////////////////////tt/t(|tt//]+{t\{][|////\//////////ttttt///t//t/////////////\//////|//{[|f}!l>~++~<<\//]l~?])tt//\\\\\/\\///\\|?<_}["^!;I^;]:. . +////////////////////////////////tttt/|{[1)]~!!+>!<_(/|[-<"i!l,]tt//ttt/t////ttt//t///ttttttt////////t//t//ttt){+. :?^ '. l_-!+l;;;|!!>~~il!lllllllllll!!lI:`'. .' :I;]_}>,?tf:.+fft)l+1//\~`'I-(//\/t/|/(-1[)/?>>II:' '.`';-'` +/////////////////////////t//()\1_<>il^'''' ,!>;.,.'{tti `~tf(`'-(|fffftttttttttt/tttttttttttt///tttft//(t|]?-+!^ ."`. `. ;!I,. .?{il-\_!~<>>!lII;IllIIIIIllllllllllI;;:,:,' '"^`(f{+{>' .<{t(I!}/||t> ^(//}>;:1\]: "[:"` ^<: . II.'.. +///////////////ttt//tt((-!+}"'^. I, ,?<:' ,:;!>~',!_~{}-1]`^!}_+\ttttttt/tttttttfff/tt\(||]-?+;,:"l" '..'.. ?]l:" -(lI;,~?~!IIIlllI:IIlI;IIIIlllllllIIIIIllII!; . . '^^;~), "~!}\/t//\\/_. '". '_i !i''' +tt//t///ttt///(]<>l>][l"'.`,. ^.^. ii ;; ~>>>. .i~I'^^<}), .;|tfftttttttttttf\]}t-!,,I` .^ '. !: . .",I;. ^,I<)/-l:;llllllI;lIll;;IIIIlllIllIlIIlIllI;><. ' .;}". '.:+](ft\}(t/t{;<\{l^>}!^l\/{>1/t(lI:I!+<<". ':" +t//tttt|?+!I!:' '` .`. ...... `^ "<^.;`^"'`,!".,^^^.,?)!. [f/+>(/tttft\tff|+^,!' '^: >[,++:`' .I^ . _?!:^. ;~{/?//)! __::. ':. '. +t/\}[{]",il'`!-<-]:`'^` .. '' .^+:'. .^'"i:`^. ';`:<_|>'.?/t/!"<)ffftf)]]!'II.,l ^' ''. '";" .' `Il, ;]>]j_;lI;ll;!!llIII>~IIIIII;: ;,~.',.<:`, 'I_|\; .i|/]^ ?(}\/////\i' '' ....'^ +tf1<}i `^. `I` .I?"'. . . ^' .^' .'` .". >}_.I|t{_(tf({~,~(); ')t};.><,. .. . .. . . .]}^{j1IlllIlI!1IlllII?{IIIIIlI;[1!I;IIllIIlIIllI<]_;+/t\(|\/1,' "` ... +)+::((:^' ll .,` . . ..'. ' :+'`{tj{,l: ^;"..;!"^.I?' '~; .` .'. . `1+ [x?-:lIlll!]r-IllI~~{~I>lllll[i\--+;;I~IIIII!l;x] "I"-<<_> >i.' l{}:itf/}[/\)(\}))|(:^^..'. `". +>.._f|i.:l,;^^''__. .^' `' "+,`]1i`!1_. ^l: .". .` '1I +JIt!IIlll;]\) >1}c(_i(!IllI_l;(f. ,_";~~+^ .. .;-i '+([i+: !//1](||/\(?:^..^^ +i'"}_,.` ''^... '. `. ^<`_> .. +x??_~]:[|!,.ll` . {+ ;Y[^|,>~IlIIf\ {/;I!\ [[-'<+l-{ _??]f\n]lllI[!;1v` `+"]-}]~" ..'`l, ''i-` l+?\\\/t{!)t[:' .^^ +;,:: :,^..;:. . i+..;^ `_ ]]<-?l``-]' I>]?+. ^-|\\_I?]{t/?` .... +^(\]I^~?;."!" . .. . ^<^ :( >t) _[il>|+:(U<1nYQ0Xx\> . .~xcXXYzx(n?IllI}">xCI .:1]_-" . .^. `}>!}((1-^,+?" .. +1+,~I.(l' . ... ~|r:;`.+I?\};+t) "".-?;lI>(;]xn. '_>]!+. '^'`l:11l[|((+?: . +[-`.':;..""' lv|. .:_(;I!u> ^,",^. .;?I]?IlI}n[^ ')(I' `tlII>x1" <}1{)l "~+ |[II;\[:~zl .i;. .... `"i\\}]..!' ^.... +-" ` ' . ":` .|+<<;!\U)>^ '^`' ^"I?)c-;j/ <1I~;` .!}\(: .;"`' +' 'l, .' ~[><+;!f()nn|]!:' ..^:!+1fcjx}}v!_)})>|n` ~^ ^;"'` .<+I<)/||\i'"<"'^ `. + II ^}_'+_!fI?_/-jJjUr\\ucJJz\|J>}?-j{]^ni" .;](),.;-<`' .^ .^' + +[" +]{.`i;I; ::."!??l.^Ywj}<, (n, ,~_:` .,, ` `` ' '... ^+-l,]}]}\j/!. . ` 'I<~`'{tl..^` + '' .,<{[>" i/i" `-[; ,<_[>^i_l,:^_! ',+l.. ^,:,,. ;~>l;^ l> ',;I^???~,'l".. .. + ;{?l. !+ .. .<1i '^' "}|{:-+-;?\[)-] ^:l1-:. '' '`, . ';.`~^ '. ..^`. + 'i+;]}!,. <))\!<|ji >((_}}?t)}\\v|]?jI!), "lf!l. ... .^ . ". + .+{>` l/z\!,>""I+~_){]vQjut_~~>>>_-<]<-)f":l_v){\/1}}}{t/\0?z~. ^' `-l . . .. . + l\ :_>>i:^+\)_-]!:>-+l'...`^;1!^ 'l>})l\n\Qt?]?]})1{][[(XC>^ ... + ^-+^.i-((?!"`:>l<[~<]nQY+?????][{\cmO||l . ''.. + '|: '^[{~)\_+++))1{uxnvt(t){{[[u0\1|({1()){-?|xfc: .. + .<-, ]-]]]})11)){{{{}{{)|{}}{1{111{11{{}}]_!"x\]Xf + ,]<\}][[[[[[[[[[[[[[[]][[[[[[[[][[[[[[]?-+!YC{z} .` + "_[}?]][[[[[[[[[[[[[[[[[[][[[[[[[[[[[[[[[]vn\?. ^ + ^. ;{_(_??][[[[[[[[[[[[[[[[[|[[[[[[[[[[[[[[[[v_(]^ + .' '. :t>/?[[[[[[[[[[[[[[[[[[[]t\][[[[[[[[[[][]?u;()_ .. .'. . . + `. .` :)!j_]][[[]]]]][[[[[[[[[[}j(/{[[[[[][[}1{~n!)ft . .. .. . .. . + ". ..' .` . "' .^":;~ti{\1]][]]]??]]]]][]]??[){[}[[[[?+}]!^':``^``,`;I.!<>?>:??i:;-;,<_..^,Ii: 'l,i+```' ..'!; ''. ?~.'<+ .li!:,1?}[[[[[[[[[[[[[[[[[[[[)1}]t[:, "O" . .' . ^!' . ". i+' . +^. . .':`'`~/1,-<-~^'^'^^,`.."i_>^. `1t]!,^I]l^;`,I::_?]?[!:;`.`"'`l!l<1f{~>;]\1(]I>~l!l[<,,;`lI,~},^>!>l'...'": 'x' ]l>i .:I1[~]]]]}}}}}[[}}[}}}}}[[]]]??1}}[}~;:>vx. :;..:??,.' ^` I;>.";:"^' .. ^'.^"" ' .!},' . +~!;:!".":i"^_/|]^li(\1;;it{' .[\fft+<}(/{}/)|f||'.^{/[)?!:(?+-,I+fjtil"'"+fj{i:',!!;!!^:.`r. r; !l'"i1?!i>~+_?][[[?-???]][[]?-+-]??+~<_{[_l?> ''^l;l`-}<`^.i>+l ``;I":+?!~-l ,>>l.'.;. ':!!(/!":,I +/t1ffft+{jff/ttff)];)?1(/tt\/t/tfttttfftf/1\|t\|/?<_]_]{<_]/f({fffjttf/[i>1//|tft|" :<~:+}, ]>if" .:-~ >) ^`^l)f(_{/\}-+1\()t-{j/]!:^'l<]\)+ ."_?I_{ +ffft)|)(t[_-{tjjrjrj/{(||}(rjj\1)I<\((ffj/rjffttjffftrjfffrtfff/f[1jjffffftt//)}tttff/ttt[<{rj}tf1?<:~{/j)>)fttf|?)tfffftt1_;+tf1-1|~i1, >;:} '1_ ;( .. .. `:"_1{}tjtvj)vjr/|jfff/<(tf)+1/)1j)~~-[j[l|[(/\j{:-]]([}\t From aafdcc1fb213017c94559d7bf8293bf6811f67a9 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Tue, 5 Sep 2023 14:14:01 +0800 Subject: [PATCH 29/40] scala port stage1 (not tested) --- .dockerignore | 2 + .gitignore | 2 + build.gradle | 56 ++-- gradle.properties | 4 +- .../cc/sukazyo/cono/morny/ServerMain.java | 296 ----------------- .../morny/bot/command/DirectMsgClear.java | 58 ---- .../cono/morny/bot/command/Encryptor.java | 208 ------------ .../cono/morny/bot/command/EventHack.java | 99 ------ .../morny/bot/command/GetUsernameAndId.java | 85 ----- .../cono/morny/bot/command/Ip186Query.java | 85 ----- .../morny/bot/command/MornyInfoOnHello.java | 44 --- .../morny/bot/command/MornyInformation.java | 301 ------------------ .../cono/morny/bot/command/Nbnhhsh.java | 80 ----- .../cono/morny/bot/command/Testing.java | 36 --- .../sukazyo/cono/morny/bot/command/喵呜.java | 82 ----- .../cono/morny/bot/command/私わね.java | 40 --- .../cono/morny/bot/event/OnCallMe.java | 181 ----------- .../cono/morny/bot/event/OnInlineQueries.java | 47 --- .../cono/morny/bot/event/OnUserRandoms.java | 42 --- .../morny/bot/event/OnUserSlashAction.java | 84 ----- .../cono/morny/bot/query/ITelegramQuery.java | 15 - .../cono/morny/bot/query/MornyQueries.java | 31 -- .../cono/morny/bot/query/MyInformation.java | 35 -- .../sukazyo/cono/morny/bot/query/RawText.java | 31 -- .../morny/bot/query/ShareToolBilibili.java | 80 ----- .../morny/bot/query/ShareToolTwitter.java | 50 --- .../morny/data/ip186/IP186QueryHandler.java | 89 ------ .../morny/data/ip186/IP186QueryResponse.java | 11 - .../cc/sukazyo/cono/morny/Log.java | 0 .../cc/sukazyo/cono/morny/MornyAbout.java | 0 .../cc/sukazyo/cono/morny/MornyAssets.java | 0 .../cc/sukazyo/cono/morny/MornyCoeur.java | 2 +- .../cc/sukazyo/cono/morny/MornyConfig.java | 0 .../cc/sukazyo/cono/morny/MornySystem.java | 0 .../cc/sukazyo/cono/morny/MornyTrusted.java | 0 .../cono/morny/bot/api/EventListener.java | 0 .../morny/bot/api/EventListenerManager.java | 0 .../cono/morny/bot/api/InlineQueryUnit.java | 0 .../sukazyo/cono/morny/bot/api/OnUpdate.java | 0 .../morny/bot/command/ISimpleCommand.java | 0 .../morny/bot/command/ITelegramCommand.java | 0 .../cono/morny/bot/command/MornyCommands.java | 32 +- .../sukazyo/cono/morny/bot/command/Roll.java | 0 .../cono/morny/bot/command/package-info.java | 0 .../cono/morny/bot/event/EventListeners.java | 13 +- .../morny/bot/event/OnActivityRecord.java | 0 .../cono/morny/bot/event/OnCallMsgSend.java | 0 .../morny/bot/event/OnEventHackHandle.java | 0 .../bot/event/OnKuohuanhuanNeedSleep.java | 0 .../bot/event/OnMedicationNotifyApply.java | 0 .../morny/bot/event/OnQuestionMarkReply.java | 0 .../morny/bot/event/OnRandomlyTriggered.java | 0 .../morny/bot/event/OnTelegramCommand.java | 0 .../morny/bot/event/OnUniMeowTrigger.java | 0 .../event/OnUpdateTimestampOffsetLock.java | 0 .../cono/morny/daemon/MedicationTimer.java | 0 .../cono/morny/daemon/MornyDaemons.java | 0 .../cono/morny/daemon/MornyReport.java | 2 +- .../cono/morny/daemon/TrackerDataManager.java | 0 .../cc/sukazyo/cono/morny/data/MornyJrrp.java | 0 .../sukazyo/cono/morny/data/NbnhhshQuery.java | 0 .../cono/morny/data/TelegramImages.java | 0 .../cono/morny/data/TelegramStickers.java | 0 .../cono/morny/internal/BuildConfigField.java | 0 .../cc/sukazyo/cono/morny/util/BiliTool.java | 0 .../cono/morny/util/CommonConvert.java | 0 .../cono/morny/util/CommonEncrypt.java | 0 .../sukazyo/cono/morny/util/CommonFormat.java | 0 .../sukazyo/cono/morny/util/CommonRandom.java | 0 .../cc/sukazyo/cono/morny/util/FileUtils.java | 0 .../sukazyo/cono/morny/util/OkHttpPublic.java | 0 .../cono/morny/util/UniversalCommand.java | 0 .../cono/morny/util/tgapi/ExtraAction.java | 0 .../cono/morny/util/tgapi/InputCommand.java | 0 .../cono/morny/util/tgapi/Standardize.java | 7 + .../tgapi/event/EventRuntimeException.java | 0 .../util/tgapi/formatting/MsgEscape.java | 0 .../util/tgapi/formatting/NamedUtils.java | 0 .../util/tgapi/formatting/TGToString.java | 0 .../tgapi/formatting/TGToStringFromChat.java | 1 + .../formatting/TGToStringFromMessage.java | 0 .../tgapi/formatting/TGToStringFromUser.java | 0 .../formatting/TelegramUserInformation.java | 0 .../cc/sukazyo/cono/morny/ServerMain.scala | 156 +++++++++ .../morny/bot/command/DirectMsgClear.scala | 54 ++++ .../cono/morny/bot/command/Encryptor.scala | 178 +++++++++++ .../cono/morny/bot/command/EventHack.scala | 56 ++++ .../morny/bot/command/GetUsernameAndId.scala | 66 ++++ .../cono/morny/bot/command/IP186Query.scala | 77 +++++ .../morny/bot/command/MornyInfoOnHello.scala | 35 ++ .../morny/bot/command/MornyInformation.scala | 167 ++++++++++ .../cono/morny/bot/command/Nbnhhsh.scala | 85 +++++ .../cono/morny/bot/command/Testing.scala | 30 ++ .../sukazyo/cono/morny/bot/command/喵呜.scala | 67 ++++ .../cono/morny/bot/command/私わね.scala | 26 ++ .../cono/morny/bot/event/OnCallMe.scala | 90 ++++++ .../cono/morny/bot/event/OnInlineQuery.scala | 37 +++ .../cono/morny/bot/event/OnUserRandom.scala | 38 +++ .../morny/bot/event/OnUserSlashAction.scala | 67 ++++ .../cono/morny/bot/query/ITelegramQuery.scala | 11 + .../cono/morny/bot/query/MornyQueries.scala | 27 ++ .../cono/morny/bot/query/MyInformation.scala | 31 ++ .../cono/morny/bot/query/RawText.scala | 27 ++ .../morny/bot/query/ShareToolBilibili.scala | 77 +++++ .../morny/bot/query/ShareToolTwitter.scala | 42 +++ .../morny/data/ip186/IP186QueryHandler.scala | 41 +++ .../cono/morny/data/ip186/IP186Response.scala | 3 + 107 files changed, 1556 insertions(+), 2163 deletions(-) delete mode 100644 src/main/java/cc/sukazyo/cono/morny/ServerMain.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformation.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/event/OnInlineQueries.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/query/MornyQueries.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/query/MyInformation.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/query/RawText.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryResponse.java rename src/main/{java => old}/cc/sukazyo/cono/morny/Log.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/MornyAbout.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/MornyAssets.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/MornyCoeur.java (99%) rename src/main/{java => old}/cc/sukazyo/cono/morny/MornyConfig.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/MornySystem.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/MornyTrusted.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/api/EventListener.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/api/OnUpdate.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/command/MornyCommands.java (96%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/command/Roll.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/command/package-info.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/EventListeners.java (79%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/daemon/MedicationTimer.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/daemon/MornyDaemons.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/daemon/MornyReport.java (99%) rename src/main/{java => old}/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/data/MornyJrrp.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/data/NbnhhshQuery.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/data/TelegramImages.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/data/TelegramStickers.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/internal/BuildConfigField.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/BiliTool.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/CommonConvert.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/CommonEncrypt.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/CommonFormat.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/CommonRandom.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/FileUtils.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/OkHttpPublic.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/UniversalCommand.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java (100%) create mode 100644 src/main/old/cc/sukazyo/cono/morny/util/tgapi/Standardize.java rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java (92%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java (100%) rename src/main/{java => old}/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java (100%) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186Response.scala diff --git a/.dockerignore b/.dockerignore index df3d0f1..e23a3f5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,6 +10,8 @@ #build /build/ /bin/ +.metals/ +.bloop/ .project lcoal.properties diff --git a/.gitignore b/.gitignore index df3d0f1..e23a3f5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ #build /build/ /bin/ +.metals/ +.bloop/ .project lcoal.properties diff --git a/build.gradle b/build.gradle index aff5df9..a15ac59 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'java' + id 'scala' id 'java-library' id 'application' id 'maven-publish' @@ -50,7 +50,9 @@ final long proj_code_time = proj_clean ? grgit.head().dateTime.toInstant().toEpo final JavaVersion proj_java = JavaVersion.VERSION_17 final Charset proj_file_encoding = StandardCharsets.UTF_8 - +final proj_scala_api = 3 +//final proj_scala_lib = proj_scala_api+'.4.0-RC1-bin-20230901-89e8dba-NIGHTLY' +final proj_scala_lib = proj_scala_api+'.3.1-RC7' String publish_local_url = null String publish_remote_url = null String publish_remote_username = null @@ -72,6 +74,7 @@ repositories { dependencies { + api "org.scala-lang:scala3-library_3:${proj_scala_lib}" compileOnlyApi "com.github.spotbugs:spotbugs-annotations:${lib_spotbugs_v}" implementation "cc.sukazyo:messiva:${lib_messiva_v}" @@ -86,8 +89,30 @@ dependencies { } -application { - mainClass = proj_application_main +sourceSets { + main { + scala { srcDirs = ['src/main/scala', 'src/main/old'] } + } +} + +scala { + + compileJava { + + sourceCompatibility '17' + targetCompatibility '17' + + options.encoding = proj_file_encoding.name() + + } + + compileScala { + + options.encoding = proj_file_encoding.name() + scalaCompileOptions.encoding = proj_file_encoding.name() + + } + } test { @@ -97,27 +122,8 @@ test { } } -java { - - sourceCompatibility proj_java - targetCompatibility proj_java - - withSourcesJar() - -} - -tasks.withType(JavaCompile).configureEach { - options.encoding = proj_file_encoding.name() -} - -tasks.withType(Javadoc).configureEach { - options.encoding = proj_file_encoding.name() - options.docEncoding = proj_file_encoding.name() - options.charSet = proj_file_encoding.name() -} - -tasks.test { - useJUnitPlatform() +application { + mainClass = proj_application_main } buildConfig { diff --git a/gradle.properties b/gradle.properties index 2996049..908c69b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,8 +7,8 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s VERSION = 1.0.0-RC4 -USE_DELTA = false -VERSION_DELTA = +USE_DELTA = true +VERSION_DELTA = scalaport1 CODENAME = beiping diff --git a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java b/src/main/java/cc/sukazyo/cono/morny/ServerMain.java deleted file mode 100644 index 208ed1b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/ServerMain.java +++ /dev/null @@ -1,296 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.cono.morny.util.CommonFormat; - -import javax.annotation.Nonnull; - -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.List; - -import static cc.sukazyo.cono.morny.Log.logger; - -/** - * 程序启动入口
      - *
      - * 会处理程序传入的参数和选项等数据,并执行对应的启动方式
      - * - * @since 0.4.0.0 - */ -public class ServerMain { - - public static final long systemStartupTime = System.currentTimeMillis(); - - private static final String THREAD_MORNY_INIT = "morny-init"; - - /** - * 程序入口,也是参数处理器
      - *
      - * 以 {@code -} 开头的参数会被解析为选项
      - *
      - * 支持以下选项 - *

      - * 除去选项之外,第一个参数会被赋值为 bot 的 telegram bot api token, - * 第二个参数会被赋值为 bot 的 username 限定名。其余的参数会被认定为无法理解。
      - * 自 {@code 0.4.2.3},token 和 username 的赋值已被选项组支持
      - * 自 {@code 0.5.0.4},旧的直接通过参数为 bot token & username 赋值的方式已被删除 - * 使用参数所进行取值的 token 和 username 已被转移至 {@code --token} 和 {@code --username} 参数
      - * - * @see MornyCoeur#init - * @since 0.4.0.0 - * @param args 参数组 - */ - public static void main (@Nonnull String[] args) { - - //# - //# 启动参数设置区块 - //# - final MornyConfig.Prototype config = new MornyConfig.Prototype(); - boolean versionEchoMode = false; - boolean welcomeEchoMode = false; - boolean showWelcome = true; - - config.eventOutdatedTimestamp = systemStartupTime; - - List unknownArgs = new ArrayList<>(); - - //# 从命令行参数设置启动参数 - for (int i = 0; i < args.length; i++) { - - if (args[i].startsWith("-")) { - - switch (args[i]) { - case "-d", "--dbg", "--debug" -> { - Log.debug(true); - continue; - } - case "--outdated-block", "-ob" -> { - config.eventIgnoreOutdated = true; - continue; - } - case "--no-hello", "-hf", "--quiet", "-q" -> { - showWelcome = false; - continue; - } - case "--only-hello", "-ho", "-o", "-hi" -> { - welcomeEchoMode = true; - continue; - } - case "--version", "-v" -> { - versionEchoMode = true; - continue; - } - case "--token", "-t" -> { - i++; - config.telegramBotKey = args[i]; - continue; - } - case "--username", "-u" -> { - i++; - config.telegramBotUsername = args[i]; - continue; - } - case "--master", "-mm" -> { - i++; - config.trustedMaster = Long.parseLong(args[i]); - continue; - } - case "--trusted-chat", "-trs" -> { - i++; - config.trustedChat = Long.parseLong(args[i]); - continue; - } - // noinspection SpellCheckingInspection - case "--trusted-reader-dinner", "-trsd" -> { - i++; - config.dinnerTrustedReaders.add(Long.parseLong(args[i])); - continue; - } - case "--dinner-chat", "-chd" -> { - i++; - config.dinnerChatId = Long.parseLong(args[i]); - continue; - } - case "--auto-cmd", "-cmd", "-c" -> { - config.commandLoginRefresh = true; - config.commandLogoutClear = true; - continue; - } - case "--auto-cmd-list", "-ca" -> { - config.commandLoginRefresh = true; - continue; - } - case "--auto-cmd-remove", "-cr" -> { - config.commandLogoutClear = true; - continue; - } - case "--api", "-a" -> { - i++; - config.telegramBotApiServer = args[i]; - continue; - } - case "--api-files", "files-api", "-af" -> { - i++; - config.telegramBotApiServer4File = args[i]; - continue; - } - case "--report-to" -> { - i++; - config.reportToChat = Long.parseLong(args[i]); - continue; - } - case "--medication-notify-chat", "-medc" -> { - i++; - config.medicationNotifyToChat = Long.parseLong(args[i]); - continue; - } - case "--medication-notify-timezone", "-medtz" -> { - i++; - config.medicationTimerUseTimezone = ZoneOffset.ofHours(Integer.parseInt(args[i])); - continue; - } - case "--medication-notify-times", "-medt" -> { - i++; - for (String u : args[i].split(",")) - config.medicationNotifyAt.add(Integer.parseInt(u)); - continue; - } - } - - } - - unknownArgs.add(args[i]); - - } - - //# 从环境变量设置启动参数 - String propToken = null; - String propTokenKey = null; - for (String iKey : MornyConfig.PROP_TOKEN_KEY) { - if (System.getenv(iKey) != null) { - propToken = System.getenv(iKey); - propTokenKey = iKey; - } - } - - - //# - //# 启动信息输出 - //# 启动相关参数的检查和处理 - //# - - if (showWelcome) logger.info(MornyAbout.MORNY_PREVIEW_IMAGE_ASCII); - if (welcomeEchoMode) return; - - unknownArgs.forEach(arg -> logger.warn("Can't understand arg to some meaning :\n " + arg)); - - if (Log.debug()) - logger.warn("Debug log output enabled.\n It may lower your performance, make sure that you are not in production environment."); - - logger.debug("Debug log output enabled."); - - if (versionEchoMode) { - - logger.info(String.format(""" - Morny Cono Version - - version : - Morny %s - %s%s - - md5hash : - %s - - gitstat : - %s - - co.time : - %d - %s [UTC]""", - MornySystem.CODENAME.toUpperCase(), - MornySystem.VERSION_BASE, - MornySystem.isUseDelta() ? "-δ"+MornySystem.VERSION_DELTA : "", - MornySystem.getJarMd5(), - MornySystem.isGitBuild() ? (String.format( - "on commit %s\n %s", - MornySystem.isCleanBuild() ? "- clean-build" : "<δ/non-clean-build>", - BuildConfig.COMMIT - )) : "", - BuildConfig.CODE_TIMESTAMP, - CommonFormat.formatDate(BuildConfig.CODE_TIMESTAMP, 0) - )); - return; - - } - - logger.info(String.format(""" - ServerMain.java Loaded >>> - - version %s - - Morny %s - - <%s> [%d]""", - MornySystem.VERSION_FULL, - MornySystem.CODENAME.toUpperCase(), - MornySystem.getJarMd5(), BuildConfig.CODE_TIMESTAMP - )); - - //# - //# Coeur 参数检查和正式启动主程序 - //# - - if (propToken != null) { - config.telegramBotKey = propToken; - logger.info("Parameter set by EnvVar $"+propTokenKey); - } - - Thread.currentThread().setName(THREAD_MORNY_INIT); - try { - MornyCoeur.init(new MornyConfig(config)); - } catch (MornyConfig.CheckFailure.NullTelegramBotKey ignore) { - logger.info("Parameter required has no value:\n --token."); - } catch (MornyConfig.CheckFailure e) { - logger.error("Unknown failure occurred while starting ServerMain!:"); - e.printStackTrace(System.out); - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.java deleted file mode 100644 index 54b850a..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.java +++ /dev/null @@ -1,58 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.DeleteMessage; -import com.pengrad.telegrambot.request.GetChatMember; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class DirectMsgClear implements ISimpleCommand { - - @Nonnull @Override public String getName () { return "r"; } - - @Nullable @Override public String[] getAliases () { return new String[0]; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - logger.debug("Executing command /r"); - if (event.message().replyToMessage() == null) return; - logger.trace("Message is a reply"); - if (event.message().replyToMessage().from().id() != MornyCoeur.getUserid()) return; - logger.trace("Message is from me"); - if (System.currentTimeMillis()/1000 - event.message().replyToMessage().date() > 48*60*60) return; - logger.trace("Message is not older than 48 hours"); - - final boolean isTrusted = MornyCoeur.trustedInstance().isTrusted(event.message().from().id()); - - if ( - isTrusted || ( - event.message().replyToMessage().replyToMessage() != null && - event.message().replyToMessage().replyToMessage().from().id().equals(event.message().from().id()) - ) - ) { - - MornyCoeur.extra().exec(new DeleteMessage( - event.message().chat().id(), event.message().replyToMessage().messageId() - )); - if (event.message().chat().type() == Chat.Type.Private || ( - MornyCoeur.extra().exec( - new GetChatMember(event.message().chat().id(), event.message().from().id()) - ).chatMember().canDeleteMessages() - )) { - MornyCoeur.extra().exec(new DeleteMessage( - event.message().chat().id(), event.message().messageId() - )); - } - - } else logger.trace("User is not trusted"); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java deleted file mode 100644 index 5ceb213..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Encryptor.java +++ /dev/null @@ -1,208 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.daemon.MornyReport; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.CommonConvert; -import cc.sukazyo.cono.morny.util.CommonEncrypt; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape; -import com.pengrad.telegrambot.model.PhotoSize; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.GetFile; -import com.pengrad.telegrambot.request.SendDocument; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.IOException; -import java.util.Base64; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class Encryptor implements ITelegramCommand { - - @Nonnull @Override public String getName () { return "encrypt"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[algorithm|(l)] [(uppercase)]"; } - @Nonnull @Override public String getDescription () { return "通过指定算法加密回复的内容 (目前只支持文本)"; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - // show a simple help page - // the first paragraph lists available encrypt algorithms, and its aliases. - // with the separator "---", - // the second paragraphs shows the mods available and its aliases. - if (!command.hasArgs() || (command.getArgs()[0].equals("l") && command.getArgs().length==1)) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), """ - base64, b64 - base64url, base64u, b64u - base64decode, base64d, b64d - base64url-decode, base64ud, b64ud - sha1 - sha256 - sha512 - md5 - --- - uppercase, upper, u (sha1/sha256/sha512/md5 only) - """ - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - return; - } - - // param1 is the encrypting algorithm, it MUST EXIST. - // so the mod will be set in param2. - // and for now only support UPPERCASE mod, so it exists in param2, or there should no any params. - boolean modUpperCase = false; - if (command.getArgs().length > 1) { - if (command.getArgs().length < 3 && ( - command.getArgs()[1].equalsIgnoreCase("uppercase") || - command.getArgs()[1].equalsIgnoreCase("u") || - command.getArgs()[1].equalsIgnoreCase("upper") - )) { - modUpperCase = true; - } else { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - return; - } - } - - // for now, only support reply to A TEXT MESSAGE or ONE UNIVERSAL FILE - // if the replied message contains a UNIVERSAL FILE, it will use the file and will not use the text with it - // do not support TELEGRAM INLINE IMAGE/VIDEO/AUDIO yet - // do not support MULTI_FILE yet - // if there's no text message in reply, it will report null as result. - boolean inputText; - byte[] data; - String dataName; - if (event.message().replyToMessage() != null && event.message().replyToMessage().document() != null) { - inputText = false; - try { - data = MornyCoeur.getAccount().getFileContent(MornyCoeur.extra().exec(new GetFile( - event.message().replyToMessage().document().fileId() - )).file()); - } catch (IOException e) { - logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage()); - MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI"); - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_NETWORK_ERR - ).replyToMessageId(event.message().messageId())); - return; - } - dataName = event.message().replyToMessage().document().fileName(); - } else if (event.message().replyToMessage() != null && event.message().replyToMessage().photo() != null) { - inputText = false; - try { - PhotoSize originPhoto = null; - long photoSize = 0; - for (PhotoSize size : event.message().replyToMessage().photo()) if (photoSize < (long)size.width() *size.height()) { - originPhoto = size; - photoSize = (long)size.width() *size.height(); - } // found max size (original) image in available sizes - if (originPhoto==null) throw new IOException("no photo object from api."); - data = MornyCoeur.getAccount().getFileContent(MornyCoeur.extra().exec(new GetFile( - originPhoto.fileId() - )).file()); - } catch (IOException e) { - logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage()); - MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI"); - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_NETWORK_ERR - ).replyToMessageId(event.message().messageId())); - return; - } - dataName = "photo"+CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(String.valueOf(System.currentTimeMillis()))).substring(32-12).toUpperCase()+".png"; - } else if (event.message().replyToMessage() != null && event.message().replyToMessage().text() != null) { - inputText = true; - data = event.message().replyToMessage().text().getBytes(CommonEncrypt.ENCRYPT_STANDARD_CHARSET); - dataName = null; - } else { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "null" - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - return; - } - - boolean echoString = true; - String resultString = null; - byte[] result = null; - String resultName = null; - switch (command.getArgs()[0]) { - case "base64", "b64", "base64url", "base64u", "b64u" -> { - final Base64.Encoder b64tool = command.getArgs()[0].contains("u") ? Base64.getUrlEncoder() : Base64.getEncoder(); - result = b64tool.encode(data); - if (!inputText) { - echoString = false; - resultName = dataName+".b64.txt"; - } else { - resultString = new String(result, CommonEncrypt.ENCRYPT_STANDARD_CHARSET); - } - } - case "base64decode", "base64d", "b64d", "base64url-decode", "base64ud", "b64ud" -> { - final Base64.Decoder b64tool = command.getArgs()[0].contains("u") ? Base64.getUrlDecoder() : Base64.getDecoder(); - try { result = b64tool.decode(data); } - catch (IllegalArgumentException e) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - return; - } - if (!inputText) { - echoString = false; - resultName = CommonEncrypt.base64FilenameLint(dataName); - } else { - resultString = new String(result, CommonEncrypt.ENCRYPT_STANDARD_CHARSET); - } - } - case "md5" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(data)); - case "sha1" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashSha1(data)); - case "sha256" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashSha256(data)); - case "sha512" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashSha512(data)); - default -> { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - return; - } - } - if (modUpperCase) { - // modUpperCase support only algorithm that showed as HEX value. - // it means md5, sha1, sha256, sha512 here. - // other will report wrong param. - switch (command.getArgs()[0]) { - case "md5", "sha1", "sha256", "sha512" -> { - assert resultString != null; - resultString = resultString.toUpperCase(); - } - default -> { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - return; - } - } - } - if (echoString) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "
      " + MsgEscape.escapeHtml(resultString) + "
      " - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - } else { - MornyCoeur.extra().exec(new SendDocument( - event.message().chat().id(), - result - ).fileName(resultName).replyToMessageId(event.message().messageId())); - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java deleted file mode 100644 index dbcfc81..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/EventHack.java +++ /dev/null @@ -1,99 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.MornyTrusted; -import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle; -import cc.sukazyo.cono.morny.data.TelegramStickers; - -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.SendSticker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * {@link OnEventHackHandle} 的命令行前端 - * @since 0.4.2.0 - */ -public class EventHack implements ITelegramCommand { - - @Nonnull @Override public String getName () { return "event_hack"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return "[(user|group|any)]"; } - @Nonnull @Override public String getDescription () { return "输出 bot 下一个获取到的事件序列化数据"; } - - /** - * {@link OnEventHackHandle} 的命令行前端
      - *
      - * 实现了通过命令行进行 EventHack 功能。
      - * 支持三种模式,默认为 {@link OnEventHackHandle.HackType#USER USER}, - * {@link OnEventHackHandle.HackType#ANY ANY} 时,将会通过 {@link MornyTrusted#isTrusted(long)} 检查触发用户的权限 - * - * @param event 命令基础参数,触发的事件对象本身 - * @param command 命令基础参数,解析出的命令对象 - * @since 0.4.2.0 - */ - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - enum Status { - OK, - FORBIDDEN_FOR_ANY - } - Status status; - - String x_mode = ""; - if (command.hasArgs()) { - x_mode = command.getArgs()[0]; - } - switch (x_mode) { - case "any" -> { - if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) { - OnEventHackHandle.registerHack( - event.message().messageId(), - event.message().from().id(), - event.message().chat().id(), - OnEventHackHandle.HackType.ANY - ); - status = Status.OK; - } else { - status = Status.FORBIDDEN_FOR_ANY; - } - } - case "group" -> { - OnEventHackHandle.registerHack( - event.message().messageId(), - event.message().from().id(), - event.message().chat().id(), - OnEventHackHandle.HackType.GROUP - ); - status = Status.OK; - } - default -> { - OnEventHackHandle.registerHack( - event.message().messageId(), - event.message().from().id(), - event.message().chat().id(), - OnEventHackHandle.HackType.USER - ); - status = Status.OK; - } - } - - switch (status) { - case OK -> MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_WAITING - ).replyToMessageId(event.message().messageId()) - ); - case FORBIDDEN_FOR_ANY -> MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_403 - ).replyToMessageId(event.message().messageId()) - ); - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java deleted file mode 100644 index 553b14f..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.java +++ /dev/null @@ -1,85 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.User; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.GetChatMember; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.response.GetChatMemberResponse; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public class GetUsernameAndId implements ITelegramCommand { - - @Nonnull @Override public String getName () { return "user"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return "[userid]"; } - @Nonnull @Override public String getDescription () { return "获取指定或回复的用户相关信息"; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - final String[] args = command.getArgs(); - - // 不支持大于一个参数 - if (args.length > 1) { MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] Too much arguments." - ).replyToMessageId(event.message().messageId())); return; } - - // 发送者自己的 id - long userId = event.message().from().id(); - - // 如果有回复某个人,则使用被回复人的 id - if (event.message().replyToMessage()!= null) { - userId = event.message().replyToMessage().from().id(); - } - // 如果有指定 id,则使用指定的 id - if (args.length > 0) { - try { - userId = Long.parseLong(args[0]); - } catch (NumberFormatException e) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] " + e.getMessage() - ).replyToMessageId(event.message().messageId())); - return; - } - } - - // 重新获取用户对象 - final GetChatMemberResponse response = MornyCoeur.getAccount().execute( - new GetChatMember(event.message().chat().id(), userId) - ); - - if (response.chatMember() == null) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] user not found." - ).replyToMessageId(event.message().messageId())); - return; - } - - // 获取并发送用户信息 - final User user = response.chatMember().user(); - - if (user.id() == 136817688) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "$__channel_identify" - )); - return; - } - - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - TelegramUserInformation.informationOutputHTML(user) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java deleted file mode 100644 index 4d5b4f3..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Ip186Query.java +++ /dev/null @@ -1,85 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.data.ip186.IP186QueryResponse; -import cc.sukazyo.cono.morny.data.ip186.IP186QueryHandler; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - - -/** - * {@value IP186QueryHandler#SITE_URL} 查询的 telegram 命令前端 - * @since 0.4.2.10 - */ -public class Ip186Query { - - public static final String CMD_IP = "ip"; - public static final String CMD_WHOIS = "whois"; - - public static class Ip implements ITelegramCommand { - @Nonnull @Override public String getName () { return CMD_IP; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[ip]"; } - @Nonnull @Override public String getDescription () { return "通过 https://ip.186526.xyz 查询 ip 资料"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { exec(event, command); } - } - - public static class Whois implements ITelegramCommand { - @Nonnull @Override public String getName () { return CMD_WHOIS; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[domain]"; } - @Nonnull @Override public String getDescription () { return "通过 https://ip.186526.xyz 查询域名资料"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { exec(event, command); } - } - - private static void exec (@Nonnull Update event, @Nonnull InputCommand command) { - - String arg = null; - if (!command.hasArgs()) { - if (event.message().replyToMessage() != null) { - arg = event.message().replyToMessage().text(); - } - } else if (command.getArgs().length > 1) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] Too much arguments." - ).replyToMessageId(event.message().messageId())); - return; - } else { - arg = command.getArgs()[0]; - } - if (arg == null) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Unavailable] No ip defined." - ).replyToMessageId(event.message().messageId())); - return; - } - - try { - IP186QueryResponse response = switch (command.getCommand()) { - case CMD_IP -> IP186QueryHandler.queryIp(arg); - case CMD_WHOIS -> IP186QueryHandler.queryWhoisPretty(arg); - default -> throw new IllegalArgumentException("Unknown 186-IP query method " + command.getCommand()); - }; - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - escapeHtml(response.url()) + "\n" + escapeHtml(response.body()) + "" - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - } catch (Exception e) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Exception] in query:\n" + escapeHtml(e.getMessage()) + "" - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.java deleted file mode 100644 index 245bae8..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.java +++ /dev/null @@ -1,44 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendPhoto; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * The implementation of Telegram special command `/start`. - * - * @see MornyInformation related class where some data comes from. - * - * @since 1.0.0-RC4 - */ -public class MornyInfoOnHello implements ISimpleCommand { - - @Nonnull @Override public String getName() { return "start"; } - @Nullable @Override public String[] getAliases() { return new String[0]; } - // @Override public String getParamRule() { return ""; } - // @Override public String getDescription() { return "" } - - @Override - public void execute(@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendPhoto( - event.message().chat().id(), - MornyInformation.getAboutPic() - ).caption(""" - 欢迎使用 Morny Cono来自安妮的侍从小鼠。 - Morny 具有各种各样的功能。 - - ———————————————— - %s - ———————————————— - - (你可以随时通过 /info 重新获得这些信息)""".formatted(MornyInformation.getMornyAboutLinksHTML()) - ).parseMode(ParseMode.HTML)); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformation.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformation.java deleted file mode 100644 index 6a204e0..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformation.java +++ /dev/null @@ -1,301 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.BuildConfig; -import cc.sukazyo.cono.morny.MornyAbout; -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.MornySystem; -import cc.sukazyo.cono.morny.data.TelegramImages; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.tgapi.ExtraAction; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; - -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendPhoto; -import com.pengrad.telegrambot.request.SendSticker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Objects; - -import static cc.sukazyo.cono.morny.util.CommonFormat.formatDate; -import static cc.sukazyo.cono.morny.util.CommonFormat.formatDuration; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class MornyInformation implements ITelegramCommand { - - private static final String SUB_STICKER = "stickers"; - private static final String SUB_RUNTIME = "runtime"; - private static final String SUB_VERSION = "version"; - private static final String SUB_VERSION_2 = "v"; - - @Nonnull @Override public String getName () { return "info"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return "[(version|runtime|stickers[.IDs])]"; } - @Nonnull @Override public String getDescription () { return "输出当前 Morny 的各种信息"; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - if (!command.hasArgs()) { - echoInfo(event.message().chat().id(), event.message().messageId()); - return; - } - - final String action = command.getArgs()[0]; - - if (action.startsWith(SUB_STICKER)) { - echoStickers(command, event); - } else if (action.equals(SUB_RUNTIME)) { - echoRuntime(event); - } else if (action.equals(SUB_VERSION) || action.equals(SUB_VERSION_2)) { - echoVersion(event); - } else { - echo404(event); - } - - } - - /** - * Subcommand /info without params. - * - * @since 1.0.0-RC4 - */ - public void echoInfo (long chatId, int replayToMessage) { - MornyCoeur.extra().exec(new SendPhoto( - chatId, - getAboutPic() - ).caption(""" - Morny Cono - 来自安妮的侍从小鼠。 - ———————————————— - %s""".formatted(getMornyAboutLinksHTML()) - ).parseMode(ParseMode.HTML).replyToMessageId(replayToMessage)); - } - - /** - * subcommand /info stickers - * - * @see #SUB_STICKER - */ - public void echoStickers (@Nonnull InputCommand command, @Nonnull Update event) { - final long echoTo = event.message().chat().id(); - final int replyToMessage = event.message().messageId(); - String id = null; - if (command.getArgs()[0].equals(SUB_STICKER)) { - if (command.getArgs().length == 1) { - id = ""; - } else if (command.getArgs().length == 2) { - id = command.getArgs()[1]; - } - } else if (command.getArgs().length == 1) { - if (command.getArgs()[0].startsWith(SUB_STICKER+".") || command.getArgs()[0].startsWith(SUB_STICKER+"#")) { - id = command.getArgs()[0].substring(SUB_STICKER.length()+1); - } - } - if (id == null) { echo404(event); return; } - echoStickers(id, echoTo, replyToMessage); - } - - /** - * 向 telegram 输出一个或全部 sticker. - * - * @param id - * sticker 在 {@link TelegramStickers} 中的字段名。 - * 使用 {@link ""}(空字符串)(不是{@link null}) 表示输出全部 sticker - * @param chatId 目标 chat id - * @param messageId 要回复的消息 id,依据 {@link TelegramStickers#echoStickerByID(String, ExtraAction, long, int) 上游} - * 逻辑,使用 {@link -1} 表示不回复消息。 - * - * @see TelegramStickers#echoStickerByID(String, ExtraAction, long, int) - * @see TelegramStickers#echoAllStickers(ExtraAction, long, int) - */ - public static void echoStickers (@Nonnull String id, long chatId, int messageId) { - if (id.isEmpty()) TelegramStickers.echoAllStickers(MornyCoeur.extra(), chatId, messageId); - else TelegramStickers.echoStickerByID(id, MornyCoeur.extra(), chatId, messageId); - } - - /** - * Subcommand /info runtime. - * - * @see #SUB_RUNTIME - * - * @since 1.0.0-alpha4 - */ - public static void echoRuntime (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format(""" - system: - - %s - - %s (%s) %s - java runtime: - - %s - - %s - vm memory: - - %d / %d MB - - %d cores - coeur version: - - %s - - %s - - %s [UTC] - - [%d] - continuous: - - %s - - [%d] - - %s [UTC] - - [%d]""", - // system - escapeHtml(getRuntimeHostName()==null ? "" : getRuntimeHostName()), - escapeHtml(System.getProperty("os.name")), - escapeHtml(System.getProperty("os.arch")), - escapeHtml(System.getProperty("os.version")), - // java - escapeHtml(System.getProperty("java.vm.vendor")+"."+System.getProperty("java.vm.name")), - escapeHtml(System.getProperty("java.vm.version")), - // memory - Runtime.getRuntime().totalMemory() / 1024 / 1024, - Runtime.getRuntime().maxMemory() / 1024 / 1024, - Runtime.getRuntime().availableProcessors(), - // version - getVersionAllFullTagHtml(), - escapeHtml(MornySystem.getJarMd5()), - escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)), - BuildConfig.CODE_TIMESTAMP, - // continuous - escapeHtml(formatDuration(System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp)), - System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp, - escapeHtml(formatDate(MornyCoeur.coeurStartTimestamp, 0)), - MornyCoeur.coeurStartTimestamp - ) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - } - - /** - * Subcommand /info version or /info v. - * - * @see #SUB_VERSION - * @see #SUB_VERSION_2 - * - * @since 1.0.0-alpha4 - */ - public static void echoVersion (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format( - """ - version: - - Morny %s - - %s%s%s - core md5_hash: - - %s - coding timestamp: - - %d - - %s [UTC]""", - escapeHtml(MornySystem.CODENAME.toUpperCase()), - escapeHtml(MornySystem.VERSION_BASE), - MornySystem.isUseDelta() ? String.format("-δ%s", escapeHtml(Objects.requireNonNull(MornySystem.VERSION_DELTA))) : "", - MornySystem.isGitBuild() ? "\n- git "+getVersionGitTagHtml() : "", - escapeHtml(MornySystem.getJarMd5()), - BuildConfig.CODE_TIMESTAMP, - escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)) - ) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - } - - /** - * 取得 {@link MornySystem} 的 git commit 相关版本信息的 HTML 格式化标签. - * - * @return 格式类似于 {@code 28e8c82a.δ} 的以 HTML 方式格式化的版本号组件。 - * 其中 {@code .δ} 对应着 {@link MornySystem#isCleanBuild}; - * commit tag 字段如果支援 {@link MornySystem#currentCodePath} 则会以链接形式解析,否则则为 code 格式 - * 为了对 telegram api html 格式兼容所以不支援嵌套链接与code标签。 - * 如果 {@link MornySystem#isGitBuild} 为 {@link false},则方法会返回 {@link ""} - * - * @since 1.0.0-beta2 - */ - @Nonnull - public static String getVersionGitTagHtml () { - if (!MornySystem.isGitBuild()) return ""; - final StringBuilder g = new StringBuilder(); - final String cp = MornySystem.currentCodePath(); - if (cp == null) g.append(String.format("%s", BuildConfig.COMMIT.substring(0, 8))); - else g.append(String.format("
      %s", MornySystem.currentCodePath(), BuildConfig.COMMIT.substring(0, 8))); - if (!MornySystem.isCleanBuild()) g.append(".δ"); - return g.toString(); - } - - /** - * 取得完整 Morny 版本的 HTML 格式化标签. - *

      - * 相比于 {@link MornySystem#VERSION_FULL},这个版本号还包含了 {@link MornySystem#CODENAME 版本 codename}。 - * 各个部分也被以 HTML 的格式进行了格式化以可以更好的在富文本中插入使用. - * @return 基于 HTML 标签进行了格式化了的类似于 - * {@link MornySystem#VERSION_BASE 5.38.2-alpha1}{@link MornySystem#isUseDelta() -δ}{@link MornySystem#VERSION_DELTA tt}{@link MornySystem#isGitBuild() +git.}{@link #getVersionGitTagHtml() 28e8c82a.δ}*{@link MornySystem#CODENAME TOKYO} - * 的版本号。 - * @since 1.0.0-beta2 - */ - @Nonnull - public static String getVersionAllFullTagHtml () { - final StringBuilder v = new StringBuilder(); - v.append("").append(MornySystem.VERSION_BASE).append(""); - if (MornySystem.isUseDelta()) v.append("-δ").append(MornySystem.VERSION_DELTA).append(""); - if (MornySystem.isGitBuild()) v.append("+git.").append(getVersionGitTagHtml()); - v.append("*").append(MornySystem.CODENAME.toUpperCase()).append(""); - return v.toString(); - } - - /** - * 获取 coeur 运行时的宿主机的主机名 - * @return coeur 宿主机主机名,或者 {@link null} 表示获取失败 - */ - @Nullable - public static String getRuntimeHostName () { - try { - return InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - return null; - } - } - - /** - * Get the about-pic (intro picture or featured image) of Morny. - * - * @return the Telegram file binary data of the about-pic. - * @throws IllegalStateException {@link TelegramImages.AssetsFileImage#get() get() image data} may - * throws {@link IllegalStateException} while read error. - */ - @Nonnull - public static byte[] getAboutPic () { - return TelegramImages.IMG_ABOUT.get(); - } - - private static void echo404 (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - } - - /** - * The formatted about links of Morny Cono and Morny Coeur. - *

      - * With the Telegram HTML formatting, used in /info and /start. - * Provided the end user the links that can find resources about Morny. - */ - @Nonnull - public static String getMornyAboutLinksHTML () { - return """ - source code | backup - 反馈 / issue tracker - 使用说明书 / user guide & docs""".formatted( - MornyAbout.MORNY_SOURCECODE_LINK, MornyAbout.MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK, - MornyAbout.MORNY_ISSUE_TRACKER_LINK, - MornyAbout.MORNY_USER_GUIDE_LINK - ); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java deleted file mode 100644 index 20adb23..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.java +++ /dev/null @@ -1,80 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.data.NbnhhshQuery; -import com.pengrad.telegrambot.request.SendSticker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class Nbnhhsh implements ITelegramCommand { - - @Nonnull @Override public String getName () { return "nbnhhsh"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return "[text]"; } - @Nonnull @Override public String getDescription () { return "检索文本内 nbnhhsh 词条"; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - class TagNoContent extends Exception {} - try { - - String queryTarget; - if (event.message().replyToMessage() != null && event.message().replyToMessage().text() != null) - queryTarget = event.message().replyToMessage().text(); - else if (command.hasArgs()) - queryTarget = stringsConnecting(command.getArgs(), " ", 0, command.getArgs().length-1); - else { - throw new TagNoContent(); - } - - NbnhhshQuery.GuessResult response = NbnhhshQuery.sendGuess(queryTarget); - - StringBuilder message = new StringBuilder("## Result of nbnhhsh query :"); - - for (NbnhhshQuery.Word word : response.words) { - if (word.trans != null && word.trans.length == 0) word.trans = null; - if (word.inputting != null && word.inputting.length == 0) word.inputting = null; - if (word.trans == null && word.inputting == null) continue; - message.append("\n\n[[ ").append(escapeHtml(word.name)).append(" ]]"); - if (word.trans != null) for (String trans : word.trans) { - message.append("\n* ").append(escapeHtml(trans)).append(""); - } - if (word.inputting != null) { - if (word.trans != null) message.append("\n"); - message.append(" maybe:"); - for (String trans : word.inputting) { - message.append("\n` ").append(escapeHtml(trans)).append(""); - } - } - } - - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - message.toString() - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - - } catch (TagNoContent tag) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId())); - } catch (Exception e) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "[Exception] in query:\n" + escapeHtml(e.getMessage()) + "" - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java deleted file mode 100644 index 8039ce7..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/Testing.java +++ /dev/null @@ -1,36 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public class Testing implements ISimpleCommand { - - @Nonnull - @Override - public String getName () { - return "test"; - } - - @Nullable - @Override - public String[] getAliases () { - return null; - } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - "Just a TEST command." - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java deleted file mode 100644 index 692ce2a..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/喵呜.java +++ /dev/null @@ -1,82 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * WARNING that {@link cc.sukazyo.cono.morny.bot.event.OnTelegramCommand} - * 并不能够处理非 english word 字符之外的命令. - *

      - * 出于这个限制,以下几个命令目前都无法使用 - * @see 抱抱 - * @see 揉揉 - * @see 蹭蹭 - * @see 贴贴 - */ -@SuppressWarnings("NonAsciiCharacters") -public class 喵呜 { - - public static class 抱抱 implements ISimpleCommand { - @Nonnull @Override public String getName () { return "抱抱"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - replyingSet(event, "抱抱", "抱抱"); - } - } - - public static class 揉揉 implements ISimpleCommand { - @Nonnull @Override public String getName () { return "揉揉"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - replyingSet(event, "蹭蹭", "摸摸"); - } - } - - public static class 蹭蹭 implements ISimpleCommand { - @Nonnull @Override public String getName () { return "蹭蹭"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - replyingSet(event, "揉揉", "蹭蹭"); - } - } - - public static class 贴贴 implements ISimpleCommand { - @Nonnull @Override public String getName () { return "贴贴"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - replyingSet(event, "贴贴", "贴贴"); - } - } - - private static void replyingSet (@Nonnull Update event, @Nonnull String whileRec, @Nonnull String whileNew) { - final boolean isNew = event.message().replyToMessage() == null; - final Message target = isNew ? event.message() : event.message().replyToMessage(); - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - isNew ? whileNew : whileRec - ).replyToMessageId(target.messageId()).parseMode(ParseMode.HTML)); - } - - public static class Progynova implements ITelegramCommand { - @Nonnull @Override public String getName () { return "install"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "抽取一个神秘盒子"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_PROGYNOVA - ).replyToMessageId(event.message().messageId())); - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java deleted file mode 100644 index 6006c0d..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/私わね.java +++ /dev/null @@ -1,40 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue; - -@SuppressWarnings("NonAsciiCharacters") -public class 私わね implements ISimpleCommand { - - @Nonnull - @Override public String getName () { return "me"; } - - @Nullable - @Override public String[] getAliases () { return null; } - - @Override - public void execute (@Nonnull InputCommand command, @Nonnull Update event) { - if (probabilityTrue(521)) { - // 可以接入未来的心情系统(如果有的话) -// final String text = switch (ThreadLocalRandom.current().nextInt(11)) { -// case 0,7,8,9,10 -> "才不是"; -// case 1,2,3,6 -> "才不是!"; -// case 4,5 -> "才不是.."; -// default -> throw new IllegalStateException("Unexpected random value in 私わね command."); -// }; - final String text = "/打假"; - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - text - ).replyToMessageId(event.message().messageId())); - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java deleted file mode 100644 index 89ed0fc..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMe.java +++ /dev/null @@ -1,181 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.CommonFormat; -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape; -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.ForwardMessage; -import com.pengrad.telegrambot.request.GetChat; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; -import com.pengrad.telegrambot.response.SendResponse; - -import javax.annotation.Nonnull; - -/** - * 通过 bot 呼叫主人的事件监听管理类 - * @since 0.4.2.1 - */ -public class OnCallMe extends EventListener { - - /** - * 主人的 telegram user id,同时被用于 chat id
      - * 跟随 {@link cc.sukazyo.cono.morny.MornyConfig#trustedMaster} 的值 - * @since 0.4.2.1 - */ - private static final long ME = MornyCoeur.config().trustedMaster; - - /** - * 监听私聊 bot 的消息进行呼叫关键字匹配。 - * 如果成功,将会执行呼叫函数,并向呼叫者回显{@link TelegramStickers#ID_WAITING "已呼叫"贴纸} - * - * @param update 事件基础参数,消息事件所属的 tgapi:update 对象 - * @return 事件基础返回值,是否已完成处理事件:
      - * 如果匹配到呼叫,则返回{@code true},反之返回{@code false} - */ - @Override - public boolean onMessage (@Nonnull Update update) { - if (update.message().text() == null) - return false; - if (update.message().chat().type() != Chat.Type.Private) - return false; - switch (update.message().text().toLowerCase()) { - case "steam", "sbeam", "sdeam" -> - requestSteamJoin(update); - case "hana paresu", "花宫", "内群" -> - requestHanaParesuJoin(update); - case "dinner", "lunch", "breakfast", "meal", "eating", "安妮今天吃什么" -> - requestLastDinner(update); - default -> { - if (update.message().text().startsWith("cc::")) { - requestCustomCall(update); - break; - } - return false; - } - } - MornyCoeur.extra().exec(new SendSticker( - update.message().chat().id(), - TelegramStickers.ID_SENT - ).replyToMessageId(update.message().messageId()) - ); - return true; - } - - /** - * 执行 steam library 呼叫
      - * 将会向 {@link #ME} 发送 - * - * @param event 执行呼叫的tg事件 - */ - private static void requestSteamJoin (Update event) { - MornyCoeur.extra().exec(new SendMessage( - ME, String.format( - """ - request STEAM LIBRARY - from %s""", - TGToString.as(event.message().from()).fullnameRefHtml() - ) - ).parseMode(ParseMode.HTML)); - } - - /** - * 执行花宫呼叫
      - * 将会向 {@link #ME} 发送 - * - * @param event 执行呼叫的tg事件 - */ - private static void requestHanaParesuJoin (Update event) { - MornyCoeur.extra().exec(new SendMessage( - ME, String.format( - """ - request Hana Paresu - from %s""", - TGToString.as(event.message().from()).fullnameRefHtml() - ) - ).parseMode(ParseMode.HTML)); - } - - /** - * 对访问最近一次的饭局的请求进行回复
      - * - * @param event 执行呼叫的tg事件 - */ - private static void requestLastDinner (Update event) { - boolean isAllowed = false; - Message lastDinnerData = null; - if (MornyCoeur.trustedInstance().isTrustedForDinnerRead(event.message().from().id())) { - lastDinnerData = MornyCoeur.extra().exec(new GetChat(MornyCoeur.config().dinnerChatId)).chat().pinnedMessage(); - SendResponse sendResp = MornyCoeur.extra().exec(new ForwardMessage( - event.message().from().id(), - lastDinnerData.forwardFromChat().id(), - lastDinnerData.forwardFromMessageId() - )); - MornyCoeur.extra().exec(new SendMessage( - event.message().from().id(), - String.format("on %s [UTC+8]\n- %s before", - MsgEscape.escapeHtml( - CommonFormat.formatDate((long)lastDinnerData.forwardDate()*1000, 8) - ), MsgEscape.escapeHtml( - CommonFormat.formatDuration(System.currentTimeMillis()-(long)lastDinnerData.forwardDate()*1000) - ) - ) - ).replyToMessageId(sendResp.message().messageId()).parseMode(ParseMode.HTML)); - isAllowed = true; - } else { - MornyCoeur.extra().exec(new SendSticker( - event.message().from().id(), - TelegramStickers.ID_403 - ).replyToMessageId(event.message().messageId())); - } - MornyCoeur.extra().exec(new SendMessage( - ME, String.format( - """ - request Last Annie Dinner - from %s - %s""", - TGToString.as(event.message().from()).fullnameRefHtml(), - isAllowed ? "Allowed and returned " + String.format( - "https://t.me/c/%d/%d", Math.abs(lastDinnerData.forwardFromChat().id()+1000000000000L), lastDinnerData.forwardFromMessageId() - ) : "Forbidden by perm check." - ) - ).parseMode(ParseMode.HTML)); - } - - /** - * 执行自定义呼叫
      - * 将会向 {@link #ME} 发送一个 request 数据消息和转发的原始请求消息
      - *
      - * known issue

        - *
      • 无法处理与转发带有媒体的消息
      • - *
      - *
      - * 现在你可以通过这个 bot 来呼叫主人(sukazyo)任何事情了 —— - * 但是直接私聊sukazyo不好吗 - * - * @param event 执行呼叫的tg事件 - * @since 0.4.2.2 - */ - private static void requestCustomCall (Update event) { - MornyCoeur.extra().exec(new SendMessage( - ME, String.format( - """ - request [???] - from %s""", - TGToString.as(event.message().from()).fullnameRefHtml() - ) - ).parseMode(ParseMode.HTML)); - MornyCoeur.extra().exec(new ForwardMessage( - ME, - event.message().chat().id(), - event.message().messageId() - )); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnInlineQueries.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnInlineQueries.java deleted file mode 100644 index ed06f0b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnInlineQueries.java +++ /dev/null @@ -1,47 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResult; -import com.pengrad.telegrambot.request.AnswerInlineQuery; - -import javax.annotation.Nonnull; -import java.util.List; - -/** - * telegram inlineQuery 功能的处理类, - * 也是一个 InlineQueryManager(还没做) - * - * @since 0.4.1.3 - */ -public class OnInlineQueries extends EventListener { - - /** - * @since 0.4.1.3 - */ - @Override - public boolean onInlineQuery (@Nonnull Update update) { - - List> results = MornyCoeur.queryManager().query(update); - - int cacheTime = Integer.MAX_VALUE; - boolean isPersonal = InlineQueryUnit.DEFAULT_INLINE_PERSONAL_RESP; - InlineQueryResult[] inlineQueryResults = new InlineQueryResult[results.size()]; - for (int i = 0; i < results.size(); i++) { - inlineQueryResults[i] = results.get(i).result; - if (cacheTime > results.get(i).cacheTime()) cacheTime = results.get(i).cacheTime(); - if (results.get(i).isPersonal()) isPersonal = true; - } - - if (results.size() == 0) return false; - - MornyCoeur.extra().exec(new AnswerInlineQuery( - update.inlineQuery().id(), inlineQueryResults - ).cacheTime(cacheTime).isPersonal(isPersonal)); - return true; - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java deleted file mode 100644 index b1deec4..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserRandoms.java +++ /dev/null @@ -1,42 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static cc.sukazyo.cono.morny.util.CommonRandom.iif; - -public class OnUserRandoms extends EventListener { - - private static final Pattern USER_OR_QUERY = Pattern.compile("(.+)(?:还是|or)(.+)"); - private static final Pattern USER_IF_QUERY = Pattern.compile("(.+)[吗?|?]+$"); - - @Override - public boolean onMessage (@Nonnull Update update) { - - if (update.message().text() == null) return false; - if (!update.message().text().startsWith("/")) return false; - - final String query = update.message().text().substring(1); - String result = null; - Matcher matcher; - if ((matcher = USER_OR_QUERY.matcher(query)).find()) { - result = iif() ? matcher.group(1) : matcher.group(2); - } else if ((matcher = USER_IF_QUERY.matcher(query)).matches()) { - result = (iif()?"":"不") + matcher.group(1); - } - - if (result == null) return false; - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), result - ).replyToMessageId(update.message().messageId())); - return true; - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.java deleted file mode 100644 index cf1910b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.java +++ /dev/null @@ -1,84 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.util.UniversalCommand; -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; - -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; - -import static cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class OnUserSlashAction extends EventListener { - - @Override - public boolean onMessage (@Nonnull Update event) { - final String text = event.message().text(); - if (text == null) return false; - - if (text.startsWith("/")) - { - - /// Due to @Lapis_Apple, we stopped slash action function at .DP7 groups. - /// It may be enabled after some updates when the function will not be conflicted to other bots. - // if (event.message().chat().id() == ) return false; -//{ if (event.message().chat().title() != null && event.message().chat().title().contains(".DP7")) { -// logger.info(String.format(""" -// Chat slash action ignored due to the following keyword. -// - %s -// - ".DP7\"""", -// TGToString.as(event.message().chat()).toStringFullNameId() -// )); -// return false; -// } - - final String[] action = UniversalCommand.format(text); - action[0] = action[0].substring(1); - - if (action[0].matches("^\\w+(@\\w+)?$")) { - return false; // 忽略掉 Telegram 命令格式的输入 - } else if (action[0].contains("/")) { - return false; // 忽略掉疑似目录格式的输入 - } - - final boolean isHardParse = "".equals(action[0]); - /* 忽略空数据 */ if (isHardParse && action.length < 2) { return false; } - final String verb = isHardParse ? action[1] : action[0]; - final boolean hasObject = action.length != (isHardParse?2:1); - final String object = - hasObject ? - stringsConnecting(action, " ", isHardParse?2:1, action.length-1) : - ""; - final Message origin = event.message(); - final Message target = (event.message().replyToMessage() == null ? ( - origin - ): ( - event.message().replyToMessage() - )); - - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format( - "%s %s%s %s %s!", - TGToString.as(origin).getSenderFirstNameRefHtml(), - escapeHtml(verb), escapeHtml((hasObject?"":"了")), - origin==target ? - "自己" : - TGToString.as(target).getSenderFirstNameRefHtml(), - escapeHtml(hasObject ? object+" " : "") - ) - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); - - return true; - - } - return false; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.java deleted file mode 100644 index c79ab37..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.java +++ /dev/null @@ -1,15 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import javax.annotation.Nullable; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; - -import java.util.List; - -public interface ITelegramQuery { - - @Nullable - List> query (Update event); - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/MornyQueries.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/MornyQueries.java deleted file mode 100644 index b0dc135..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/MornyQueries.java +++ /dev/null @@ -1,31 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.List; - -public class MornyQueries { - - private final List queryInstances = new ArrayList<>(); - - public MornyQueries () { - queryInstances.add(new RawText()); - queryInstances.add(new MyInformation()); - queryInstances.add(new ShareToolTwitter()); - queryInstances.add(new ShareToolBilibili()); - } - - @Nonnull - public List> query (@Nonnull Update event) { - final List> results = new ArrayList<>(); - for (ITelegramQuery instance : queryInstances) { - final List> r = instance.query(event); - if (r!=null) results.addAll(r); - } - return results; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/MyInformation.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/MyInformation.java deleted file mode 100644 index 7921174..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/MyInformation.java +++ /dev/null @@ -1,35 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import javax.annotation.Nullable; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResultArticle; -import com.pengrad.telegrambot.model.request.InputTextMessageContent; -import com.pengrad.telegrambot.model.request.ParseMode; - -import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation; - -import java.util.Collections; -import java.util.List; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; - -public class MyInformation implements ITelegramQuery { - - public static final String ID_PREFIX = "[morny/info/me]"; - public static final String TITLE = "My Account Information"; - - @Override - @Nullable - public List> query(Update event) { - if (!(event.inlineQuery().query() == null || "".equals(event.inlineQuery().query()))) return null; - return Collections.singletonList(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX), TITLE, - new InputTextMessageContent( - TelegramUserInformation.informationOutputHTML(event.inlineQuery().from()) - ).parseMode(ParseMode.HTML) - )).isPersonal(true).cacheTime(10)); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/RawText.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/RawText.java deleted file mode 100644 index a773f6b..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/RawText.java +++ /dev/null @@ -1,31 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; - -import javax.annotation.Nullable; - -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResultArticle; -import com.pengrad.telegrambot.model.request.InputTextMessageContent; - -import java.util.Collections; -import java.util.List; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; - -public class RawText implements ITelegramQuery { - - public static final String ID_PREFIX = "[morny/r/text]"; - public static final String TITLE = "Raw Text"; - - @Override - @Nullable - public List> query (Update event) { - if (event.inlineQuery().query() == null || "".equals(event.inlineQuery().query())) return null; - return Collections.singletonList(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX, event.inlineQuery().query()), TITLE, - new InputTextMessageContent(event.inlineQuery().query()) - ))); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java deleted file mode 100644 index da6c2fa..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.java +++ /dev/null @@ -1,80 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import cc.sukazyo.cono.morny.util.BiliTool; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResultArticle; -import com.pengrad.telegrambot.model.request.InputTextMessageContent; -import com.pengrad.telegrambot.model.request.ParseMode; - -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static cc.sukazyo.cono.morny.Log.logger; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; - -public class ShareToolBilibili implements ITelegramQuery { - - public static final String TITLE_BILI_AV = "[bilibili] Share video / av"; - public static final String TITLE_BILI_BV = "[bilibili] Share video / BV"; - public static final String ID_PREFIX_BILI_AV = "[morny/share/bili/av]"; - public static final String ID_PREFIX_BILI_BV = "[morny/share/bili/bv]"; - public static final Pattern REGEX_BILI_VIDEO = Pattern.compile("^(?:(?:https?://)?(?:www\\.)?bilibili\\.com(?:/s)?/video/((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))/?(\\?(?:p=(\\d+))?.*)?|(?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$"); - - private static final String SHARE_FORMAT_HTML = "%s"; - - @Nullable - @Override - public List> query (Update event) { - if (event.inlineQuery().query() == null) return null; - final Matcher regex = REGEX_BILI_VIDEO.matcher(event.inlineQuery().query()); - if (regex.matches()) { - - logger.debug(String.format( - "====== Share Tool Bilibili Catch ok\n1: %s\n2: %s\n3: %s\n4: %s\n5: %s\n6: %s\n7: %s", - regex.group(1), regex.group(2), regex.group(3), regex.group(4), - regex.group(5), regex.group(6), regex.group(7) - )); - - // get video id from input, also get video part id - String av = regex.group(2)==null ? regex.group(6)==null ? null : regex.group(6) : regex.group(2); - String bv = regex.group(3)==null ? regex.group(7)==null ? null : regex.group(7) : regex.group(3); - logger.trace(String.format("catch id av[%s] bv[%s]", av, bv)); - final int part = regex.group(5)==null ? -1 : Integer.parseInt(regex.group(5)); - logger.trace(String.format("catch part [%s]", part)); - if (av == null) { - assert bv != null; - av = String.valueOf(BiliTool.toAv(bv)); - logger.trace(String.format("converted bv[%s] to av[%s]", bv, av)); - } else { - bv = BiliTool.toBv(Long.parseLong(av)); - logger.trace(String.format("converted av[%s] to bv[%s]", av, bv)); - } - // build standard share links - final String linkPartParam = part==-1 ? "" : "?p="+part; - final String linkAv = "https://www.bilibili.com/video/av"+av + linkPartParam; - final String linkBv = "https://www.bilibili.com/video/BV"+bv + linkPartParam; - final String idAv = "av"+av; - final String idBv = "BV"+bv; - logger.trace("built all data."); - - // build share message element - List> result = new ArrayList<>(); - result.add(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av, - new InputTextMessageContent(String.format(SHARE_FORMAT_HTML, linkAv, idAv)).parseMode(ParseMode.HTML) - ))); - result.add(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX_BILI_BV+bv), TITLE_BILI_BV+bv, - new InputTextMessageContent(String.format(SHARE_FORMAT_HTML, linkBv, idBv)).parseMode(ParseMode.HTML) - ))); - return result; - - } - return null; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.java b/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.java deleted file mode 100644 index 9c50d6f..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.java +++ /dev/null @@ -1,50 +0,0 @@ -package cc.sukazyo.cono.morny.bot.query; - -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.InlineQueryResultArticle; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds; - -public class ShareToolTwitter implements ITelegramQuery { - - public static final String TITLE_VX = "[tweet] Share as VxTwitter"; - public static final String TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)"; - public static final String ID_PREFIX_VX = "[morny/share/twitter/vxtwi]"; - public static final String ID_PREFIX_VX_COMBINED = "[morny/share/twitter/vxtwi_combine]"; - - public static final Pattern REGEX_TWEET_LINK = Pattern.compile( - "^(?:https?://)?((?:(?:c\\.)?vx|fx|www\\.)?twitter\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(\\?[\\w&=-]+)?$"); - - @Nullable - @Override - public List> query (@Nonnull Update event) { - if (event.inlineQuery().query() == null) return null; - final Matcher regex = REGEX_TWEET_LINK.matcher(event.inlineQuery().query()); - if (regex.matches()) { - - List> result = new ArrayList<>(); - - result.add(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX_VX+event.inlineQuery().query()), TITLE_VX, - String.format("https://vxtwitter.com/%s", regex.group(2)) - ))); - result.add(new InlineQueryUnit<>(new InlineQueryResultArticle( - inlineIds(ID_PREFIX_VX_COMBINED+event.inlineQuery().query()), TITLE_VX_COMBINED, - String.format("https://c.vxtwitter.com/%s", regex.group(2)) - ))); - - return result; - - } - return null; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.java b/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.java deleted file mode 100644 index 0f60d81..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.java +++ /dev/null @@ -1,89 +0,0 @@ -package cc.sukazyo.cono.morny.data.ip186; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -import javax.annotation.Nonnull; -import java.io.IOException; - -/** - * 通过 {@value #SITE_URL} 进行 {@link #queryIp ip}/{@link #queryWhois whois} 数据查询的工具类 - * - * @since 0.4.2.10 - */ -public class IP186QueryHandler { - - /** - * 请求所使用的 HTTP API 站点链接 - * @since 0.4.2.10 - */ - public static final String SITE_URL = "https://ip.186526.xyz/"; - - /** - * 进行 {@link #queryIp ip 查询}时所使用的 API 参数.
      - * 目的使 API 直接返回原始数据 - */ - private static final String QUERY_IP_PARAM = "type=json&format=true"; - - /** - * 进行 {@link #queryWhois whois 查询}时所使用的 API 参数.
      - * 目的使 API 直接返回原始数据 - */ - private static final String QUERY_WHOIS_PARAM = "type=plain"; - - /** 请求时使用的 OkHttp 请求工具实例 */ - private static final OkHttpClient httpClient = new OkHttpClient(); - - /** - * 通过 {@value #SITE_URL} 获取 ip 信息. - * @see #QUERY_IP_PARAM 发送请求时所使用的 API 参数 - * @param ip 需要进行查询的 ip - * @return 查询结果。data 根据 {@value #SITE_URL} 的规则以 json 序列化 - * @throws IOException 任何请求或解析错误 - */ - @Nonnull - public static IP186QueryResponse queryIp (String ip) throws IOException { - final String requestUrl = SITE_URL + ip; - return commonQuery(requestUrl, QUERY_IP_PARAM); - } - - /** - * 通过 {@value #SITE_URL} 获取域名信息. - * @see #QUERY_WHOIS_PARAM 发送请求时所使用的 API 参数 - * @param domain 需要进行查询的域名 - * @return 查询结果。data 根据 {@value #SITE_URL} 的规则以 plain 序列化 - * @throws IOException 任何请求或解析错误 - */ - @Nonnull - public static IP186QueryResponse queryWhois (String domain) throws IOException { - final String requestUrl = SITE_URL + "whois/" + domain; - return commonQuery(requestUrl, QUERY_WHOIS_PARAM); - } - - /** - * 将 {@link #queryWhois(String)} 的结果进行裁剪. - *
      - * 将会删除返回内容中 {@code >>> XXX <<<} 行以后的注释串, - * 以达到只保留重要信息的目的。 - * - * @see #queryWhois(String) - */ - @Nonnull - public static IP186QueryResponse queryWhoisPretty (String domain) throws IOException { - final IP186QueryResponse raw = queryWhois(domain); - return new IP186QueryResponse(raw.url(), raw.body().substring(0, raw.body().indexOf("<<<")+3)); - } - - @Nonnull - private static IP186QueryResponse commonQuery (String requestUrl, String queryIpParam) throws IOException { - Request request = new Request.Builder().url(requestUrl + "?" + queryIpParam).build(); - try (Response response = httpClient.newCall(request).execute()) { - final ResponseBody body = response.body(); - if (body == null) throw new IOException("Null body."); - return new IP186QueryResponse(requestUrl, body.string()); - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryResponse.java b/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryResponse.java deleted file mode 100644 index f30fb3d..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/data/ip186/IP186QueryResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package cc.sukazyo.cono.morny.data.ip186; - -/** - * {@link IP186QueryHandler} 的请求结果数据的通用封装类. - * - * @since 0.4.2.10 - * @param url 请求数据的人类可读的来源链接,并非api链接 - * @param body API 传回的数据内容 - */ -public record IP186QueryResponse(String url, String body) { -} diff --git a/src/main/java/cc/sukazyo/cono/morny/Log.java b/src/main/old/cc/sukazyo/cono/morny/Log.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/Log.java rename to src/main/old/cc/sukazyo/cono/morny/Log.java diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyAbout.java b/src/main/old/cc/sukazyo/cono/morny/MornyAbout.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/MornyAbout.java rename to src/main/old/cc/sukazyo/cono/morny/MornyAbout.java diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyAssets.java b/src/main/old/cc/sukazyo/cono/morny/MornyAssets.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/MornyAssets.java rename to src/main/old/cc/sukazyo/cono/morny/MornyAssets.java diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java b/src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java similarity index 99% rename from src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java rename to src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java index d891a84..517332d 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornyCoeur.java +++ b/src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java @@ -58,7 +58,7 @@ public class MornyCoeur { * morny 主程序启动时间
      * 用于统计数据 */ - public static final long coeurStartTimestamp = ServerMain.systemStartupTime; + public static final long coeurStartTimestamp = ServerMain.systemStartupTime(); private Object whileExitReason = null; diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/old/cc/sukazyo/cono/morny/MornyConfig.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/MornyConfig.java rename to src/main/old/cc/sukazyo/cono/morny/MornyConfig.java diff --git a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java b/src/main/old/cc/sukazyo/cono/morny/MornySystem.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/MornySystem.java rename to src/main/old/cc/sukazyo/cono/morny/MornySystem.java diff --git a/src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java b/src/main/old/cc/sukazyo/cono/morny/MornyTrusted.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/MornyTrusted.java rename to src/main/old/cc/sukazyo/cono/morny/MornyTrusted.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListener.java b/src/main/old/cc/sukazyo/cono/morny/bot/api/EventListener.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/api/EventListener.java rename to src/main/old/cc/sukazyo/cono/morny/bot/api/EventListener.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java b/src/main/old/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java rename to src/main/old/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java b/src/main/old/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java rename to src/main/old/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/api/OnUpdate.java b/src/main/old/cc/sukazyo/cono/morny/bot/api/OnUpdate.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/api/OnUpdate.java rename to src/main/old/cc/sukazyo/cono/morny/bot/api/OnUpdate.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java rename to src/main/old/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java rename to src/main/old/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/MornyCommands.java similarity index 96% rename from src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java rename to src/main/old/cc/sukazyo/cono/morny/bot/command/MornyCommands.java index e84ab3e..d3cf37f 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ b/src/main/old/cc/sukazyo/cono/morny/bot/command/MornyCommands.java @@ -55,15 +55,15 @@ public class MornyCommands { register( new ON(), new Hello(), /* new {@link HelloOnStart}, */ - new MornyInfoOnHello(), - new GetUsernameAndId(), - new EventHack(), - new Nbnhhsh(), - new Ip186Query.Ip(), - new Ip186Query.Whois(), - new Encryptor(), + MornyInformation$.MODULE$, + GetUsernameAndId$.MODULE$, + EventHack$.MODULE$, + Nbnhhsh$.MODULE$, + IP186Query.IP$.MODULE$, + IP186Query.Whois$.MODULE$, + Encryptor$.MODULE$, new SaveData(), - new MornyInformation(), + MornyInfoOnHello$.MODULE$, new Version(), new MornyRuntime(), new Jrrp(), @@ -72,21 +72,21 @@ public class MornyCommands { // 特殊的命令 register( - new Testing(), - new DirectMsgClear() + Testing$.MODULE$, + DirectMsgClear$.MODULE$ ); // 统一注册这些奇怪的东西&.& register( - new 私わね(), - new 喵呜.Progynova() + 私わね$.MODULE$, + 喵呜.Progynova$.MODULE$ ); // special: 注册出于兼容使用的特别 event 的数据 OnUniMeowTrigger.register( - new 喵呜.抱抱(), - new 喵呜.揉揉(), - new 喵呜.蹭蹭(), - new 喵呜.贴贴() + 喵呜.抱抱$.MODULE$, + 喵呜.揉揉$.MODULE$, + 喵呜.蹭蹭$.MODULE$, + 喵呜.贴贴$.MODULE$ ); } diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/Roll.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/Roll.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/command/Roll.java rename to src/main/old/cc/sukazyo/cono/morny/bot/command/Roll.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/command/package-info.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/package-info.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/command/package-info.java rename to src/main/old/cc/sukazyo/cono/morny/bot/command/package-info.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/EventListeners.java similarity index 79% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/EventListeners.java index 76c9256..f60095e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/bot/event/EventListeners.java +++ b/src/main/old/cc/sukazyo/cono/morny/bot/event/EventListeners.java @@ -1,18 +1,15 @@ package cc.sukazyo.cono.morny.bot.event; import cc.sukazyo.cono.morny.bot.api.EventListenerManager; +import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; public class EventListeners { public static final OnTelegramCommand COMMANDS_LISTENER = new OnTelegramCommand(); // public static final OnActivityRecord ACTIVITY_RECORDER = new OnActivityRecord(); - public static final OnUserSlashAction USER_SLASH_ACTION = new OnUserSlashAction(); public static final OnUpdateTimestampOffsetLock UPDATE_TIMESTAMP_OFFSET_LOCK = new OnUpdateTimestampOffsetLock(); - public static final OnInlineQueries INLINE_QUERY = new OnInlineQueries(); - public static final OnCallMe CALL_ME = new OnCallMe(); public static final OnEventHackHandle EVENT_HACK_HANDLE = new OnEventHackHandle(); // static final OnKuohuanhuanNeedSleep KUOHUANHUAN_NEED_SLEEP = new OnKuohuanhuanNeedSleep(); - public static final OnUserRandoms USER_RANDOMS = new OnUserRandoms(); public static final OnCallMsgSend CALL_MSG_SEND = new OnCallMsgSend(); public static final OnMedicationNotifyApply MEDICATION_NOTIFY_APPLY = new OnMedicationNotifyApply(); public static final OnRandomlyTriggered RANDOMLY_TRIGGERED = new OnRandomlyTriggered(); @@ -28,11 +25,11 @@ public class EventListeners { COMMANDS_LISTENER, UNI_MEOW_TRIGGER, RANDOMLY_TRIGGERED, - USER_RANDOMS, + OnUserRandom$.MODULE$, QUESTION_MARK_REPLY, - USER_SLASH_ACTION, - INLINE_QUERY, - CALL_ME, + OnUserSlashAction$.MODULE$, + OnInlineQuery$.MODULE$, + OnCallMe$.MODULE$, CALL_MSG_SEND, MEDICATION_NOTIFY_APPLY, EVENT_HACK_HANDLE diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java diff --git a/src/main/java/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java rename to src/main/old/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java b/src/main/old/cc/sukazyo/cono/morny/daemon/MedicationTimer.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/daemon/MedicationTimer.java rename to src/main/old/cc/sukazyo/cono/morny/daemon/MedicationTimer.java diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyDaemons.java b/src/main/old/cc/sukazyo/cono/morny/daemon/MornyDaemons.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/daemon/MornyDaemons.java rename to src/main/old/cc/sukazyo/cono/morny/daemon/MornyDaemons.java diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java b/src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java similarity index 99% rename from src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java rename to src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java index 11a646c..9a81a5e 100644 --- a/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java +++ b/src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java @@ -88,7 +88,7 @@ public class MornyReport { as config fields: %s """, - MornyInformation.getVersionAllFullTagHtml(), + MornyInformation.getVersionAllFullTagHTML(), MornyCoeur.getUsername(), sectionConfigFields(MornyCoeur.config()) ) diff --git a/src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java b/src/main/old/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java rename to src/main/old/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java diff --git a/src/main/java/cc/sukazyo/cono/morny/data/MornyJrrp.java b/src/main/old/cc/sukazyo/cono/morny/data/MornyJrrp.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/data/MornyJrrp.java rename to src/main/old/cc/sukazyo/cono/morny/data/MornyJrrp.java diff --git a/src/main/java/cc/sukazyo/cono/morny/data/NbnhhshQuery.java b/src/main/old/cc/sukazyo/cono/morny/data/NbnhhshQuery.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/data/NbnhhshQuery.java rename to src/main/old/cc/sukazyo/cono/morny/data/NbnhhshQuery.java diff --git a/src/main/java/cc/sukazyo/cono/morny/data/TelegramImages.java b/src/main/old/cc/sukazyo/cono/morny/data/TelegramImages.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/data/TelegramImages.java rename to src/main/old/cc/sukazyo/cono/morny/data/TelegramImages.java diff --git a/src/main/java/cc/sukazyo/cono/morny/data/TelegramStickers.java b/src/main/old/cc/sukazyo/cono/morny/data/TelegramStickers.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/data/TelegramStickers.java rename to src/main/old/cc/sukazyo/cono/morny/data/TelegramStickers.java diff --git a/src/main/java/cc/sukazyo/cono/morny/internal/BuildConfigField.java b/src/main/old/cc/sukazyo/cono/morny/internal/BuildConfigField.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/internal/BuildConfigField.java rename to src/main/old/cc/sukazyo/cono/morny/internal/BuildConfigField.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java b/src/main/old/cc/sukazyo/cono/morny/util/BiliTool.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java rename to src/main/old/cc/sukazyo/cono/morny/util/BiliTool.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java b/src/main/old/cc/sukazyo/cono/morny/util/CommonConvert.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java rename to src/main/old/cc/sukazyo/cono/morny/util/CommonConvert.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java b/src/main/old/cc/sukazyo/cono/morny/util/CommonEncrypt.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java rename to src/main/old/cc/sukazyo/cono/morny/util/CommonEncrypt.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java b/src/main/old/cc/sukazyo/cono/morny/util/CommonFormat.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java rename to src/main/old/cc/sukazyo/cono/morny/util/CommonFormat.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java b/src/main/old/cc/sukazyo/cono/morny/util/CommonRandom.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java rename to src/main/old/cc/sukazyo/cono/morny/util/CommonRandom.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java b/src/main/old/cc/sukazyo/cono/morny/util/FileUtils.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java rename to src/main/old/cc/sukazyo/cono/morny/util/FileUtils.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java b/src/main/old/cc/sukazyo/cono/morny/util/OkHttpPublic.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java rename to src/main/old/cc/sukazyo/cono/morny/util/OkHttpPublic.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java b/src/main/old/cc/sukazyo/cono/morny/util/UniversalCommand.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java rename to src/main/old/cc/sukazyo/cono/morny/util/UniversalCommand.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/Standardize.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/Standardize.java new file mode 100644 index 0000000..ae3efa3 --- /dev/null +++ b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/Standardize.java @@ -0,0 +1,7 @@ +package cc.sukazyo.cono.morny.util.tgapi; + +public class Standardize { + + public static final int CHANNEL_SPEAKER_MAGIC_ID = 136817688; + +} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java similarity index 92% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java index 8671056..46045f0 100644 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java +++ b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java @@ -1,6 +1,7 @@ package cc.sukazyo.cono.morny.util.tgapi.formatting; import com.pengrad.telegrambot.model.Chat; +import com.pengrad.telegrambot.model.Message; public class TGToStringFromChat { diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java rename to src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java diff --git a/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala new file mode 100644 index 0000000..91907ae --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala @@ -0,0 +1,156 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyConfig.CheckFailure +import cc.sukazyo.cono.morny.util.CommonFormat + +import java.time.ZoneOffset +import scala.collection.mutable.ArrayBuffer +import scala.language.postfixOps + +object ServerMain { + + private val THREAD_MORNY_INIT: String = "morny-init" + + val systemStartupTime: Long = System.currentTimeMillis() + + def main (args: Array[String]): Unit = { + + val config = new MornyConfig.Prototype() + var mode_echoVersion = false + var mode_echoHello = false + var showHello = true + + config.eventOutdatedTimestamp = systemStartupTime + + val unknownArgs = ArrayBuffer[String]() + + var i = 0 + while (i < args.length) { + args(i) match { + + case "-d" | "--dbg" | "--debug" => Log.debug(true) + + case "--no-hello" | "-hf" | "--quiet" | "-q" => showHello = false + case "--only-hello" | "-ho" | "-o" | "-hi" => mode_echoHello = true + case "--version" | "-v" => mode_echoVersion = true + + case "--outdated-block" | "-ob" => config.eventIgnoreOutdated = true + + case "--api" | "-a" => i+=1 ; config.telegramBotApiServer = args(i) + case "--api-files" | "files-api" | "-af" => i+=1; config.telegramBotApiServer4File = args(i) + + case "--token" | "-t" => i+=1 ; config.telegramBotKey = args(i) + case "--username" | "-u" => i+=1 ; config.telegramBotUsername = args(i) + + case "--master" | "-mm" => i+=1 ; config.trustedMaster = args(i)toLong + case "--trusted-chat" | "-trs" => i+=1 ; config.trustedChat = args(i)toLong + case "--report-to" => i+=1; config.reportToChat = args(i)toLong + + case "--trusted-reader-dinner" | "-trsd" => i+=1 ; config.dinnerTrustedReaders add (args(i)toLong) + case "--dinner-chat" | "-chd" => i+=1 ; config.dinnerChatId = args(i)toLong + + case "--medication-notify-chat" | "-medc" => i+=1 ; config.medicationNotifyToChat = args(i)toLong + case "--medication-notify-timezone" | "-medtz" => + i+=1 + config.medicationTimerUseTimezone = ZoneOffset.ofHours(args(i)toInt) + case "--medication-notify-times" | "-medt" => + i+=1 + for (u <- args(i) split ",") { + config.medicationNotifyAt add (u toInt) + } + + case "--auto-cmd-list" | "-ca" => config.commandLoginRefresh = true + case "--auto-cmd-remove" | "-cr" => config.commandLogoutClear = true + case "--auto-cmd" | "-cmd" | "-c" => + config.commandLoginRefresh = true + config.commandLogoutClear = true + + case _ => unknownArgs append args(i) + + } + i+=1 + } + + /// Setup launch params from ENVIRONMENT + var propToken: String = null + var propTokenKey: String = null + for (iKey <- MornyConfig.PROP_TOKEN_KEY) { + if ((System getenv iKey) != null) { + propToken = System getenv iKey + propTokenKey = iKey + } + } + + /// + /// Output startup message + /// process startup params - like startup mode + /// + + if (showHello) logger info MornyAbout.MORNY_PREVIEW_IMAGE_ASCII + if (mode_echoHello) return; + + if (unknownArgs.nonEmpty) logger warn + s"""Can't understand arg to some meaning + | ${unknownArgs mkString "\n "}""" + .stripMargin + + if (Log debug) + logger warn + """Debug log output enabled. + | It may lower your performance, make sure that you are not in production environment.""" + .stripMargin + + if (mode_echoVersion) { + + logger info + s"""Morny Cono Version + |- version : + | Morny ${MornySystem.CODENAME toUpperCase} + | ${MornySystem.VERSION_BASE}${if (MornySystem.isUseDelta) "-δ"+MornySystem.VERSION_DELTA else ""} + |- md5hash : + | ${MornySystem.getJarMd5} + |- gitstat : + |${ if (MornySystem.isGitBuild) { + s""" on commit ${if (MornySystem.isCleanBuild) "- clean-build" else "<δ/non-clean-build>"} + | ${BuildConfig.COMMIT}""" + .stripMargin + } else " "} + |- buildtd : + | ${BuildConfig.CODE_TIMESTAMP} + | ${CommonFormat.formatDate(BuildConfig.CODE_TIMESTAMP, 0)} [UTC]""" + .stripMargin + return + + } + + logger info + s"""ServerMain.java Loaded >>> + |- version ${MornySystem.VERSION_FULL} + |- Morny ${MornySystem.CODENAME toUpperCase} + |- <${MornySystem.getJarMd5}> [${BuildConfig.CODE_TIMESTAMP}]""".stripMargin + + /// + /// Check Coeur arguments + /// finally start Coeur Program + /// + + if (propToken != null) { + config.telegramBotKey = propToken + logger info s"Parameter set by EnvVar $$$propTokenKey" + } + + Thread.currentThread setName THREAD_MORNY_INIT + + try + MornyCoeur.init(new MornyConfig(config)) + catch { + case _: CheckFailure.NullTelegramBotKey => + logger.info("Parameter required has no value:\n --token.") + case e: CheckFailure => + logger.error("Unknown failure occurred while starting ServerMain!:") + e.printStackTrace(System.out) + } + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala new file mode 100644 index 0000000..e3e1761 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala @@ -0,0 +1,54 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.{Chat, Update} +import com.pengrad.telegrambot.request.{DeleteMessage, GetChatMember, SendSticker} + +import scala.language.postfixOps + +object DirectMsgClear extends ISimpleCommand { + + override def getName: String = "r" + override def getAliases: Array[String] = null + + override def execute (command: InputCommand, event: Update): Unit = { + + logger debug "executing command /r" + if (event.message.replyToMessage == null) return; + logger trace "message is a reply" + if (event.message.replyToMessage.from.id != MornyCoeur.getUserid) return; + logger trace "message replied is from me" + if (System.currentTimeMillis/1000 - event.message.replyToMessage.date > 48*60*60) return; + logger trace "message is not outdated(48 hrs ago)" + + val isTrusted = MornyCoeur.trustedInstance isTrusted event.message.from.id + def _isReplyTrusted: Boolean = + if (event.message.replyToMessage.replyToMessage == null) false + else if (event.message.replyToMessage.replyToMessage.from.id == event.message.from.id) true + else false + + if (isTrusted || _isReplyTrusted) { + + MornyCoeur.extra exec DeleteMessage( + event.message.chat.id, event.message.replyToMessage.messageId + ) + + def _isPrivate: Boolean = event.message.chat.`type` == Chat.Type.Private + def _isPermission: Boolean = + (MornyCoeur.extra exec GetChatMember(event.message.chat.id, event.message.from.id)) + .chatMember.canDeleteMessages + if (_isPrivate || _isPermission) { + MornyCoeur.extra exec DeleteMessage(event.message.chat.id, event.message.messageId) + } + + } else MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(event.message.messageId) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala new file mode 100644 index 0000000..77be888 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala @@ -0,0 +1,178 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.daemon.MornyReport +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex +import cc.sukazyo.cono.morny.util.CommonEncrypt +import cc.sukazyo.cono.morny.util.CommonEncrypt.* +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.{PhotoSize, Update} +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{GetFile, SendDocument, SendMessage, SendSticker} + +import java.io.IOException +import java.util.Base64 +import scala.language.postfixOps + +object Encryptor extends ITelegramCommand { + + override def getName: String = "encrypt" + override def getAliases: Array[String] = null + override def getParamRule: String = "[algorithm|(l)] [(uppercase)]" + override def getDescription: String = "通过指定算法加密回复的内容 (目前只支持文本)" + + override def execute (command: InputCommand, event: Update): Unit = { + + val args = command.getArgs + + if ((args isEmpty) || ((args(0) equals "l") && (args.length == 1))) + echoHelp(event.message.chat.id, event.message.messageId) + return + + def _is_mod_u(arg: String): Boolean = + if (arg equalsIgnoreCase "uppercase") return true + if (arg equalsIgnoreCase "u") return true + if (arg equalsIgnoreCase "upper") return true + false + val mod_uppercase = if (args.length > 1) { + if (args.length < 3 && _is_mod_u(args(1))) true + else + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + return + } else false + + trait XEncryptable { val asByteArray: Array[Byte] } + case class XFile (data: Array[Byte], name: String) extends XEncryptable { + val asByteArray: Array[Byte] = data + } + case class XText (data: String) extends XEncryptable { + val asByteArray: Array[Byte] = data getBytes CommonEncrypt.ENCRYPT_STANDARD_CHARSET + } + val input: XEncryptable = + val _r = event.message.replyToMessage + if ((_r ne null) && (_r.document ne null)) { + try {XFile( + MornyCoeur.getAccount getFileContent (MornyCoeur.extra exec GetFile(_r.document.fileId)).file, + _r.document.fileName + )} catch case e: IOException => + logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" + MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI") + return + } else if ((_r ne null) && (_r.photo ne null)) { + try { + var _photo_origin: PhotoSize = null + var _photo_size: Long = 0 + for (size <- _r.photo) + val _size = (size.width longValue)*size.height + if (_photo_size < _size) + _photo_origin = size + _photo_size = _size + if (_photo_origin eq null) throw IllegalArgumentException("no photo from api.") + XFile( + MornyCoeur.getAccount getFileContent (MornyCoeur.extra exec GetFile(_photo_origin.fileId)).file, + s"photo${byteArrayToHex(hashMd5(System.currentTimeMillis toString)) substring 32-12 toUpperCase}.png" + ) + } catch + case e: IOException => + logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" + MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI") + return + case e: IllegalArgumentException => + logger warn s"FileProcess error: PhotoSize:\n\t${e.getMessage}" + MornyReport.exception(e, "FileProcess error: PhotoSize") + return + } else if ((_r ne null) && (_r.text ne null)) { + XText(_r.text) + } else { + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + "null" + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + return + } + + + trait EXTextLike { val text: String } + case class EXFile (result: Array[Byte], resultName: String) + case class EXText (result: String) extends EXTextLike { override val text:String = result } + case class EXHash (result: String) extends EXTextLike { override val text:String = result } + def genResult_encrypt (source: XEncryptable, processor: Array[Byte]=>Array[Byte], filenameProcessor: String=>String): EXFile|EXText = { + source match + case x_file: XFile => EXFile(processor(x_file asByteArray), filenameProcessor(x_file.name)) + case x: XText => EXText(String(processor(x asByteArray), ENCRYPT_STANDARD_CHARSET)) + } + def genResult_hash (source: XEncryptable, processor: Array[Byte]=>Array[Byte]): EXHash = + val hashed = byteArrayToHex(processor(source asByteArray)) + EXHash(if mod_uppercase then hashed toUpperCase else hashed) + val result: EXHash|EXFile|EXText = args(0) match + case "base64" | "b64" | "base64url" | "base64u" | "b64u" => + val _tool_b64 = + if args(0) contains "u" then Base64.getUrlEncoder + else Base64.getEncoder + genResult_encrypt( + input, + _tool_b64.encode, + n => n+".b64.txt" + ) + case "base64decode" | "base64d" | "b64d" | "base64url-decode" | "base64ud" | "b64ud" => + val _tool_b64d = + if args(0) contains "u" then Base64.getUrlDecoder + else Base64.getDecoder + try { genResult_encrypt( + input, + _tool_b64d.decode, + CommonEncrypt.base64FilenameLint + ) } catch case _: IllegalArgumentException => + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 // todo: is here better erro notify? + ).replyToMessageId(event.message.messageId) + return + case "md5" => genResult_hash(input, hashMd5) + case "sha1" => genResult_hash(input, hashSha1) + case "sha256" => genResult_hash(input, hashSha256) + case "sha512" => genResult_hash(input, hashSha512) + case _ => + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + return; + + result match + case _file: EXFile => + MornyCoeur.extra exec SendDocument( + event.message.chat.id, + _file.result + ).fileName(_file.resultName).replyToMessageId(event.message.messageId) + case _text: EXTextLike => + import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + s"
      ${h(_text.text)}
      " + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + + } + + private def echoHelp(chat: Long, replyTo: Int): Unit = + MornyCoeur.extra exec SendMessage( + chat, + s"""base64, b64 + |base64url, base64u, b64u + |base64decode, base64d, b64d + |base64url-decode, base64ud, b64ud + |sha1 + |sha256 + |sha512 + |md5 + |--- + |uppercase, upper, u (sha1/sha256/sha512/md5 only)""" + .stripMargin + ).replyToMessageId(replyTo).parseMode(ParseMode HTML) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala new file mode 100644 index 0000000..a4fddb0 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala @@ -0,0 +1,56 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update +import OnEventHackHandle.{HackType, registerHack} +import cc.sukazyo.cono.morny.data.TelegramStickers +import com.pengrad.telegrambot.request.SendSticker + +import scala.language.postfixOps + +object EventHack extends ITelegramCommand { + + override def getName: String = "event_hack" + override def getAliases: Array[String] = null + override def getParamRule: String = "[(user|group|any)]" + override def getDescription: String = "输出 bot 下一个获取到的事件序列化数据" + + override def execute (command: InputCommand, event: Update): Unit = { + + val x_mode = if (command.hasArgs) command.getArgs()(0) else "" + + def done_ok = + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_WAITING + ).replyToMessageId(event.message.messageId) + def done_forbiddenForAny = + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(event.message.messageId) + + def doRegister (t: HackType): Unit = + registerHack( + event.message.messageId longValue, + event.message.from.id, + event.message.chat.id, + t + ) + x_mode match + case "any" => + if (MornyCoeur.trustedInstance isTrusted event.message.from.id) + doRegister(HackType ANY) + done_ok + else done_forbiddenForAny + case "group" => + doRegister(HackType GROUP) + done_ok + case _ => + doRegister(HackType USER) + done_ok + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala new file mode 100644 index 0000000..3f03650 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala @@ -0,0 +1,66 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation +import cc.sukazyo.cono.morny.util.tgapi.{InputCommand, Standardize} +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{GetChatMember, SendMessage} + +import scala.language.postfixOps + +object GetUsernameAndId extends ITelegramCommand { + + override def getName: String = "user" + override def getAliases: Array[String] = Array() + override def getParamRule: String = "[userid]" + override def getDescription: String = "获取指定或回复的用户相关信息" + + override def execute (command: InputCommand, event: Update): Unit = { + + val args = command.getArgs + + if (args.length > 1) + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + "[Unavailable] Too much arguments." + ).replyToMessageId(event.message.messageId) + return + + val userId: Long = + if (args nonEmpty) { + try args(0) toLong + catch case e: NumberFormatException => + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + s"[Unavailable] ${e.getMessage}" + ).replyToMessageId(event.message.messageId) + return + } else if (event.message.replyToMessage eq null) event.message.from.id + else event.message.replyToMessage.from.id + + val response = MornyCoeur.getAccount execute GetChatMember(event.message.chat.id, userId) + + if (response.chatMember eq null) + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + "[Unavailable] user not found." + ).replyToMessageId(event.message.messageId) + return + + val user = response.chatMember.user + + if (user.id eq Standardize.CHANNEL_SPEAKER_MAGIC_ID) + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + "$__channel_identify" + ).replyToMessageId(event.message.messageId) + return; + + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + TelegramUserInformation informationOutputHTML user + ).replyToMessageId(event.message.messageId()).parseMode(ParseMode HTML) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala new file mode 100644 index 0000000..a901580 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala @@ -0,0 +1,77 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.ip186.IP186QueryHandler +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps + +object IP186Query { + + private enum Subs (val cmd: String): + case IP extends Subs("ip") + case WHOIS extends Subs("whois") + + object IP extends ITelegramCommand: + override def getName: String = "ip" + override def getAliases: Array[String] = null + override def getParamRule: String = "[ip]" + override def getDescription: String = "通过 https://ip.186526.xyz 查询 ip 资料" + override def execute (command: InputCommand, event: Update): Unit = query(event, command) + object Whois extends ITelegramCommand: + override def getName: String = "whois" + override def getAliases: Array[String] = null + override def getParamRule: String = "[domain]" + override def getDescription: String = "通过 https://ip.186526.xyz 查询域名资料" + override def execute (command: InputCommand, event: Update): Unit = query(event, command) + + private def query (event: Update, command: InputCommand): Unit = { + + val target: String|Null = + if (command.getArgs isEmpty) + if event.message.replyToMessage eq null then null else event.message.replyToMessage.text + else if (command.getArgs.length > 1) + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + "[Unavailable] Too much arguments." + ).replyToMessageId(event.message.messageId) + return + else command.getArgs()(0) + + if (target eq null) + MornyCoeur.extra exec new SendMessage( + event.message.chat.id, + "[Unavailable] No ip defined." + ).replyToMessageId(event.message.messageId) + return; + + try { + + val response = command.getCommand match + case Subs.IP.cmd => IP186QueryHandler.query_ip(target) + case Subs.WHOIS.cmd => IP186QueryHandler.query_whoisPretty(target) + case _ => throw IllegalArgumentException(s"Unknown 186-IP query method ${command.getCommand}") + + import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + s"""${h(response.url)} + |${h(response.body)}""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + + } catch case e: Exception => + import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + MornyCoeur.extra().exec(new SendMessage( + event.message().chat().id(), + s"""[Exception] in query: + |${h(e.getMessage)}""" + .stripMargin + ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.scala new file mode 100644 index 0000000..2df1ba5 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.scala @@ -0,0 +1,35 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendPhoto + +import scala.language.postfixOps + +object MornyInfoOnHello extends ISimpleCommand { + + override def getName: String = "start" + override def getAliases: Array[String] = Array() + + override def execute (command: InputCommand, event: Update): Unit = { + + MornyCoeur.extra exec new SendPhoto( + event.message.chat.id, + MornyInformation.getAboutPic + ).caption( + s"""欢迎使用 Morny Cono来自安妮的侍从小鼠。 + |Morny 具有各种各样的功能。 + | + |———————————————— + |${MornyInformation.getMornyAboutLinksHTML} + |———————————————— + | + |(你可以随时通过 /info 重新获得这些信息)""" + .stripMargin + ).parseMode(ParseMode HTML) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala new file mode 100644 index 0000000..e81a9fb --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -0,0 +1,167 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.data.{TelegramImages, TelegramStickers} +import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h +import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornyCoeur, MornySystem} +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{SendMessage, SendPhoto, SendSticker} + +import java.lang.System +import java.net.InetAddress +import java.rmi.UnknownHostException +import scala.language.postfixOps + +object MornyInformation extends ITelegramCommand { + + private case object Subs { + val STICKERS = "stickers" + val RUNTIME = "runtime" + val VERSION = "version" + val VERSION_2 = "v" + } + + override def getName: String = "info" + override def getAliases: Array[String] = Array() + override def getParamRule: String = "[(version|runtime|stickers[.IDs])]" + override def getDescription: String = "输出当前 Morny 的各种信息" + + override def execute (command: InputCommand, event: Update): Unit = { + + if (!command.hasArgs) { + echoInfo(event.message.chat.id, event.message.messageId) + return + } + + val action: String = command.getArgs()(0) + + action match { + case Subs.STICKERS => echoStickers(command, event) + case Subs.RUNTIME => echoRuntime(event) + case Subs.VERSION | Subs.VERSION_2 => echoVersion(event) + case _ => echo404(event) + } + + } + + def getVersionGitTagHTML: String = { + if (!MornySystem.isGitBuild) return "" + val g = StringBuilder() + val cm = BuildConfig.COMMIT substring(0, 8) + val cp = MornySystem.currentCodePath + if (cp == null) g++= s"$cm" + else g++= s"$cm" + if (!MornySystem.isCleanBuild) g++= ".δ" + g toString + } + + def getVersionAllFullTagHTML: String = { + val v = StringBuilder() + v ++= s"${MornySystem VERSION_BASE}" + if (MornySystem isUseDelta) v++=s"-δ${MornySystem VERSION_DELTA}" + if (MornySystem isGitBuild) v++="+"++=getVersionGitTagHTML + v ++= s"*${MornySystem.CODENAME toUpperCase}" + v toString + } + + def getRuntimeHostname: String|Null = { + try InetAddress.getLocalHost.getHostName + catch case _:UnknownHostException => null + } + + def getAboutPic: Array[Byte] = TelegramImages.IMG_ABOUT get + + def getMornyAboutLinksHTML: String = + s"""source code | backup + |反馈 / issue tracker + |使用说明书 / user guide & docs""" + .stripMargin + + private def echoInfo (chatId: Long, replyTo: Int): Unit = { + MornyCoeur.extra exec new SendPhoto( + chatId, + getAboutPic + ).caption( + s"""Morny Cono + |来自安妮的侍从小鼠。 + |———————————————— + |$getMornyAboutLinksHTML""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(replyTo) + } + + private def echoStickers (command: InputCommand, event: Update): Unit = { + val chat = event.message.chat.id + val replyTo = event.message.messageId + var sid: String|Null = null + if (command.getArgs()(0) eq Subs.STICKERS) { + if (command.getArgs.length == 1) sid = "" + else if (command.getArgs.length == 2) sid = command.getArgs()(1) + } else if (command.getArgs.length == 1) { + if ((command.getArgs()(0) startsWith s"${Subs.STICKERS}.") || (command.getArgs()(0) startsWith s"${Subs.STICKERS}#")) { + sid = command.getArgs()(0) substring Subs.STICKERS.length+1 + } + } + if (sid == null) echo404(event) + else echoStickers(sid, chat, replyTo) + } + + private def echoStickers (sid: String, send_chat: Long, send_replyTo: Int): Unit = { + if (sid isEmpty) TelegramStickers echoAllStickers(MornyCoeur.extra, send_chat, send_replyTo) + else TelegramStickers echoStickerByID(sid, MornyCoeur.extra, send_chat, send_replyTo) + } + + private[command] def echoVersion (event: Update): Unit = { + val versionDeltaHTML = if (MornySystem.isUseDelta) s"-δ${h(MornySystem.VERSION_DELTA)}" else "" + val versionGitHTML = if (MornySystem.isGitBuild) s"git $getVersionGitTagHTML" else "" + MornyCoeur.extra exec new SendMessage( + event.message.chat.id, + s"""version: + |- Morny ${h(MornySystem.CODENAME toUpperCase)} + |- ${h(MornySystem.VERSION_BASE)}$versionDeltaHTML${if (MornySystem.isGitBuild) "\n- " + versionGitHTML else ""} + |coeur md5_hash: + |- ${h(MornySystem.getJarMd5)} + |coding timestamp: + |- ${BuildConfig.CODE_TIMESTAMP} + |- ${h(formatDate(BuildConfig.CODE_TIMESTAMP, 0))} [UTC] + |""".stripMargin + ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) + } + + private[command] def echoRuntime (event: Update): Unit = { + def sysprop (p: String): String = System.getProperty(p) + MornyCoeur.extra exec new SendMessage( + event.message.chat.id, + /* html */ + s"""system: + |- Morny ${h(if (getRuntimeHostname == null) "" else getRuntimeHostname)} + |- ${h(sysprop("os.name"))} ${h(sysprop("os.arch"))} ${h(sysprop("os.version"))} + |java runtime: + |- ${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))} + |- ${h(sysprop("java.vm.version"))} + |vm memory: + |- ${Runtime.getRuntime.totalMemory/1024/1024} / ${Runtime.getRuntime.maxMemory/1024/1024} + |- ${Runtime.getRuntime.availableProcessors} cores + |coeur version: + |- $getVersionAllFullTagHTML + |- ${h(MornySystem.getJarMd5)} + |- ${h(formatDate(BuildConfig.CODE_TIMESTAMP, 0))} [UTC] + |- [${BuildConfig.CODE_TIMESTAMP}] + |continuous: + |- ${h(formatDuration(System.currentTimeMillis - MornyCoeur.coeurStartTimestamp))} + |- [${System.currentTimeMillis - MornyCoeur.coeurStartTimestamp}] + |- ${h(formatDate(MornyCoeur.coeurStartTimestamp, 0))} + |- [${MornyCoeur.coeurStartTimestamp}]""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + } + + private def echo404 (event: Update): Unit = + MornyCoeur.extra exec new SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala new file mode 100644 index 0000000..5c7d799 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala @@ -0,0 +1,85 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.{NbnhhshQuery, TelegramStickers} +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{SendMessage, SendSticker} + +import java.io.IOException +import scala.language.postfixOps + +object Nbnhhsh extends ITelegramCommand { + + private val NBNHHSH_RESULT_HEAD_HTML = "## Result of nbnhhsh query :" + + override def getName: String = "nbnhhsh" + override def getAliases: Array[String]|Null = null + override def getParamRule: String = "[text]" + override def getDescription: String = "检索文本内 nbnhhsh 词条" + + override def execute (command: InputCommand, event: Update): Unit = { + + val queryTarget: String|Null = + import cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting + if (event.message.replyToMessage != null && event.message.replyToMessage.text != null) + event.message.replyToMessage.text + else if command hasArgs then + stringsConnecting(command.getArgs, " ", 0, command.getArgs.length-1) + else null + + if (queryTarget == null) + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + return; + + try { + + val queryResp = NbnhhshQuery sendGuess queryTarget + + val message = StringBuilder(NBNHHSH_RESULT_HEAD_HTML) + + import cc.sukazyo.cono.morny.Log.logger + logger debug s"**xx len=${queryResp.words.length}" + for (_word <- queryResp.words) { + logger debug "**exec" + if ((_word.trans ne null) && (_word.trans isEmpty)) _word.trans = null + if ((_word.inputting ne null) && (_word.inputting isEmpty)) _word.inputting = null + if ((_word.trans ne null) || (_word.inputting ne null)) + message ++= s"\n\n[[ ${h(_word.name)} ]]" + logger debug s"**used [${_word.name}]" + if (_word.trans != null) for (_trans <- _word.trans) + message ++= s"\n* ${h(_trans)}" + logger debug s"**used [${_word.name}] used `${_trans}``" + if (_word.inputting != null) + logger debug s"**used [${_word.name}] inputting" + if (_word.trans != null) + message += '\n' + message ++= " maybe:" + for (_inputting <- _word.inputting) + logger debug s"**used [${_word.name}] used-i ${_inputting}" + message ++= s"\n` ${h(_inputting)}" + logger debug s"**exec as ${_word.name}" + } + + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + message toString + ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) + + } catch case e: IOException => { + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + s"""[Exception] in query: + |${h(e.getMessage)} + |""".stripMargin + ) + } + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala new file mode 100644 index 0000000..4d453da --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala @@ -0,0 +1,30 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import javax.annotation.Nonnull +import javax.annotation.Nullable +import scala.language.postfixOps + +object Testing extends ISimpleCommand { + + override def getName: String = "test" + override def getAliases: Array[String] = null + + override def execute (command: InputCommand, event: Update): Unit = { + + val a = StringBuilder("value") + a ++= "Changed" + + MornyCoeur.extra exec new SendMessage( + event.message.chat.id, + "Just a TEST command. num is:" + (a toString) + ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala new file mode 100644 index 0000000..c870e8e --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala @@ -0,0 +1,67 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.model.{Message, Update} +import com.pengrad.telegrambot.request.{SendMessage, SendSticker} + +import javax.swing.text.html.HTML +import scala.annotation.unused +import scala.language.postfixOps + +@SuppressWarnings(Array("NonAsciiCharacters")) +object 喵呜 { + + object 抱抱 extends ISimpleCommand { + override def getName: String = "抱抱" + override def getAliases: Array[String] = Array() + override def execute (command: InputCommand, event: Update): Unit = + replyingSet(event, "贴贴", "贴贴") + } + + object 揉揉 extends ISimpleCommand { + override def getName: String = "揉揉" + override def getAliases: Array[String] = Array() + override def execute (command: InputCommand, event: Update): Unit = + replyingSet(event, "蹭蹭", "摸摸") + } + + object 蹭蹭 extends ISimpleCommand { + override def getName: String = "蹭蹭" + override def getAliases: Array[String] = Array() + override def execute (command: InputCommand, event: Update): Unit = + replyingSet(event, "揉揉", "蹭蹭") + } + + object 贴贴 extends ISimpleCommand { + override def getName: String = "贴贴" + override def getAliases: Array[String] = Array() + override def execute (command: InputCommand, event: Update): Unit = + replyingSet(event, "贴贴", "贴贴") + } + + object Progynova extends ITelegramCommand { + override def getName: String = "install" + override def getAliases: Array[String] = Array() + override def getParamRule: String = "" + override def getDescription: String = "抽取一个神秘盒子" + override def execute (command: InputCommand, event: Update): Unit = { + MornyCoeur.extra exec new SendSticker( + event.message.chat.id, + TelegramStickers ID_PROGYNOVA + ).replyToMessageId(event.message.messageId) + } + } + + private def replyingSet (event: Update, whileRec: String, whileNew: String): Unit = { + val isNew = event.message.replyToMessage == null; + val target = if (isNew) event.message else event.message.replyToMessage + MornyCoeur.extra exec new SendMessage( + event.message.chat.id, + if (isNew) whileNew else whileRec + ).replyToMessageId(target.messageId).parseMode(ParseMode HTML) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala new file mode 100644 index 0000000..67da3f5 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala @@ -0,0 +1,26 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendMessage + +object 私わね extends ISimpleCommand { + + override def getName: String = "me" + override def getAliases: Array[String] = Array() + + override def execute (command: InputCommand, event: Update): Unit = { + + if (probabilityTrue(521)) { + val text = "/打假" + MornyCoeur.extra exec new SendMessage( + event.message.chat.id, + text + ).replyToMessageId(event.message.messageId) + } + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala new file mode 100644 index 0000000..1f51e4d --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -0,0 +1,90 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.model.{Chat, Message, Update, User} +import com.pengrad.telegrambot.request.{ForwardMessage, GetChat, SendMessage, SendSticker} + +import scala.language.postfixOps + +object OnCallMe extends EventListener { + + private val me = MornyCoeur.config.trustedMaster + + override def onMessage (update: Update): Boolean = { + + if update.message.text == null then return false + if update.message.chat.`type` != (Chat.Type Private) then return false + + (update.message.text toLowerCase) match + case "steam" | "sbeam" | "sdeam" => + requestItem(update.message.from, "STEAM LIBRARY") + case "hana paresu" | "花宫" | "内群" => + requestItem(update.message.from, "Hana Paresu") + case "dinner" | "lunch" | "breakfast" | "meal" | "eating" | "安妮今天吃什么" => + requestLastDinner(update.message) + case cc if cc startsWith "cc::" => + requestCustom(update.message) + case _ => + return false + + MornyCoeur.extra exec SendSticker( + update.message.chat.id, + TelegramStickers ID_SENT + ).replyToMessageId(update.message.messageId) + true + + } + + private def requestItem (user: User, itemHTML: String, extra: String|Null = null): Unit = + MornyCoeur.extra exec SendMessage( + me, + s"""request $itemHTML + |from ${(TGToString as user) fullnameRefHtml}${if extra == null then "" else "\n"+extra}""" + .stripMargin + ).parseMode(ParseMode HTML) + + private def requestLastDinner (req: Message): Unit = { + var isAllowed = false + var lastDinnerData: Message|Null = null + if (MornyCoeur.trustedInstance isTrustedForDinnerRead req.from.id) { + lastDinnerData = (MornyCoeur.extra exec GetChat(MornyCoeur.config.dinnerChatId)).chat.pinnedMessage + val sendResp = MornyCoeur.extra exec ForwardMessage( + req.from.id, + lastDinnerData.forwardFromChat.id, + lastDinnerData.forwardFromMessageId + ) + import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} + import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + def lastDinner_dateMillis: Long = lastDinnerData.forwardDate longValue; + MornyCoeur.extra exec SendMessage( + req.from.id, + "on %s [UTC+8]\n- %s before".formatted( + h(formatDate(lastDinner_dateMillis, 8)), + h(formatDuration(lastDinner_dateMillis)) + ) + ).parseMode(ParseMode HTML).replyToMessageId(sendResp.message.messageId) + isAllowed = true + } else { + MornyCoeur.extra exec SendSticker( + req.from.id, + TelegramStickers ID_403 + ).replyToMessageId(req.messageId) + } + import Math.abs + requestItem( + req.from, "Last Annie Dinner", + if isAllowed then s"Allowed and returned https://t.me/c/${abs(lastDinnerData.forwardFromChat.id+1000000000000L)}/${lastDinnerData.forwardFromMessageId}" + else "Forbidden by perm check." + ) + } + + private def requestCustom (message: Message): Unit = + requestItem(message.from, "[???]") + MornyCoeur.extra exec ForwardMessage(me, message.chat.id, message.messageId) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala new file mode 100644 index 0000000..96f2ad0 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala @@ -0,0 +1,37 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.api.{EventListener, InlineQueryUnit} +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.InlineQueryResult +import com.pengrad.telegrambot.request.AnswerInlineQuery + +import scala.collection.mutable.ListBuffer +import scala.language.postfixOps +import scala.reflect.ClassTag + +object OnInlineQuery extends EventListener { + + override def onInlineQuery (update: Update): Boolean = { + + val results: List[InlineQueryUnit[_]] = MornyCoeur.queryManager query update + + var cacheTime = Int.MaxValue + var isPersonal = InlineQueryUnit.DEFAULT_INLINE_PERSONAL_RESP + val resultAnswers = ListBuffer[InlineQueryResult[_]]() + for (r <- results) { + if (cacheTime > r.cacheTime) cacheTime = r.cacheTime + if (r isPersonal) isPersonal = true + resultAnswers += r.result + } + + if (results isEmpty) return false + + MornyCoeur.extra exec AnswerInlineQuery( + update.inlineQuery.id, resultAnswers toArray:_* + ).cacheTime(cacheTime).isPersonal(isPersonal) + true + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala new file mode 100644 index 0000000..2e21f63 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala @@ -0,0 +1,38 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.api.EventListener +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps + +object OnUserRandom extends EventListener { + + private val USER_OR_QUERY = "(.+)(?:还是|or)(.+)"r + private val USER_IF_QUERY = "(.+)[吗?|?]+$"r + + override def onMessage(update: Update): Boolean = { + + if update.message.text == null then return false + if update.message.text startsWith "/" then return false + + import cc.sukazyo.cono.morny.util.CommonRandom.iif + val query = update.message.text substring 1 + val result: String|Null = query match + case USER_OR_QUERY(_con1, _con2) => + if iif then _con1 else _con2 + case USER_IF_QUERY(_con) => + (if iif then "不" else "") + _con + case _ => null + + if result == null then return false + + MornyCoeur.extra exec SendMessage( + update.message.chat.id, result + ).replyToMessageId(update.message.messageId) + true + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala new file mode 100644 index 0000000..1970726 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala @@ -0,0 +1,67 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.util.UniversalCommand +import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h +import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps + +object OnUserSlashAction extends EventListener { + + private val TG_FORMAT = "^\\w+(@\\w+)?$"r + + override def onMessage (update: Update): Boolean = { + + val text = update.message.text; + if text == null then return false + + if (text startsWith "/") { + + val actions = UniversalCommand format text + actions(0) = actions(0) substring 1 + + actions(0) + + actions(0) match + case TG_FORMAT(_) => + return false + case x if x contains "/" => return false + + val isHardParse = actions(0) isBlank + def hp_len(i: Int) = if isHardParse then i+1 else i + if isHardParse && actions.length < 2 then return false + val v_verb = actions(hp_len(0)) + val hasObject = actions.length != hp_len(1) + val v_object = + if hasObject then + actions slice(hp_len(1), actions.length) mkString(" ") + else "" + val origin = update.message + val target = + if update.message.replyToMessage == null then + origin + else update.message.replyToMessage + + MornyCoeur.extra exec SendMessage( + update.message.chat.id, + "%s %s%s %s %s!".format( + (TGToString as origin) getSenderFirstNameRefHtml, + h(v_verb), if hasObject then "" else "了", + if (origin == target) + s"自己" + else (TGToString as target) getSenderFirstNameRefHtml, + if hasObject then h(v_object+" ") else "" + ) + ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) + true + + } else false + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala new file mode 100644 index 0000000..d03a05c --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala @@ -0,0 +1,11 @@ +package cc.sukazyo.cono.morny.bot.query + +import javax.annotation.Nullable +import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit +import com.pengrad.telegrambot.model.Update + +trait ITelegramQuery { + + def query (event: Update): List[InlineQueryUnit[_]] | Null + +} \ No newline at end of file diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala new file mode 100644 index 0000000..faeaefb --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit +import cc.sukazyo.cono.morny.bot.query +import com.pengrad.telegrambot.model.Update + +import scala.collection.mutable.ListBuffer + +class MornyQueries { + + private val queryInstances = Set[ITelegramQuery]( + RawText, + MyInformation, + ShareToolTwitter, + ShareToolBilibili + ) + + def query (event: Update): List[InlineQueryUnit[_]] = { + val results = ListBuffer[InlineQueryUnit[_]]() + for (instance <- queryInstances) { + val r = instance query event + if (r != null) results ++= r + } + results.result() + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala new file mode 100644 index 0000000..c98feb6 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala @@ -0,0 +1,31 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} + +import scala.language.postfixOps + +object MyInformation extends ITelegramQuery { + + private val ID_PREFIX = "[morny/info/me]" + private val TITLE = "My Account Information" + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + + if (event.inlineQuery.query == null || (event.inlineQuery.query isBlank)) return null + + List( + InlineQueryUnit(InlineQueryResultArticle( + inlineIds(ID_PREFIX), TITLE, + new InputTextMessageContent( + TelegramUserInformation informationOutputHTML event.inlineQuery.from + ).parseMode(ParseMode HTML) + )).isPersonal(true).cacheTime(10) + ) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala new file mode 100644 index 0000000..2d19149 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.bot.query +import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent} + +import scala.language.postfixOps + +object RawText extends ITelegramQuery { + + private val ID_PREFIX = "[morny/r/text]" + private val TITLE = "Raw Text" + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + + if (event.inlineQuery.query == null || (event.inlineQuery.query isBlank)) return null + + List( + InlineQueryUnit(InlineQueryResultArticle( + inlineIds(ID_PREFIX, event.inlineQuery.query), TITLE, + InputTextMessageContent(event.inlineQuery.query) + )) + ) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala new file mode 100644 index 0000000..90288e2 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala @@ -0,0 +1,77 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit +import cc.sukazyo.cono.morny.util.BiliTool +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} + +import scala.language.postfixOps +import scala.util.matching.Regex + +object ShareToolBilibili extends ITelegramQuery { + + private val TITLE_BILI_AV = "[bilibili] Share video / av" + private val TITLE_BILI_BV = "[bilibili] Share video / BV" + private val ID_PREFIX_BILI_AV = "[morny/share/bili/av]" + private val ID_PREFIX_BILI_BV = "[morny/share/bili/bv]" + private val LINK_PREFIX = "https://bilibili.com/video/" + private val REGEX_BILI_VIDEO: Regex = "^(?:(?:https?://)?(?:www\\.)?bilibili\\.com(?:/s)?/video/((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))/?(\\?(?:p=(\\d+))?.*)?|(?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$"r + private val SHARE_FORMAT_HTML = "%s" + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + + if (event.inlineQuery.query == null) return null + + event.inlineQuery.query match + case REGEX_BILI_VIDEO(_1, _2, _3, _4, _5, _6, _7) => + + logger debug + s"""====== Share Tool Bilibili Catch ok + |1: ${_1} + |2: ${_2} + |3: ${_3} + |4: ${_4} + |5: ${_5} + |6: ${_6} + |7: ${_7}""" + .stripMargin + + var av = if (_2 != null) _2 else if (_6 != null) _6 else null + var bv = if (_3!=null) _3 else if (_7!=null) _7 else null + logger trace s"catch id av[$av] bv[$bv]" + val part: Int|Null = if (_5!=null) _5 toInt else null + logger trace s"catch video part[$part]" + + if (av == null) { + assert (bv != null) + av = BiliTool.toAv(bv) toString; + logger trace s"converted bv[$av] to av[$av]" + } else { + bv = BiliTool.toBv(av toLong) + logger trace s"converted av[$av] to bv[$bv]" + } + + val id_av = s"av$av" + val id_bv = s"BV$bv" + val linkParams = if (part!=null) s"?p=$part" else "" + val link_av = LINK_PREFIX + id_av + linkParams + val link_bv = LINK_PREFIX + id_bv + linkParams + + List( + InlineQueryUnit(InlineQueryResultArticle( + inlineIds(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av, + InputTextMessageContent(SHARE_FORMAT_HTML.format(link_av, id_av)).parseMode(ParseMode HTML) + )), + InlineQueryUnit(InlineQueryResultArticle( + inlineIds(ID_PREFIX_BILI_BV + bv), TITLE_BILI_BV + bv, + InputTextMessageContent(SHARE_FORMAT_HTML.format(link_bv, id_bv)).parseMode(ParseMode HTML) + )) + ) + + case _ => null + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala new file mode 100644 index 0000000..9427dca --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala @@ -0,0 +1,42 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.InlineQueryResultArticle + +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds + +import scala.language.postfixOps +import scala.util.matching.Regex + +object ShareToolTwitter extends ITelegramQuery { + + val TITLE_VX = "[tweet] Share as VxTwitter" + val TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)" + val ID_PREFIX_VX = "[morny/share/twitter/vxtwi]" + val ID_PREFIX_VX_COMBINED = "[morny/share/twitter/vxtwi_combine]" + val REGEX_TWEET_LINK: Regex = "^(?:https?://)?((?:(?:c\\.)?vx|fx|www\\.)?twitter\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(\\?[\\w&=-]+)?$"r + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + + if (event.inlineQuery.query == null) return null + + event.inlineQuery.query match + + case REGEX_TWEET_LINK(_1, _2, _) => + List( + InlineQueryUnit(InlineQueryResultArticle( + inlineIds(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX, + s"https://vxtwitter.com/$_2" + )), + InlineQueryUnit(InlineQueryResultArticle( + inlineIds(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED, + s"https://c.vxtwitter.com/$_2" + )) + ) + + case _ => null + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala new file mode 100644 index 0000000..65abaca --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala @@ -0,0 +1,41 @@ +package cc.sukazyo.cono.morny.data.ip186 + +import okhttp3.{OkHttpClient, Request} + +import java.io.IOException +import scala.language.postfixOps +import scala.util.Using + +object IP186QueryHandler { + + private val SITE_URL = "https://ip.186526.xyz/" + private val QUERY_PARAM_IP = "type=json&format=true" + private val QUERY_PARAM_WHOIS = "type=plain" + + private val httpClient = OkHttpClient() + + @throws[IOException] + def query_ip (ip: String): IP186Response = + commonQuery(SITE_URL + ip, QUERY_PARAM_IP) + + @throws[IOException] + def query_whois (domain: String): IP186Response = + commonQuery(SITE_URL+"whois/"+domain, QUERY_PARAM_WHOIS) + + @throws[IOException] + def query_whoisPretty (domain: String): IP186Response = + val raw = query_whois(domain) + IP186Response(raw.url, raw.body substring(0, (raw.body indexOf "<<<")+3)) + + @throws[IOException] + private def commonQuery (requestUrl: String, queryParam: String): IP186Response = { + val request = Request.Builder().url(requestUrl + "?" + queryParam).build + var _body_string: String|Null = null + Using ((httpClient newCall request) execute) { response => + if response.body ne null then _body_string = response.body.string + } + if _body_string eq null then throw IOException("Response of ip186: body is empty!") + IP186Response(requestUrl, _body_string) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186Response.scala b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186Response.scala new file mode 100644 index 0000000..6fcad79 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186Response.scala @@ -0,0 +1,3 @@ +package cc.sukazyo.cono.morny.data.ip186 + +case class IP186Response (url: String, body: String) From 1a31a22cd9fc7a84a00e060bbc27786d13413b05 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Thu, 7 Sep 2023 22:15:06 +0800 Subject: [PATCH 30/40] scala port stage2 (not tested) --- build.gradle | 9 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/old/cc/sukazyo/cono/morny/Log.java | 3 +- .../old/cc/sukazyo/cono/morny/MornyCoeur.java | 19 +- .../cono/morny/bot/api/EventListener.java | 66 ---- .../morny/bot/api/EventListenerManager.java | 118 ------- .../cono/morny/bot/api/InlineQueryUnit.java | 36 --- .../sukazyo/cono/morny/bot/api/OnUpdate.java | 59 ---- .../morny/bot/command/ISimpleCommand.java | 19 -- .../morny/bot/command/ITelegramCommand.java | 12 - .../cono/morny/bot/command/MornyCommands.java | 297 ------------------ .../cono/morny/bot/event/EventListeners.java | 39 --- .../morny/bot/event/OnActivityRecord.java | 4 +- .../cono/morny/bot/event/OnCallMsgSend.java | 222 ------------- .../morny/bot/event/OnEventHackHandle.java | 145 --------- .../bot/event/OnKuohuanhuanNeedSleep.java | 2 +- .../bot/event/OnMedicationNotifyApply.java | 29 -- .../morny/bot/event/OnQuestionMarkReply.java | 38 --- .../morny/bot/event/OnRandomlyTriggered.java | 3 +- .../morny/bot/event/OnTelegramCommand.java | 30 -- .../morny/bot/event/OnUniMeowTrigger.java | 36 --- .../event/OnUpdateTimestampOffsetLock.java | 56 ---- .../cono/morny/daemon/MornyReport.java | 2 +- .../cc/sukazyo/cono/morny/data/MornyJrrp.java | 46 --- .../sukazyo/cono/morny/data/NbnhhshQuery.java | 47 --- .../tgapi/formatting/TGToStringFromChat.java | 39 ++- .../cono/morny/bot/api/EventListener.scala | 22 ++ .../morny/bot/api/EventListenerManager.scala | 84 +++++ .../bot/api/TelegramUpdatesListener.scala | 17 + .../morny/bot/command/DirectMsgClear.scala | 6 +- .../cono/morny/bot/command/Encryptor.scala | 10 +- .../cono/morny/bot/command/EventHack.scala | 10 +- .../morny/bot/command/GetUsernameAndId.scala | 15 +- .../morny/bot/command/ICommandAlias.scala | 18 ++ .../cono/morny/bot/command/IP186Query.scala | 22 +- .../morny/bot/command/ISimpleCommand.scala | 13 + .../morny/bot/command/ITelegramCommand.scala | 8 + .../morny/bot/command/MornyCommands.scala | 111 +++++++ .../cono/morny/bot/command/MornyHellos.scala | 43 +++ ...foOnHello.scala => MornyInfoOnStart.scala} | 8 +- .../morny/bot/command/MornyInformation.scala | 33 +- .../bot/command/MornyInformationOlds.scala | 17 + .../morny/bot/command/MornyManagers.scala | 86 +++++ .../cono/morny/bot/command/MornyOldJrrp.scala | 36 +++ .../cono/morny/bot/command/Nbnhhsh.scala | 26 +- .../cono/morny/bot/command/Testing.scala | 12 +- .../sukazyo/cono/morny/bot/command/喵呜.scala | 44 +-- .../cono/morny/bot/command/私わね.scala | 6 +- .../morny/bot/event/MornyEventListeners.scala | 27 ++ .../cono/morny/bot/event/OnCallMe.scala | 2 +- .../cono/morny/bot/event/OnCallMsgSend.scala | 153 +++++++++ .../morny/bot/event/OnEventHackHandle.scala | 80 +++++ .../cono/morny/bot/event/OnInlineQuery.scala | 7 +- .../bot/event/OnMedicationNotifyApply.scala | 21 ++ .../morny/bot/event/OnQuestionMarkReply.scala | 30 ++ .../morny/bot/event/OnTelegramCommand.scala | 34 ++ .../morny/bot/event/OnUniMeowTrigger.scala | 23 ++ .../event/OnUpdateTimestampOffsetLock.scala | 17 + .../cono/morny/bot/event/OnUserRandom.scala | 2 +- .../morny/bot/event/OnUserSlashAction.scala | 7 +- .../cono/morny/bot/query/ITelegramQuery.scala | 1 - .../morny/bot/query/InlineQueryUnit.scala | 27 ++ .../cono/morny/bot/query/MornyQueries.scala | 1 - .../cono/morny/bot/query/MyInformation.scala | 1 - .../cono/morny/bot/query/RawText.scala | 1 - .../morny/bot/query/ShareToolBilibili.scala | 1 - .../morny/bot/query/ShareToolTwitter.scala | 1 - .../sukazyo/cono/morny/data/MornyJrrp.scala | 17 + .../cono/morny/data/NbnhhshQuery.scala | 37 +++ .../morny/data/ip186/IP186QueryHandler.scala | 9 +- 71 files changed, 1093 insertions(+), 1433 deletions(-) delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/api/EventListener.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/api/OnUpdate.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/command/MornyCommands.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/EventListeners.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/data/MornyJrrp.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/data/NbnhhshQuery.java create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala rename src/main/scala/cc/sukazyo/cono/morny/bot/command/{MornyInfoOnHello.scala => MornyInfoOnStart.scala} (79%) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/query/InlineQueryUnit.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/NbnhhshQuery.scala diff --git a/build.gradle b/build.gradle index a15ac59..4d45291 100644 --- a/build.gradle +++ b/build.gradle @@ -99,8 +99,8 @@ scala { compileJava { - sourceCompatibility '17' - targetCompatibility '17' + sourceCompatibility proj_java.getMajorVersion() + targetCompatibility proj_java.getMajorVersion() options.encoding = proj_file_encoding.name() @@ -108,9 +108,14 @@ scala { compileScala { + sourceCompatibility proj_java.getMajorVersion() + targetCompatibility proj_java.getMajorVersion() + options.encoding = proj_file_encoding.name() scalaCompileOptions.encoding = proj_file_encoding.name() +// scalaCompileOptions.additionalParameters.add("-Yexplicit-nulls") + } } diff --git a/gradle.properties b/gradle.properties index 908c69b..4f1df34 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s VERSION = 1.0.0-RC4 USE_DELTA = true -VERSION_DELTA = scalaport1 +VERSION_DELTA = scalaport2 CODENAME = beiping diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84a0b92..db9a6b8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/old/cc/sukazyo/cono/morny/Log.java b/src/main/old/cc/sukazyo/cono/morny/Log.java index 4c3dadd..57ab124 100644 --- a/src/main/old/cc/sukazyo/cono/morny/Log.java +++ b/src/main/old/cc/sukazyo/cono/morny/Log.java @@ -5,6 +5,7 @@ import cc.sukazyo.messiva.log.LogLevel; import cc.sukazyo.messiva.logger.Logger; import cc.sukazyo.messiva.appender.ConsoleAppender; +import javax.annotation.Nonnull; import java.io.PrintWriter; import java.io.StringWriter; @@ -48,7 +49,7 @@ public class Log { * @return {@link String} 格式的异常的堆栈报告信息. * @see 1.0.0-alpha5 */ - public static String exceptionLog (Exception e) { + public static String exceptionLog (@Nonnull Throwable e) { final StringWriter stackTrace = new StringWriter(); e.printStackTrace(new PrintWriter(stackTrace)); return stackTrace.toString(); diff --git a/src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java b/src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java index 517332d..e6d201d 100644 --- a/src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java +++ b/src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java @@ -1,8 +1,8 @@ package cc.sukazyo.cono.morny; -import cc.sukazyo.cono.morny.bot.api.OnUpdate; +import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener$; import cc.sukazyo.cono.morny.bot.command.MornyCommands; -import cc.sukazyo.cono.morny.bot.event.EventListeners; +import cc.sukazyo.cono.morny.bot.event.MornyEventListeners; import cc.sukazyo.cono.morny.bot.query.MornyQueries; import cc.sukazyo.cono.morny.daemon.MornyDaemons; import cc.sukazyo.cono.morny.daemon.TrackerDataManager; @@ -31,8 +31,6 @@ public class MornyCoeur { /** 当前 Morny 的{@link MornyTrusted 信任验证机}实例 */ private final MornyTrusted trusted; - /** 当前 Morny 的 telegram 命令管理器 */ - private final MornyCommands commandManager = new MornyCommands(); private final MornyQueries queryManager = new MornyQueries(); /** morny 的 bot 账户 */ @@ -127,12 +125,12 @@ public class MornyCoeur { MornyDaemons.start(); logger.info("start telegram events listening"); - EventListeners.registerAllListeners(); - INSTANCE.account.setUpdatesListener(OnUpdate::onNormalUpdate); + MornyEventListeners.registerAllEvents(); + INSTANCE.account.setUpdatesListener(TelegramUpdatesListener$.MODULE$); if (config.commandLoginRefresh) { logger.info("resetting telegram command list"); - commandManager().automaticUpdateList(); + MornyCommands.automaticTGListUpdate(); } logger.info("Coeur start complete"); @@ -156,7 +154,7 @@ public class MornyCoeur { private void exitCleanup () { MornyDaemons.stop(); if (config.commandLogoutClear) { - commandManager.automaticRemoveList(); + MornyCommands.automaticTGListRemove(); } } @@ -288,11 +286,6 @@ public class MornyCoeur { return INSTANCE.trusted; } - @Nonnull - public static MornyCommands commandManager () { - return INSTANCE.commandManager; - } - @Nonnull public static MornyQueries queryManager () { return INSTANCE.queryManager; diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/api/EventListener.java b/src/main/old/cc/sukazyo/cono/morny/bot/api/EventListener.java deleted file mode 100644 index a7c8c5e..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/api/EventListener.java +++ /dev/null @@ -1,66 +0,0 @@ -package cc.sukazyo.cono.morny.bot.api; - -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; - -@SuppressWarnings("unused") -public abstract class EventListener { - - public boolean onMessage (@Nonnull Update update) { - return false; - } - - public boolean onEditedMessage (@Nonnull Update update) { - return false; - } - - public boolean onChannelPost (@Nonnull Update update) { - return false; - } - - public boolean onEditedChannelPost (@Nonnull Update update) { - return false; - } - - public boolean onInlineQuery (@Nonnull Update update) { - return false; - } - - public boolean onChosenInlineResult (@Nonnull Update update) { - return false; - } - - public boolean onCallbackQuery (@Nonnull Update update) { - return false; - } - - public boolean onShippingQuery (@Nonnull Update update) { - return false; - } - - public boolean onPreCheckoutQuery (@Nonnull Update update) { - return false; - } - - public boolean onPoll (@Nonnull Update update) { - return false; - } - - public boolean onPollAnswer (@Nonnull Update update) { - return false; - } - - public boolean onMyChatMemberUpdated (@Nonnull Update update) { - return false; - } - - public boolean onChatMemberUpdated (@Nonnull Update update) { - return false; - } - - public boolean onChatJoinRequest (@Nonnull Update update) { - return false; - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java b/src/main/old/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java deleted file mode 100644 index 671dfd3..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/api/EventListenerManager.java +++ /dev/null @@ -1,118 +0,0 @@ -package cc.sukazyo.cono.morny.bot.api; - -import cc.sukazyo.cono.morny.Log; -import cc.sukazyo.cono.morny.daemon.MornyReport; -import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; -import com.google.gson.GsonBuilder; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Function; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class EventListenerManager { - - private static final List listeners = new ArrayList<>(); - - private static class EventPublisher extends Thread { - - private final Function exec; - - public EventPublisher(@Nonnull Update update, @Nonnull Function exec) { - this.setName("EVT"+update.updateId()); - this.exec = exec; - } - - @Override - public void run () { - for (EventListener x : listeners) { - try { - - if (exec.apply(x)) return; - - } catch (Exception e) { - - final StringBuilder errorMessage = new StringBuilder(); - errorMessage.append("Event throws unexpected exception:\n"); - errorMessage.append(Log.exceptionLog(e).indent(4)); - if (e instanceof EventRuntimeException.ActionFailed) { - errorMessage.append("\ntg-api action: response track: "); - errorMessage.append(new GsonBuilder().setPrettyPrinting().create().toJson( - ((EventRuntimeException.ActionFailed)e).getResponse() - ).indent(4)).append('\n'); - } - logger.error(errorMessage.toString()); - - MornyReport.exception(e, "on event running"); - - } - } - } - - } - - public static void addListener (@Nonnull EventListener... listeners) { - EventListenerManager.listeners.addAll(Arrays.asList(listeners)); - } - - public static void publishMessageEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onMessage(update)).start(); - } - - public static void publishEditedMessageEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onEditedMessage(update)).start(); - } - - public static void publishChannelPostEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onChannelPost(update)).start(); - } - - public static void publishEditedChannelPostEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onEditedChannelPost(update)).start(); - } - - public static void publishInlineQueryEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onInlineQuery(update)).start(); - } - - public static void publishChosenInlineResultEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onChosenInlineResult(update)).start(); - } - - public static void publishCallbackQueryEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onCallbackQuery(update)).start(); - } - - public static void publishShippingQueryEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onShippingQuery(update)).start(); - } - - public static void publishPreCheckoutQueryEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onPreCheckoutQuery(update)).start(); - } - - public static void publishPollEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onPoll(update)).start(); - } - - public static void publishPollAnswerEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onPollAnswer(update)).start(); - } - - public static void publishMyChatMemberUpdatedEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onMyChatMemberUpdated(update)).start(); - } - - public static void publishChatMemberUpdatedEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onChatMemberUpdated(update)).start(); - } - - public static void publishChatJoinRequestEvent (@Nonnull Update update) { - new EventPublisher(update, x -> x.onChatJoinRequest(update)).start(); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java b/src/main/old/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java deleted file mode 100644 index 43f0376..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/api/InlineQueryUnit.java +++ /dev/null @@ -1,36 +0,0 @@ -package cc.sukazyo.cono.morny.bot.api; - -import com.pengrad.telegrambot.model.request.InlineQueryResult; - -public class InlineQueryUnit> { - - public static final int DEFAULT_INLINE_CACHE_TIME = 300; - public static final boolean DEFAULT_INLINE_PERSONAL_RESP = false; - - private int cacheTime = DEFAULT_INLINE_CACHE_TIME; - private boolean isPersonal = DEFAULT_INLINE_PERSONAL_RESP; - public final T result; - - public InlineQueryUnit (T result) { - this.result = result; - } - - public int cacheTime () { - return cacheTime; - } - - public InlineQueryUnit cacheTime (int cacheTime) { - this.cacheTime = cacheTime; - return this; - } - - public boolean isPersonal () { - return isPersonal; - } - - public InlineQueryUnit isPersonal (boolean isPersonal) { - this.isPersonal = isPersonal; - return this; - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/api/OnUpdate.java b/src/main/old/cc/sukazyo/cono/morny/bot/api/OnUpdate.java deleted file mode 100644 index e03653b..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/api/OnUpdate.java +++ /dev/null @@ -1,59 +0,0 @@ -package cc.sukazyo.cono.morny.bot.api; - -import com.pengrad.telegrambot.UpdatesListener; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; -import java.util.List; - -public class OnUpdate { - - public static int onNormalUpdate (@Nonnull List updates) { - for (Update update : updates) { - if (update.message() != null) { - EventListenerManager.publishMessageEvent(update); - } - if (update.editedMessage() != null) { - EventListenerManager.publishEditedMessageEvent(update); - } - if (update.channelPost() != null) { - EventListenerManager.publishChannelPostEvent(update); - } - if (update.editedChannelPost() != null) { - EventListenerManager.publishEditedChannelPostEvent(update); - } - if (update.inlineQuery() != null) { - EventListenerManager.publishInlineQueryEvent(update); - } - if (update.chosenInlineResult() != null) { - EventListenerManager.publishChosenInlineResultEvent(update); - } - if (update.callbackQuery() != null) { - EventListenerManager.publishCallbackQueryEvent(update); - } - if (update.shippingQuery() != null) { - EventListenerManager.publishShippingQueryEvent(update); - } - if (update.preCheckoutQuery() != null) { - EventListenerManager.publishPreCheckoutQueryEvent(update); - } - if (update.poll() != null) { - EventListenerManager.publishPollEvent(update); - } - if (update.pollAnswer() != null) { - EventListenerManager.publishPollAnswerEvent(update); - } - if (update.myChatMember() != null) { - EventListenerManager.publishMyChatMemberUpdatedEvent(update); - } - if (update.chatMember() != null) { - EventListenerManager.publishChatMemberUpdatedEvent(update); - } - if (update.chatJoinRequest() != null) { - EventListenerManager.publishChatJoinRequestEvent(update); - } - } - return UpdatesListener.CONFIRMED_UPDATES_ALL; - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java deleted file mode 100644 index 453a97e..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.java +++ /dev/null @@ -1,19 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public interface ISimpleCommand { - - @Nonnull - String getName(); - - @Nullable - String[] getAliases(); - - void execute (@Nonnull InputCommand command, @Nonnull Update event); - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java deleted file mode 100644 index b090a95..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import javax.annotation.Nonnull; - -public interface ITelegramCommand extends ISimpleCommand { - - @Nonnull - String getParamRule(); - @Nonnull - String getDescription(); - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/command/MornyCommands.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/MornyCommands.java deleted file mode 100644 index d3cf37f..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/command/MornyCommands.java +++ /dev/null @@ -1,297 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.event.OnUniMeowTrigger; -import cc.sukazyo.cono.morny.daemon.MornyReport; -import cc.sukazyo.cono.morny.data.MornyJrrp; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; -import com.pengrad.telegrambot.model.BotCommand; -import com.pengrad.telegrambot.model.DeleteMyCommands; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; -import com.pengrad.telegrambot.request.SetMyCommands; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import java.util.*; - -import static cc.sukazyo.cono.morny.Log.logger; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class MornyCommands { - - private final Map commands = new LinkedHashMap<>(); - - private void pushCommandTo (@Nonnull String name, @Nonnull ISimpleCommand instance) { - if (commands.containsKey(name)) { - logger.warn(String.format(""" - Telegram command instance named "%s" already exists and will be override by another command instance - - current: %s - - new : %s""", - name, - commands.get(name).getClass().getName(), - instance.getClass().getName() - )); - } - commands.put(name, instance); - } - - public void register (@Nonnull ISimpleCommand... list) { - for (ISimpleCommand instance : list) { - final String[] aliases = instance.getAliases(); - pushCommandTo(instance.getName(), instance); - if (aliases!=null) for (String alias : aliases) pushCommandTo(alias, instance); - } - } - - @SuppressWarnings("NonAsciiCharacters") - public MornyCommands () { - - register( - new ON(), - new Hello(), /* new {@link HelloOnStart}, */ - MornyInformation$.MODULE$, - GetUsernameAndId$.MODULE$, - EventHack$.MODULE$, - Nbnhhsh$.MODULE$, - IP186Query.IP$.MODULE$, - IP186Query.Whois$.MODULE$, - Encryptor$.MODULE$, - new SaveData(), - MornyInfoOnHello$.MODULE$, - new Version(), - new MornyRuntime(), - new Jrrp(), - new Exit(), new ExitAlias() - ); - - // 特殊的命令 - register( - Testing$.MODULE$, - DirectMsgClear$.MODULE$ - ); - - // 统一注册这些奇怪的东西&.& - register( - 私わね$.MODULE$, - 喵呜.Progynova$.MODULE$ - ); - // special: 注册出于兼容使用的特别 event 的数据 - OnUniMeowTrigger.register( - 喵呜.抱抱$.MODULE$, - 喵呜.揉揉$.MODULE$, - 喵呜.蹭蹭$.MODULE$, - 喵呜.贴贴$.MODULE$ - ); - - } - - public boolean execute (@Nonnull InputCommand command, @Nonnull Update event) { - if (commands.containsKey(command.getCommand())) { - commands.get(command.getCommand()).execute(command, event); - return true; - } - return nonCommandExecutable(event, command); - } - - public void automaticUpdateList () { - BotCommand[] commandList = getCommandListTelegram(); - automaticRemoveList(); - MornyCoeur.extra().exec(new SetMyCommands( - commandList - )); - logger.info("automatic updated telegram command list :\n" + commandListToString(commandList)); - } - - public void automaticRemoveList () { - MornyCoeur.extra().exec(new DeleteMyCommands()); - logger.info("cleaned up command list."); - } - - private String commandListToString (@Nonnull BotCommand[] list) { - StringBuilder builder = new StringBuilder(); - for (BotCommand signal : list) { - builder.append(signal.command()).append(" - ").append(signal.description()).append("\n"); - } - return builder.substring(0, builder.length()-1); - } - - public BotCommand[] getCommandListTelegram () { - final List telegramFormatListing = new ArrayList<>(); - commands.forEach((regKey, command) -> { - if (command instanceof ITelegramCommand && regKey.equals(command.getName())) { - telegramFormatListing.add(formatTelegramCommandListLine( - command.getName(), - ((ITelegramCommand)command).getParamRule(), - ((ITelegramCommand)command).getDescription() - )); - if (command.getAliases() != null) for (String alias : command.getAliases()) { - telegramFormatListing.add(formatTelegramCommandListLine(alias, "", "↑")); - } - } - }); - return telegramFormatListing.toArray(BotCommand[]::new); - } - - private BotCommand formatTelegramCommandListLine (@Nonnull String commandName, @Nonnull String paramRule, @Nonnull String intro) { - return new BotCommand(commandName, paramRule.isEmpty() ? (intro) : (paramRule+" - "+intro)); - } - - private boolean nonCommandExecutable (Update event, InputCommand command) { - if (command.getTarget() == null) return false; // 无法解析的命令,转交事件链后代处理 - else { // 无法解析的显式命令格式,报错找不到命令 - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_404 - ).replyToMessageId(event.message().messageId()) - ); - return true; - } - } - - /// /// /// /// /// /// /// /// /// - /// - /// Old Simple Command Block - /// - - private static class ON implements ITelegramCommand { - @Nonnull @Override public String getName () { return "o"; } - @Nullable - @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "检查是否在线"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandOnExec(event); } - } - private static void onCommandOnExec (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_ONLINE_STATUS_RETURN - ).replyToMessageId(event.message().messageId()) - ); - } - - private static class Hello implements ITelegramCommand { - @Nonnull @Override public String getName () { return "hello"; } - @Nullable @Override public String[] getAliases () { return new String[]{"hi"}; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "打招呼"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandHelloExec(event); } - } - /** - * {@link Hello} on special command /start - * Deprecated due to new {@link MornyInfoOnHello} - */ - @Deprecated @SuppressWarnings("unused") - private static class HelloOnStart implements ISimpleCommand { @Nonnull @Override public String getName () { return "start"; }@Nullable @Override public String[] getAliases () { return new String[0]; }@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandHelloExec(event); }} - private static void onCommandHelloExec (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_HELLO - ).replyToMessageId(event.message().messageId()) - ); - } - - private static class Exit implements ITelegramCommand { - @Nonnull @Override public String getName () { return "exit"; } - @Nullable @Override public String[] getAliases () { return new String[0]; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "关闭 Bot (仅可信成员)"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandExitExec(event); } - } - private static class ExitAlias implements ISimpleCommand { - @Nonnull @Override public String getName () { return "quit"; } - @Nullable @Override public String[] getAliases () { return new String[]{"stop"}; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandExitExec(event); } - } - private static void onCommandExitExec (@Nonnull Update event) { - if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_EXIT - ).replyToMessageId(event.message().messageId()) - ); - logger.info("Morny exited by user " + TGToString.as(event.message().from()).toStringLogTag()); - MornyCoeur.exit(0, event.message().from()); - } else { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_403 - ).replyToMessageId(event.message().messageId()) - ); - logger.info("403 exited tag from user " + TGToString.as(event.message().from()).toStringLogTag()); - MornyReport.unauthenticatedAction("/exit", event.message().from()); - } - } - - private static class Version implements ISimpleCommand { - @Nonnull @Override public String getName () { return "version"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Deprecated public String getParamRule () { return ""; } - @Nonnull @Deprecated public String getDescription () { return "检查 Bot 版本信息"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformation.echoVersion(event); } - } - - private static class MornyRuntime implements ISimpleCommand { - @Nonnull @Override public String getName () { return "runtime"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Deprecated public String getParamRule () { return ""; } - @Nonnull @Deprecated public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformation.echoRuntime(event); } - } - - private static class Jrrp implements ITelegramCommand { - @Nonnull @Override public String getName () { return "jrrp"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "获取 (假的) jrrp"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandJrrpExec(event); } - } - private static void onCommandJrrpExec (Update event) { - final double jrrp = MornyJrrp.getJrrpFromTelegramUser(event.message().from(), System.currentTimeMillis()); - final String endChar = jrrp>70 ? "!" : jrrp>30 ? ";" : "..."; - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format( - "%s 在(utc的)今天的运气指数是———— %.2f%% %s", - TGToString.as(event.message().from()).fullnameRefHtml(), - jrrp, escapeHtml(endChar) - ) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); - } - - private static class SaveData implements ITelegramCommand { - @Nonnull @Override public String getName () { return "save"; } - @Nullable @Override public String[] getAliases () { return null; } - @Nonnull @Override public String getParamRule () { return ""; } - @Nonnull @Override public String getDescription () { return "保存缓存数据到文件(仅可信成员)"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onSaveDataExec(event); } - } - /** - * @since 0.4.3.0 - */ - private static void onSaveDataExec (Update event) { - if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) { - logger.info("called save from command by " + TGToString.as(event.message().from()).toStringLogTag()); - MornyCoeur.callSaveData(); - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_SAVED - ).replyToMessageId(event.message().messageId()) - ); - } else { - MornyCoeur.extra().exec(new SendSticker( - event.message().chat().id(), - TelegramStickers.ID_403 - ).replyToMessageId(event.message().messageId()) - ); - logger.info("403 call save tag from user " + TGToString.as(event.message().from()).toStringLogTag()); - MornyReport.unauthenticatedAction("/save", event.message().from()); - } - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/EventListeners.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/EventListeners.java deleted file mode 100644 index f60095e..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/EventListeners.java +++ /dev/null @@ -1,39 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.bot.api.EventListenerManager; -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit; - -public class EventListeners { - - public static final OnTelegramCommand COMMANDS_LISTENER = new OnTelegramCommand(); -// public static final OnActivityRecord ACTIVITY_RECORDER = new OnActivityRecord(); - public static final OnUpdateTimestampOffsetLock UPDATE_TIMESTAMP_OFFSET_LOCK = new OnUpdateTimestampOffsetLock(); - public static final OnEventHackHandle EVENT_HACK_HANDLE = new OnEventHackHandle(); -// static final OnKuohuanhuanNeedSleep KUOHUANHUAN_NEED_SLEEP = new OnKuohuanhuanNeedSleep(); - public static final OnCallMsgSend CALL_MSG_SEND = new OnCallMsgSend(); - public static final OnMedicationNotifyApply MEDICATION_NOTIFY_APPLY = new OnMedicationNotifyApply(); - public static final OnRandomlyTriggered RANDOMLY_TRIGGERED = new OnRandomlyTriggered(); - public static final OnUniMeowTrigger UNI_MEOW_TRIGGER = new OnUniMeowTrigger(); - public static final OnQuestionMarkReply QUESTION_MARK_REPLY = new OnQuestionMarkReply(); - - public static void registerAllListeners () { - EventListenerManager.addListener( -// ACTIVITY_RECORDER, - UPDATE_TIMESTAMP_OFFSET_LOCK, - /* write functional event behind here */ -// KUOHUANHUAN_NEED_SLEEP, - COMMANDS_LISTENER, - UNI_MEOW_TRIGGER, - RANDOMLY_TRIGGERED, - OnUserRandom$.MODULE$, - QUESTION_MARK_REPLY, - OnUserSlashAction$.MODULE$, - OnInlineQuery$.MODULE$, - OnCallMe$.MODULE$, - CALL_MSG_SEND, - MEDICATION_NOTIFY_APPLY, - EVENT_HACK_HANDLE - ); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java index 8ad0af4..e099fb7 100644 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java +++ b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java @@ -8,7 +8,7 @@ import com.pengrad.telegrambot.model.Update; import javax.annotation.Nonnull; @Deprecated -public class OnActivityRecord extends EventListener { +public class OnActivityRecord implements EventListener { @Override public boolean onMessage (@Nonnull Update update) { @@ -22,7 +22,7 @@ public class OnActivityRecord extends EventListener { (long)update.message().date() * 1000 ); } - return super.onMessage(update); + return false; } } diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java deleted file mode 100644 index b4eaaec..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java +++ /dev/null @@ -1,222 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.MessageEntity; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.GetChat; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.data.TelegramStickers; -import com.pengrad.telegrambot.response.GetChatResponse; -import com.pengrad.telegrambot.response.SendResponse; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - - -public class OnCallMsgSend extends EventListener { - - private static final Pattern REGEX_MSG_SENDREQ_DATA_HEAD = Pattern.compile("^\\*msg(-?\\d+)(\\*\\S+)?(?:\\n([\\s\\S]+))?$"); - - private record MessageToSend ( - @Nullable String message, - @Nullable MessageEntity[] entities, - @Nullable ParseMode parseMode, - long targetId - ) { } - - @Override - public boolean onMessage(@Nonnull Update update) { - - // 执行体检查 - if (update.message().chat().type() != Chat.Type.Private) return false; - if (update.message().text() == null) return false; - if (!update.message().text().startsWith("*msg")) return false; - - // 权限检查 - if (!MornyCoeur.trustedInstance().isTrusted(update.message().from().id())) { - MornyCoeur.extra().exec(new SendSticker( - update.message().chat().id(), - TelegramStickers.ID_403 - ).replyToMessageId(update.message().messageId())); - return true; - } - - Message msgsendReqRaw; // 用户书写的发送请求原文 - MessageToSend msgsendReqBody; // 解析后的发送请求实例 - - // *msgsend 发送标识 - // 处理发送要求 - if (update.message().text().equals("*msgsend")) { - // 发送体处理 - if (update.message().replyToMessage() == null) return answer404(update); - msgsendReqBody = parseRequest(update.message().replyToMessage()); - if (msgsendReqBody == null || msgsendReqBody.message == null) return answer404(update); - // 执行发送任务 - SendResponse sendResponse = MornyCoeur.getAccount().execute(parseMessageToSend(msgsendReqBody)); - if (!sendResponse.isOk()) { // 发送失败 - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), - String.format(""" - %d FAILED - %s""", - sendResponse.errorCode(), - sendResponse.description() - ) - ).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); - } else { // 发送成功信号 - MornyCoeur.extra().exec(new SendSticker( - update.message().chat().id(), - TelegramStickers.ID_SENT - ).replyToMessageId(update.message().messageId())); - } - return true; - // 发送完成/失败 - 事件结束 - } - - // *msg 检查标识 - if (update.message().text().equals("*msg")) { // 处理对曾经的原文的检查 - if (update.message().replyToMessage() == null) { - return answer404(update); - } - msgsendReqRaw = update.message().replyToMessage(); - } else if (update.message().text().startsWith("*msg")) { // 对接受到的原文进行检查 - msgsendReqRaw = update.message(); - } else { - return answer404(update); // 未定义的动作 - } - - // 对发送请求的用户原文进行解析 - msgsendReqBody = parseRequest(msgsendReqRaw); - if (msgsendReqBody == null) { - return answer404(update); - } - - // 输出发送目标信息 - GetChatResponse targetChatReq = MornyCoeur.getAccount().execute(new GetChat(msgsendReqBody.targetId())); - if (!targetChatReq.isOk()) { - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), - String.format(""" - %d FAILED - %s""", - targetChatReq.errorCode(), - targetChatReq.description() - ) - ).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); - } else { - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), - targetChatReq.chat().type() == Chat.Type.Private ? ( - String.format(""" - %d@%s - 🔒 %s %s""", - msgsendReqBody.targetId(), - escapeHtml(targetChatReq.chat().type().name()), - escapeHtml(targetChatReq.chat().firstName()+(targetChatReq.chat().lastName()==null?"":" "+targetChatReq.chat().lastName())), - targetChatReq.chat().username()==null? - String.format("@@", targetChatReq.chat().id()): - (escapeHtml("@"+targetChatReq.chat().username())) - ) - ) : ( - String.format(""" - %d@%s::: - %s %s%s""", - msgsendReqBody.targetId(), - escapeHtml(targetChatReq.chat().type().name()), - switch (targetChatReq.chat().type()) { - case group -> "💭"; - case channel -> "📢"; - case supergroup -> "💬"; - default -> "⭕️"; - }, - escapeHtml(targetChatReq.chat().title()), - targetChatReq.chat().username() != null?String.format( - " @%s", escapeHtml(targetChatReq.chat().username()) - ):"" - ) - ) - ).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); - } - // 发送文本测试 - if (msgsendReqBody.message == null) return true; - final SendResponse testSendResp = MornyCoeur.getAccount().execute( - parseMessageToSend(msgsendReqBody, update.message().chat().id()).replyToMessageId(update.message().messageId()) - ); - if (!testSendResp.isOk()) { - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), - String.format(""" - %d FAILED - %s""", - testSendResp.errorCode(), - testSendResp.description() - ) - ).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); - } - - return true; - - } - - @Nullable - private static MessageToSend parseRequest (@Nonnull Message requestBody) { - - final Matcher matcher = REGEX_MSG_SENDREQ_DATA_HEAD.matcher(requestBody.text()); - if (matcher.matches()) { - long targetId = Long.parseLong(matcher.group(1)); - ParseMode parseMode = matcher.group(2) == null ? null : switch (matcher.group(2)) { - case "*markdown", "*md", "*m↓" -> ParseMode.MarkdownV2; - case "*md1" -> ParseMode.Markdown; - case "*html" -> ParseMode.HTML; - default -> null; - }; - final int offset = "*msg".length()+matcher.group(1).length()+(matcher.group(2)==null?0:matcher.group(2).length())+1; - final ArrayList entities = new ArrayList<>(); - if (requestBody.entities() != null) for (MessageEntity entity : requestBody.entities()) { - final MessageEntity parsed = new MessageEntity(entity.type(), entity.offset() - offset, entity.length()); - if (entity.url() != null) parsed.url(entity.url()); - if (entity.user() != null) parsed.user(entity.user()); - if (entity.language() != null) parsed.language(entity.language()); - entities.add(parsed); - } - return new MessageToSend(matcher.group(3), entities.toArray(MessageEntity[]::new), parseMode, targetId); - } - - return null; - - } - - @Nonnull - private static SendMessage parseMessageToSend (@Nonnull MessageToSend body) { - return parseMessageToSend(body, body.targetId); - } - - @Nonnull - private static SendMessage parseMessageToSend (@Nonnull MessageToSend body, long targetId) { - SendMessage sendingBody = new SendMessage(targetId, body.message); - if (body.entities != null) sendingBody.entities(body.entities); - if (body.parseMode != null) sendingBody.parseMode(body.parseMode); - return sendingBody; - } - - private static boolean answer404 (@Nonnull Update update) { - MornyCoeur.extra().exec(new SendSticker( - update.message().chat().id(), - TelegramStickers.ID_404 - ).replyToMessageId(update.message().messageId())); - return true; - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java deleted file mode 100644 index ffba9b5..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.java +++ /dev/null @@ -1,145 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape; - -import com.google.gson.GsonBuilder; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; -import java.util.HashMap; -import java.util.Map; - -import static cc.sukazyo.cono.morny.Log.logger; - -/** - * 事件劫持与序列化工具. - * @since 0.4.2.0 - */ -public class OnEventHackHandle extends EventListener { - - /** 事件劫持请求列表 */ - private static final Map hackers = new HashMap<>(); - - /** - * 触发事件劫持的限定条件. - * @since 0.4.2.0 - */ - public enum HackType { - /** 只有相同用户发起的事件才会被触发 */ - USER, - /** 只有相同群组内发生的事件才会触发 */ - GROUP, - /** 任何事件都可以触发 */ - ANY - } - - public record Hacker(long fromChatId, long fromMessageId) { - @Override public String toString() { - return fromChatId + "/" + fromMessageId; - } - } - - /** - * @since 0.4.2.0 - */ - public static void registerHack(long fromMessageId, long fromUserId, long fromChatId, @Nonnull HackType type) { - String rec = null; - switch (type) { - case USER -> rec = String.format("((%d))", fromUserId); - case GROUP -> rec = String.format("{{%d}}", fromChatId); - case ANY -> rec = "[[]]"; - } - hackers.put(rec, new Hacker(fromChatId, fromMessageId)); - logger.debug("add hacker track " + rec); - } - - private boolean onEventHacked (Update update, long chatId, long fromUser) { - logger.debug(String.format("got event signed {{%d}}((%d))", chatId, fromUser)); - Hacker x; - x = hackers.remove(String.format("((%d))", fromUser)); - if (x == null) x = hackers.remove(String.format("{{%d}}", chatId)); - if (x == null) x = hackers.remove("[[]]"); - if (x == null) return false; - logger.debug("hacked event by " + x); - MornyCoeur.extra().exec(new SendMessage(x.fromChatId, String.format( - "%s", - MsgEscape.escapeHtml(new GsonBuilder().setPrettyPrinting().create().toJson(update)) - )).parseMode(ParseMode.HTML).replyToMessageId((int)x.fromMessageId)); - return true; - } - - @Override - public boolean onMessage (@Nonnull Update update) { - return onEventHacked(update, update.message().chat().id(), update.message().from().id()); - } - - @Override - public boolean onEditedMessage (@Nonnull Update update) { - return onEventHacked(update, update.editedMessage().chat().id(), update.editedMessage().from().id()); - } - - @Override - public boolean onChannelPost (@Nonnull Update update) { - return onEventHacked(update, update.channelPost().chat().id(), update.channelPost().chat().id()); - } - - @Override - public boolean onEditedChannelPost (@Nonnull Update update) { - return onEventHacked(update, update.editedChannelPost().chat().id(), update.editedChannelPost().chat().id()); - } - - @Override - public boolean onInlineQuery (@Nonnull Update update) { - return onEventHacked(update, 0, update.inlineQuery().from().id()); - } - - @Override - public boolean onChosenInlineResult (@Nonnull Update update) { - return onEventHacked(update, 0, update.chosenInlineResult().from().id()); - } - - @Override - public boolean onCallbackQuery (@Nonnull Update update) { - return onEventHacked(update, 0, update.callbackQuery().from().id()); - } - - @Override - public boolean onShippingQuery (@Nonnull Update update) { - return onEventHacked(update, 0, update.shippingQuery().from().id()); - } - - @Override - public boolean onPreCheckoutQuery (@Nonnull Update update) { - return onEventHacked(update, 0, update.preCheckoutQuery().from().id()); - } - - @Override - public boolean onPoll (@Nonnull Update update) { - return onEventHacked(update, 0, 0); - } - - @Override - public boolean onPollAnswer (@Nonnull Update update) { - return onEventHacked(update, 0, update.pollAnswer().user().id()); - } - - @Override - public boolean onMyChatMemberUpdated (@Nonnull Update update) { - return onEventHacked(update, update.myChatMember().chat().id(), update.myChatMember().from().id()); - } - - @Override - public boolean onChatMemberUpdated (@Nonnull Update update) { - return onEventHacked(update, update.chatMember().chat().id(), update.chatMember().from().id()); - } - - @Override - public boolean onChatJoinRequest (@Nonnull Update update) { - return onEventHacked(update, update.chatJoinRequest().chat().id(), update.chatJoinRequest().from().id()); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java index 2a51a67..8af5bc5 100644 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java +++ b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java @@ -11,7 +11,7 @@ import java.util.GregorianCalendar; import java.util.Locale; @Deprecated -public class OnKuohuanhuanNeedSleep extends EventListener { +public class OnKuohuanhuanNeedSleep implements EventListener { @Override public boolean onMessage (@Nonnull Update update) { diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java deleted file mode 100644 index 59ec01c..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.java +++ /dev/null @@ -1,29 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.daemon.MornyDaemons; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; - -public class OnMedicationNotifyApply extends EventListener { - - @Override - public boolean onEditedChannelPost (@Nonnull Update update) { - return editedMessageProcess(update.editedChannelPost()); - } - - @Override - public boolean onEditedMessage (@Nonnull Update update) { - return editedMessageProcess(update.editedMessage()); - } - - private boolean editedMessageProcess (Message edited) { - if (edited.chat().id() != MornyCoeur.config().medicationNotifyToChat) return false; - MornyDaemons.medicationTimerInstance.refreshNotificationWrite(edited); - return true; - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java deleted file mode 100644 index 8e51eeb..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.java +++ /dev/null @@ -1,38 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.SendMessage; - -import javax.annotation.Nonnull; -import java.util.Set; - -import static cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue; - -public class OnQuestionMarkReply extends EventListener { - - /** - * 一个 unicode 的问号字符列表. 不仅有半角全角问号,也包含了变体问号,和叹号结合的问好以及 uni-emoji 问号。 - * @since 1.0.0-RC3.2 - */ - public static final Set QUESTION_MARKS = Set.of('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓'); - - @Override - public boolean onMessage (@Nonnull Update update) { - - if (update.message().text() == null) return false; - - if (!probabilityTrue(8)) return false; - for (char c : update.message().text().toCharArray()) { - if (!QUESTION_MARKS.contains(c)) return false; - } - - MornyCoeur.extra().exec(new SendMessage( - update.message().chat().id(), update.message().text() - ).replyToMessageId(update.message().messageId())); - return true; - - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java index 5481a08..24c8c2b 100644 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java +++ b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java @@ -2,7 +2,8 @@ package cc.sukazyo.cono.morny.bot.event; import cc.sukazyo.cono.morny.bot.api.EventListener; -public class OnRandomlyTriggered extends EventListener { +@Deprecated +public class OnRandomlyTriggered implements EventListener { // /** // * function CODE_IK0XA1 diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java deleted file mode 100644 index a74e7a5..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; - -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class OnTelegramCommand extends EventListener { - - @Override - public boolean onMessage (@Nonnull Update event) { - if (event.message().text() == null || !event.message().text().startsWith("/") || event.message().text().startsWith("/ ")) { - logger.debug("not command"); - return false; // 检测到非(命令格式)文本,忽略掉命令处理 - } - final InputCommand command = new InputCommand(event.message().text().substring(1)); - if (!command.getCommand().matches("^\\w+$")) { logger.debug("not command");return false; } - logger.debug("is command"); - if (command.getTarget() != null && !MornyCoeur.getUsername().equals(command.getTarget())) { - return true; // 检测到命令并非针对 morny,退出整个事件处理链 - } - return MornyCoeur.commandManager().execute(command, event); // 转交命令管理器执行命令 - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java deleted file mode 100644 index 59e8be1..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.java +++ /dev/null @@ -1,36 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.bot.command.ISimpleCommand; -import cc.sukazyo.cono.morny.util.tgapi.InputCommand; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -public class OnUniMeowTrigger extends EventListener { - - private static final Map triggers = new HashMap<>(); - - public static void register (ISimpleCommand... list) { - for (ISimpleCommand cmd : list) - triggers.put(cmd.getName(), cmd); - } - - @Override - public boolean onMessage (@Nonnull Update event) { - if (event.message().text() == null) return false; - AtomicBoolean ok = new AtomicBoolean(false); - triggers.forEach((name, command) -> { - name = "/" + name; - if (name.equals(event.message().text())) { - command.execute(new InputCommand(name), event); - ok.set(true); - } - }); - return ok.get(); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java deleted file mode 100644 index faee77f..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.java +++ /dev/null @@ -1,56 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; - -/** - * 阻止 {@link cc.sukazyo.cono.morny.MornyConfig#eventOutdatedTimestamp 指定时间} 之前的事件处理. - *

      - * 只支持以下事件 - *

        - *
      • {@link EventListener#onMessage(Update) 收到消息}
      • - *
      • {@link EventListener#onEditedMessage(Update) 消息被更新}
      • - *
      • {@link EventListener#onChannelPost(Update) 收到频道消息}
      • - *
      • {@link EventListener#onEditedChannelPost(Update) 频道消息被更新}
      • - *
      - * @see #isOutdated 时间判断 - */ -public class OnUpdateTimestampOffsetLock extends EventListener { - - /** - * 检查传入时间是否在要求时间之前(即"过期"). - * @param timestamp 传入时间,秒级 - * @return 如果传入时间在要求时间之前,返回true,反之false - * @since 0.4.2.7 - */ - public boolean isOutdated(long timestamp) { - return timestamp < MornyCoeur.config().eventOutdatedTimestamp/1000; - } - - @Override - public boolean onMessage (@Nonnull Update update) { - return isOutdated(update.message().date()); - } - - /** @since 0.4.2.6 */ - @Override - public boolean onEditedMessage (@Nonnull Update update) { - return isOutdated(update.editedMessage().editDate()); - } - - /** @since 0.4.2.6 */ - @Override - public boolean onChannelPost (@Nonnull Update update) { - return isOutdated(update.channelPost().date()); - } - - /** @since 0.4.2.6 */ - @Override - public boolean onEditedChannelPost (@Nonnull Update update) { - return isOutdated(update.editedChannelPost().editDate()); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java b/src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java index 9a81a5e..8f49964 100644 --- a/src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java +++ b/src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java @@ -37,7 +37,7 @@ public class MornyReport { } } - public static void exception (@Nonnull Exception e, @Nullable String description) { + public static void exception (@Nonnull Throwable e, @Nullable String description) { if (unsupported()) return; executeReport(new SendMessage( MornyCoeur.config().reportToChat, diff --git a/src/main/old/cc/sukazyo/cono/morny/data/MornyJrrp.java b/src/main/old/cc/sukazyo/cono/morny/data/MornyJrrp.java deleted file mode 100644 index 029e476..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/data/MornyJrrp.java +++ /dev/null @@ -1,46 +0,0 @@ -package cc.sukazyo.cono.morny.data; - -import cc.sukazyo.cono.morny.util.CommonConvert; -import cc.sukazyo.cono.morny.util.CommonEncrypt; -import com.pengrad.telegrambot.model.User; - -/** - * Morny 的 jrrp 运算类. - * - * @see #getJrrpFromTelegramUser(User,long) - * @see #calcJrrpXmomi(long,long) - * @since 0.4.2.9 - */ -public class MornyJrrp { - - /** - * 通过 telegram 用户和时间戳作为参数获取 jrrp. - * - * @see #calcJrrpXmomi 当前版本的实现算法 {@code Xmomi} - * @since 0.4.2.9 - * @param user telegram 用户 - * @param timestamp 时间戳 - * @return 通过当前版本的算法计算出的用户 jrrp 值,取值为 {@code [0.00, 100.00]} - */ - public static double getJrrpFromTelegramUser (User user, long timestamp) { - return calcJrrpXmomi(user.id(), timestamp / (1000 * 60 * 60 * 24)) * 100.0; - } - - /** - * {@code Xmomi} 版本的 jrrp 算法. - *

      - * 算法规则为,将用户id与日期戳链接为 uid@daystamp 这样的字符串, - * 然后通过 MD5 计算出字符串的哈希值,取哈希值前4个字节,将其作为16进制数值表示法转换为取值为 {@code [0x0000, 0xffff]} 的数值, - * 得到的数值除以区间最大值 {@code 0xffff} 即可得到一个分布在 {@code [0.0, 1.0]} 之间的分布值, - * 这个分布值乘以 {@code 100.0},即为计算得到的 jrrp 数值。 - * - * @since 0.4.2.9 - * @param userId telegram 用户 uid - * @param dayStamp unix 时间戳转换为日期单位后的数值. 数值应该在转换前转换时区 - * @return 算法得到的 jrrp 值,取值为 {@code [0.00. 100.00]} - */ - public static double calcJrrpXmomi (long userId, long dayStamp) { - return (double)Long.parseLong(CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(userId + "@" + dayStamp)).substring(0, 4), 16) / (double)0xffff; - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/data/NbnhhshQuery.java b/src/main/old/cc/sukazyo/cono/morny/data/NbnhhshQuery.java deleted file mode 100644 index 2ed6657..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/data/NbnhhshQuery.java +++ /dev/null @@ -1,47 +0,0 @@ -package cc.sukazyo.cono.morny.data; - -import java.io.IOException; - -import com.google.gson.Gson; - -import cc.sukazyo.cono.morny.util.OkHttpPublic.MediaTypes; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; - -public class NbnhhshQuery { - - public static class Word { - public String name; - public String[] trans; - public String[] inputting; - } - - public static class GuessResult { - public Word[] words; - } - - public record GuessReq (String text) {} - - public static final String API_URL = "https://lab.magiconch.com/api/nbnhhsh/"; - public static final String API_GUESS_METHOD = "guess/"; - - private static final OkHttpClient httpClient = new OkHttpClient(); - - public static GuessResult sendGuess (String text) throws IOException { - final String reqJsonText = new Gson().toJson(new GuessReq(text)); - Request request = new Request.Builder() - .url(API_URL + API_GUESS_METHOD) - .post(RequestBody.create(reqJsonText, MediaTypes.JSON)) - .build(); - try (Response response = httpClient.newCall(request).execute()) { - final ResponseBody body = response.body(); - if (body == null) throw new IOException("Null body."); - final String x = "{ \"words\": " + body.string() + " }"; - return new Gson().fromJson(x, GuessResult.class); - } - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java index 46045f0..e327279 100644 --- a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java +++ b/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java @@ -1,10 +1,15 @@ package cc.sukazyo.cono.morny.util.tgapi.formatting; import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Message; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; public class TGToStringFromChat { + + public static final long MASK_BOTAPI_ID = -1000000000000L; + private final Chat data; public TGToStringFromChat(Chat chat) { @@ -20,4 +25,36 @@ public class TGToStringFromChat { (String.format("%s {%s}[%d]", data.title(), data.username(), data.id())); } + @Nonnull + public String getSafeName () { + if (data.type() == Chat.Type.Private) + return data.firstName() + (data.lastName()==null ? "" : " "+data.lastName()); + else return data.title(); + } + + @Nullable + public String getSafeLinkHTML () { + if (data.username() == null) { + if (data.type() == Chat.Type.Private) + // language=html + return String.format("@[u:%d]", data.id(), data.id()); + // language=html + else return String.format("@[c/%d]", id_tdLib(), id_tdLib()); + } else return "@"+data.username(); + } + + public long id_tdLib () { + return data.id() < 0 ? Math.abs(data.id() - MASK_BOTAPI_ID) : data.id(); + } + + @Nonnull + public String getTypeTag () { + return switch (data.type()) { + case Private -> "🔒"; + case group -> "💭"; + case supergroup -> "💬"; + case channel -> "📢"; + }; + } + } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala new file mode 100644 index 0000000..67b7047 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala @@ -0,0 +1,22 @@ +package cc.sukazyo.cono.morny.bot.api + +import com.pengrad.telegrambot.model.Update + +trait EventListener { + + def onMessage (using Update): Boolean = false + def onEditedMessage (using Update): Boolean = false + def onChannelPost (using Update): Boolean = false + def onEditedChannelPost (using Update): Boolean = false + def onInlineQuery (using Update): Boolean = false + def onChosenInlineResult (using Update): Boolean = false + def onCallbackQuery (using Update): Boolean = false + def onShippingQuery (using Update): Boolean = false + def onPreCheckoutQuery (using Update): Boolean = false + def onPoll (using Update): Boolean = false + def onPollAnswer (using Update): Boolean = false + def onMyChatMemberUpdated (using Update): Boolean = false + def onChatMemberUpdated (using Update): Boolean = false + def onChatJoinRequest (using Update): Boolean = false + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala new file mode 100644 index 0000000..9eed851 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala @@ -0,0 +1,84 @@ +package cc.sukazyo.cono.morny.bot.api + +import cc.sukazyo.cono.morny.Log +import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.daemon.MornyReport +import com.google.gson.GsonBuilder +import com.pengrad.telegrambot.model.Update + +import scala.collection.mutable +import scala.language.postfixOps + +object EventListenerManager { + + private val listeners = mutable.Queue.empty[EventListener] + + def register (listeners: EventListener*): Unit = + this.listeners ++= listeners + + private class EventRunner (using event: Update) extends Thread { + this setName s"evt-${event.updateId()}-nn" + private def updateThreadName (t: String): Unit = + this setName s"evt-${event.updateId()}-$t" + + override def run (): Unit = { + for (i <- listeners) { + object status: + var _status = 0 + def isOk: Boolean = _status > 0 + def check (u: Boolean): Unit = if u then _status = _status + 1 + try { + updateThreadName("message") + if event.message ne null then status check i.onMessage + updateThreadName("edited-message") + if event.editedMessage ne null then status check i.onEditedMessage + updateThreadName("channel-post") + if event.channelPost ne null then status check i.onChannelPost + updateThreadName("edited-channel-post") + if event.editedChannelPost ne null then status check i.onEditedChannelPost + updateThreadName("inline-query") + if event.inlineQuery ne null then status check i.onInlineQuery + updateThreadName("chosen-inline-result") + if event.chosenInlineResult ne null then status check i.onChosenInlineResult + updateThreadName("callback-query") + if event.callbackQuery ne null then status check i.onCallbackQuery + updateThreadName("shipping-query") + if event.shippingQuery ne null then status check i.onShippingQuery + updateThreadName("pre-checkout-query") + if event.preCheckoutQuery ne null then status check i.onPreCheckoutQuery + updateThreadName("poll") + if event.poll ne null then status check i.onPoll + updateThreadName("poll-answer") + if event.pollAnswer ne null then status check i.onPollAnswer + updateThreadName("my-chat-member") + if event.myChatMember ne null then status check i.onMyChatMemberUpdated + updateThreadName("chat-member") + if event.chatMember ne null then status check i.onChatMemberUpdated + updateThreadName("chat-join-request") + if event.chatJoinRequest ne null then status check i.onChatJoinRequest + } catch case e => { + val errorMessage = StringBuilder() + errorMessage ++= "Event throws unexpected exception:\n" + errorMessage ++= (exceptionLog(e) indent 4) + e match + case actionFailed: EventRuntimeException.ActionFailed => + errorMessage ++= "\ntg-api action: response track: " + errorMessage ++= (GsonBuilder().setPrettyPrinting().create().toJson( + actionFailed.getResponse + ) indent 4) ++= "\n" + case _ => + logger error errorMessage.toString + MornyReport.exception(e, "on event running") + } + if (status isOk) return + } + } + + } + + def publishUpdate (using Update): Unit = { + EventRunner().start() + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala new file mode 100644 index 0000000..83c2101 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala @@ -0,0 +1,17 @@ +package cc.sukazyo.cono.morny.bot.api + +import com.pengrad.telegrambot.UpdatesListener +import com.pengrad.telegrambot.model.Update + +import java.util +import scala.jdk.CollectionConverters.* + +object TelegramUpdatesListener extends UpdatesListener { + + override def process (updates: util.List[Update]): Int = { + for (update <- updates.asScala) + EventListenerManager.publishUpdate(using update) + UpdatesListener.CONFIRMED_UPDATES_ALL + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala index e3e1761..a2e38b7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala @@ -11,10 +11,10 @@ import scala.language.postfixOps object DirectMsgClear extends ISimpleCommand { - override def getName: String = "r" - override def getAliases: Array[String] = null + override val name: String = "r" + override val aliases: Array[ICommandAlias] | Null = null - override def execute (command: InputCommand, event: Update): Unit = { + override def execute (using command: InputCommand, event: Update): Unit = { logger debug "executing command /r" if (event.message.replyToMessage == null) return; diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala index 77be888..8bb3987 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala @@ -18,12 +18,12 @@ import scala.language.postfixOps object Encryptor extends ITelegramCommand { - override def getName: String = "encrypt" - override def getAliases: Array[String] = null - override def getParamRule: String = "[algorithm|(l)] [(uppercase)]" - override def getDescription: String = "通过指定算法加密回复的内容 (目前只支持文本)" + override val name: String = "encrypt" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "[algorithm|(l)] [(uppercase)]" + override val description: String = "通过指定算法加密回复的内容 (目前只支持文本)" - override def execute (command: InputCommand, event: Update): Unit = { + override def execute (using command: InputCommand, event: Update): Unit = { val args = command.getArgs diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala index a4fddb0..6118412 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala @@ -11,12 +11,12 @@ import scala.language.postfixOps object EventHack extends ITelegramCommand { - override def getName: String = "event_hack" - override def getAliases: Array[String] = null - override def getParamRule: String = "[(user|group|any)]" - override def getDescription: String = "输出 bot 下一个获取到的事件序列化数据" + override val name: String = "event_hack" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "[(user|group|any)]" + override val description: String = "输出 bot 下一个获取到的事件序列化数据" - override def execute (command: InputCommand, event: Update): Unit = { + override def execute (using command: InputCommand, event: Update): Unit = { val x_mode = if (command.hasArgs) command.getArgs()(0) else "" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala index 3f03650..f4c414b 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala @@ -1,7 +1,8 @@ package cc.sukazyo.cono.morny.bot.command + import cc.sukazyo.cono.morny.MornyCoeur -import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation import cc.sukazyo.cono.morny.util.tgapi.{InputCommand, Standardize} +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{GetChatMember, SendMessage} @@ -10,12 +11,12 @@ import scala.language.postfixOps object GetUsernameAndId extends ITelegramCommand { - override def getName: String = "user" - override def getAliases: Array[String] = Array() - override def getParamRule: String = "[userid]" - override def getDescription: String = "获取指定或回复的用户相关信息" + override val name: String = "user" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "[userid]" + override val description: String = "获取指定或回复的用户相关信息" - override def execute (command: InputCommand, event: Update): Unit = { + override def execute (using command: InputCommand, event: Update): Unit = { val args = command.getArgs @@ -49,7 +50,7 @@ object GetUsernameAndId extends ITelegramCommand { val user = response.chatMember.user - if (user.id eq Standardize.CHANNEL_SPEAKER_MAGIC_ID) + if (user.id == Standardize.CHANNEL_SPEAKER_MAGIC_ID) MornyCoeur.extra exec SendMessage( event.message.chat.id, "$__channel_identify" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala new file mode 100644 index 0000000..749f550 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala @@ -0,0 +1,18 @@ +package cc.sukazyo.cono.morny.bot.command + +trait ICommandAlias { + + val name: String + val listed: Boolean + +} + +object ICommandAlias { + + case class ListedAlias (name: String) extends ICommandAlias: + override val listed = true + + case class HiddenAlias (name: String) extends ICommandAlias: + override val listed = false + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala index a901580..5d78ea1 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala @@ -16,19 +16,19 @@ object IP186Query { case WHOIS extends Subs("whois") object IP extends ITelegramCommand: - override def getName: String = "ip" - override def getAliases: Array[String] = null - override def getParamRule: String = "[ip]" - override def getDescription: String = "通过 https://ip.186526.xyz 查询 ip 资料" - override def execute (command: InputCommand, event: Update): Unit = query(event, command) + override val name: String = "ip" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "[ip]" + override val description: String = "通过 https://ip.186526.xyz 查询 ip 资料" + override def execute (using command: InputCommand, event: Update): Unit = query object Whois extends ITelegramCommand: - override def getName: String = "whois" - override def getAliases: Array[String] = null - override def getParamRule: String = "[domain]" - override def getDescription: String = "通过 https://ip.186526.xyz 查询域名资料" - override def execute (command: InputCommand, event: Update): Unit = query(event, command) + override val name: String = "whois" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "[domain]" + override val description: String = "通过 https://ip.186526.xyz 查询域名资料" + override def execute (using command: InputCommand, event: Update): Unit = query - private def query (event: Update, command: InputCommand): Unit = { + private def query (using event: Update, command: InputCommand): Unit = { val target: String|Null = if (command.getArgs isEmpty) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala new file mode 100644 index 0000000..25181bd --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala @@ -0,0 +1,13 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update + +trait ISimpleCommand { + + val name: String + val aliases: Array[ICommandAlias]|Null + + def execute (using command: InputCommand, event: Update): Unit + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala new file mode 100644 index 0000000..2c16263 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala @@ -0,0 +1,8 @@ +package cc.sukazyo.cono.morny.bot.command + +trait ITelegramCommand extends ISimpleCommand { + + val paramRule: String + val description: String + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala new file mode 100644 index 0000000..7fa9876 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala @@ -0,0 +1,111 @@ +package cc.sukazyo.cono.morny.bot.command + +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import com.pengrad.telegrambot.model.{BotCommand, DeleteMyCommands, Update} +import com.pengrad.telegrambot.request.{SendSticker, SetMyCommands} + +import scala.collection.{mutable, SeqMap} +import scala.collection.mutable.ArrayBuffer +import scala.language.postfixOps +import cc.sukazyo.cono.morny.Log.logger + +object MornyCommands { + + private type CommandMap = SeqMap[String, ISimpleCommand] + private def CommandMap (commands: ISimpleCommand*): CommandMap = + val stash: mutable.SeqMap[String, ISimpleCommand] = mutable.SeqMap() + for (i <- commands) stash += ((i.name, i)) + stash + + private val commands: CommandMap = CommandMap( + + MornyHellos.On, + MornyHellos.On, + MornyHellos.Hello, + MornyInfoOnStart, + GetUsernameAndId, + EventHack, + Nbnhhsh, + IP186Query.IP, + IP186Query.Whois, + Encryptor, + MornyManagers.SaveData, + MornyInformation, + MornyInformationOlds.Version, + MornyInformationOlds.Runtime, + MornyOldJrrp, + MornyManagers.Exit, + + Testing, + DirectMsgClear, + + 私わね, + 喵呜.Progynova + + ) + + @SuppressWarnings(Array("NonAsciiCharacters")) + val commands_uni: CommandMap = CommandMap( + 喵呜.抱抱, + 喵呜.揉揉, + 喵呜.贴贴, + 喵呜.蹭蹭 + ) + + def execute (using command: InputCommand, event: Update): Boolean = { + if (commands contains command.getCommand) + commands(command.getCommand) execute; + true + else nonCommandExecutable + } + + private def nonCommandExecutable (using command: InputCommand, event: Update): Boolean = { + if command.getTarget eq null then false + else + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + true + } + + def automaticTGListUpdate (): Unit = { + val listing = commands_toTelegramList + automaticTGListRemove() + MornyCoeur.extra exec SetMyCommands(listing:_*) + logger info + s"""automatic updated telegram command list : + |${commandsTelegramList_toString(listing)}""".stripMargin + } + + def automaticTGListRemove (): Unit = { + MornyCoeur.extra exec DeleteMyCommands() + logger info "cleaned up command list" + } + + private def commandsTelegramList_toString (list: Array[BotCommand]): String = + val builder = StringBuilder() + for (single <- list) + builder ++= s"${single.command} - ${single.description}\n" + (builder dropRight 1) toString + + private def commands_toTelegramList: Array[BotCommand] = + val list = ArrayBuffer.empty[BotCommand] + for ((name, command) <- commands) command match + case telegramCommand: ITelegramCommand if name == command.name => + list ++= formatTelegramCommandListLine(telegramCommand) + case _ => + list toArray + + private def formatTelegramCommandListLine (command: ITelegramCommand): Array[BotCommand] = + def buildOne (name: String, paramRule: String, intro: String): BotCommand = + BotCommand(name, if paramRule isBlank then intro else s"$paramRule - $intro") + val list = mutable.ArrayBuffer[BotCommand]( + buildOne(command.name, command.paramRule, command.description)) + if (command.aliases ne null) for (alias <- command.aliases) + if (alias.listed) list += buildOne(alias.name, "", "↑") + list toArray + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala new file mode 100644 index 0000000..ea310dc --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala @@ -0,0 +1,43 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.command.ICommandAlias.ListedAlias +import cc.sukazyo.cono.morny.data.TelegramStickers +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendSticker + +import scala.language.postfixOps + +object MornyHellos { + + object On extends ITelegramCommand { + + override val name: String = "on" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "" + override val description: String = "检查是否在线" + + override def execute (using command: InputCommand, event: Update): Unit = + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_ONLINE_STATUS_RETURN + ).replyToMessageId(event.message.messageId) + + } + + object Hello extends ITelegramCommand { + + override val name: String = "hello" + override val aliases: Array[ICommandAlias] | Null = Array(ListedAlias("hi")) + override val paramRule: String = "" + override val description: String = "打招呼" + + override def execute (using command: InputCommand, event: Update): Unit = + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_HELLO + ).replyToMessageId(event.message.messageId) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala similarity index 79% rename from src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.scala rename to src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala index 2df1ba5..8a9b101 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnHello.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala @@ -8,12 +8,12 @@ import com.pengrad.telegrambot.request.SendPhoto import scala.language.postfixOps -object MornyInfoOnHello extends ISimpleCommand { +object MornyInfoOnStart extends ISimpleCommand { - override def getName: String = "start" - override def getAliases: Array[String] = Array() + override val name: String = "start" + override val aliases: Array[ICommandAlias] | Null = null - override def execute (command: InputCommand, event: Update): Unit = { + override def execute (using command: InputCommand, event: Update): Unit = { MornyCoeur.extra exec new SendPhoto( event.message.chat.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala index e81a9fb..9b3dae3 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -23,12 +23,12 @@ object MornyInformation extends ITelegramCommand { val VERSION_2 = "v" } - override def getName: String = "info" - override def getAliases: Array[String] = Array() - override def getParamRule: String = "[(version|runtime|stickers[.IDs])]" - override def getDescription: String = "输出当前 Morny 的各种信息" + override val name: String = "info" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "[(version|runtime|stickers[.IDs])]" + override val description: String = "输出当前 Morny 的各种信息" - override def execute (command: InputCommand, event: Update): Unit = { + override def execute (using command: InputCommand, event: Update): Unit = { if (!command.hasArgs) { echoInfo(event.message.chat.id, event.message.messageId) @@ -38,10 +38,10 @@ object MornyInformation extends ITelegramCommand { val action: String = command.getArgs()(0) action match { - case Subs.STICKERS => echoStickers(command, event) - case Subs.RUNTIME => echoRuntime(event) - case Subs.VERSION | Subs.VERSION_2 => echoVersion(event) - case _ => echo404(event) + case Subs.STICKERS => echoStickers + case Subs.RUNTIME => echoRuntime + case Subs.VERSION | Subs.VERSION_2 => echoVersion + case _ => echo404 } } @@ -92,11 +92,11 @@ object MornyInformation extends ITelegramCommand { ).parseMode(ParseMode HTML).replyToMessageId(replyTo) } - private def echoStickers (command: InputCommand, event: Update): Unit = { + private def echoStickers (using command: InputCommand, event: Update): Unit = { val chat = event.message.chat.id val replyTo = event.message.messageId var sid: String|Null = null - if (command.getArgs()(0) eq Subs.STICKERS) { + if (command.getArgs()(0) == Subs.STICKERS) { if (command.getArgs.length == 1) sid = "" else if (command.getArgs.length == 2) sid = command.getArgs()(1) } else if (command.getArgs.length == 1) { @@ -104,7 +104,7 @@ object MornyInformation extends ITelegramCommand { sid = command.getArgs()(0) substring Subs.STICKERS.length+1 } } - if (sid == null) echo404(event) + if (sid == null) echo404 else echoStickers(sid, chat, replyTo) } @@ -113,11 +113,12 @@ object MornyInformation extends ITelegramCommand { else TelegramStickers echoStickerByID(sid, MornyCoeur.extra, send_chat, send_replyTo) } - private[command] def echoVersion (event: Update): Unit = { + private[command] def echoVersion (using event: Update): Unit = { val versionDeltaHTML = if (MornySystem.isUseDelta) s"-δ${h(MornySystem.VERSION_DELTA)}" else "" val versionGitHTML = if (MornySystem.isGitBuild) s"git $getVersionGitTagHTML" else "" MornyCoeur.extra exec new SendMessage( event.message.chat.id, + // language=html s"""version: |- Morny ${h(MornySystem.CODENAME toUpperCase)} |- ${h(MornySystem.VERSION_BASE)}$versionDeltaHTML${if (MornySystem.isGitBuild) "\n- " + versionGitHTML else ""} @@ -130,11 +131,11 @@ object MornyInformation extends ITelegramCommand { ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) } - private[command] def echoRuntime (event: Update): Unit = { + private[command] def echoRuntime (using event: Update): Unit = { def sysprop (p: String): String = System.getProperty(p) MornyCoeur.extra exec new SendMessage( event.message.chat.id, - /* html */ + /* language=html */ s"""system: |- Morny ${h(if (getRuntimeHostname == null) "" else getRuntimeHostname)} |- ${h(sysprop("os.name"))} ${h(sysprop("os.arch"))} ${h(sysprop("os.version"))} @@ -158,7 +159,7 @@ object MornyInformation extends ITelegramCommand { ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) } - private def echo404 (event: Update): Unit = + private def echo404 (using event: Update): Unit = MornyCoeur.extra exec new SendSticker( event.message.chat.id, TelegramStickers ID_404 diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala new file mode 100644 index 0000000..6f44f16 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala @@ -0,0 +1,17 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update + +object MornyInformationOlds { + + object Version extends ISimpleCommand: + override val name: String = "version" + override val aliases: Array[ICommandAlias] | Null = null + override def execute (using command: InputCommand, event: Update): Unit = MornyInformation.echoVersion + + object Runtime extends ISimpleCommand: + override val name: String = "runtime" + override val aliases: Array[ICommandAlias] | Null = null + override def execute (using command: InputCommand, event: Update): Unit = MornyInformation.echoRuntime + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala new file mode 100644 index 0000000..7218dc9 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala @@ -0,0 +1,86 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.bot.command.ICommandAlias.HiddenAlias +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendSticker + +import scala.language.postfixOps +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.daemon.MornyReport + +object MornyManagers { + + object Exit extends ITelegramCommand { + + override val name: String = "exit" + override val aliases: Array[ICommandAlias] | Null = Array(HiddenAlias("stop"), HiddenAlias("quit")) + override val paramRule: String = "exit" + override val description: String = "关闭 Bot (仅可信成员)" + + override def execute (using command: InputCommand, event: Update): Unit = { + + val user = event.message.from + + if (MornyCoeur.trustedInstance isTrusted user.id) { + + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_EXIT + ).replyToMessageId(event.message.messageId) + logger info s"Morny exited by user ${(TGToString as user) toStringLogTag}" + MornyCoeur.exit(0, user) + + } else { + + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(event.message.messageId) + logger info s"403 exit caught from user ${(TGToString as user) toStringLogTag}" + MornyReport.unauthenticatedAction("/exit", user) + + } + + } + + } + + object SaveData extends ITelegramCommand { + + override val name: String = "save" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "" + override val description: String = "保存缓存数据到文件(仅可信成员)" + + override def execute (using command: InputCommand, event: Update): Unit = { + + val user = event.message.from + + if (MornyCoeur.trustedInstance isTrusted user.id) { + + logger info s"call save from command by ${(TGToString as user) toStringLogTag}" + MornyCoeur.callSaveData() + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_SAVED + ).replyToMessageId(event.message.messageId) + + } else { + + MornyCoeur.extra exec SendSticker( + event.message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(event.message.messageId) + logger info s"403 save caught from user ${(TGToString as user) toStringLogTag}" + MornyReport.unauthenticatedAction("/save", user) + + } + + } + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala new file mode 100644 index 0000000..23fa2fa --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala @@ -0,0 +1,36 @@ +package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update +import cc.sukazyo.cono.morny.data.MornyJrrp +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps +object MornyOldJrrp extends ITelegramCommand { + + override val name: String = "jrrp" + override val aliases: Array[ICommandAlias] | Null = null + override val paramRule: String = "" + override val description: String = "获取 (假的) jrrp" + + override def execute (using command: InputCommand, event: Update): Unit = { + + val user = event.message.from + val jrrp = MornyJrrp.jrrp_of_telegramUser(user, System.currentTimeMillis) + val ending = jrrp match + case s if s > 70 => "!" + case a if a > 30 => ";" + case _ => "..." + + import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + MornyCoeur.extra exec SendMessage( + event.message.chat.id, + // language=html + f"${(TGToString as user) fullnameRefHtml} 在(utc的)今天的运气指数是———— $jrrp%.2f%%${h(ending)}" + ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala index 5c7d799..80c0c46 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala @@ -13,14 +13,16 @@ import scala.language.postfixOps object Nbnhhsh extends ITelegramCommand { - private val NBNHHSH_RESULT_HEAD_HTML = "## Result of nbnhhsh query :" + private val NBNHHSH_RESULT_HEAD_HTML = + // language=html + "## Result of nbnhhsh query :" - override def getName: String = "nbnhhsh" - override def getAliases: Array[String]|Null = null - override def getParamRule: String = "[text]" - override def getDescription: String = "检索文本内 nbnhhsh 词条" + override val name: String = "nbnhhsh" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "[text]" + override val description: String = "检索文本内 nbnhhsh 词条" - override def execute (command: InputCommand, event: Update): Unit = { + override def execute (using command: InputCommand, event: Update): Unit = { val queryTarget: String|Null = import cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting @@ -47,17 +49,17 @@ object Nbnhhsh extends ITelegramCommand { logger debug s"**xx len=${queryResp.words.length}" for (_word <- queryResp.words) { logger debug "**exec" - if ((_word.trans ne null) && (_word.trans isEmpty)) _word.trans = null - if ((_word.inputting ne null) && (_word.inputting isEmpty)) _word.inputting = null - if ((_word.trans ne null) || (_word.inputting ne null)) + val _use_trans = (_word.trans ne null) && (_word.trans nonEmpty) + val _use_inputting = (_word.inputting ne null) && (_word.inputting nonEmpty) + if (_use_trans || _use_inputting) message ++= s"\n\n[[ ${h(_word.name)} ]]" logger debug s"**used [${_word.name}]" - if (_word.trans != null) for (_trans <- _word.trans) + if (_use_trans) for (_trans <- _word.trans) message ++= s"\n* ${h(_trans)}" logger debug s"**used [${_word.name}] used `${_trans}``" - if (_word.inputting != null) + if (_use_inputting) logger debug s"**used [${_word.name}] inputting" - if (_word.trans != null) + if (_use_trans) message += '\n' message ++= " maybe:" for (_inputting <- _word.inputting) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala index 4d453da..8316b42 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala @@ -12,17 +12,15 @@ import scala.language.postfixOps object Testing extends ISimpleCommand { - override def getName: String = "test" - override def getAliases: Array[String] = null + override val name: String = "test" + override val aliases: Array[ICommandAlias] | Null = null - override def execute (command: InputCommand, event: Update): Unit = { - - val a = StringBuilder("value") - a ++= "Changed" + override def execute (using command: InputCommand, event: Update): Unit = { MornyCoeur.extra exec new SendMessage( event.message.chat.id, - "Just a TEST command. num is:" + (a toString) + // language=html + "Just a TEST command." ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala index c870e8e..bf36bab 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala @@ -15,39 +15,39 @@ import scala.language.postfixOps object 喵呜 { object 抱抱 extends ISimpleCommand { - override def getName: String = "抱抱" - override def getAliases: Array[String] = Array() - override def execute (command: InputCommand, event: Update): Unit = - replyingSet(event, "贴贴", "贴贴") + override val name: String = "抱抱" + override val aliases: Array[ICommandAlias]|Null = null + override def execute (using command: InputCommand, event: Update): Unit = + replyingSet("贴贴", "贴贴") } object 揉揉 extends ISimpleCommand { - override def getName: String = "揉揉" - override def getAliases: Array[String] = Array() - override def execute (command: InputCommand, event: Update): Unit = - replyingSet(event, "蹭蹭", "摸摸") + override val name: String = "揉揉" + override val aliases: Array[ICommandAlias]|Null = null + override def execute (using command: InputCommand, event: Update): Unit = + replyingSet("蹭蹭", "摸摸") } object 蹭蹭 extends ISimpleCommand { - override def getName: String = "蹭蹭" - override def getAliases: Array[String] = Array() - override def execute (command: InputCommand, event: Update): Unit = - replyingSet(event, "揉揉", "蹭蹭") + override val name: String = "蹭蹭" + override val aliases: Array[ICommandAlias]|Null = null + override def execute (using command: InputCommand, event: Update): Unit = + replyingSet("揉揉", "蹭蹭") } object 贴贴 extends ISimpleCommand { - override def getName: String = "贴贴" - override def getAliases: Array[String] = Array() - override def execute (command: InputCommand, event: Update): Unit = - replyingSet(event, "贴贴", "贴贴") + override val name: String = "贴贴" + override val aliases: Array[ICommandAlias]|Null = null + override def execute (using command: InputCommand, event: Update): Unit = + replyingSet("贴贴", "贴贴") } object Progynova extends ITelegramCommand { - override def getName: String = "install" - override def getAliases: Array[String] = Array() - override def getParamRule: String = "" - override def getDescription: String = "抽取一个神秘盒子" - override def execute (command: InputCommand, event: Update): Unit = { + override val name: String = "install" + override val aliases: Array[ICommandAlias]|Null = null + override val paramRule: String = "" + override val description: String = "抽取一个神秘盒子" + override def execute (using command: InputCommand, event: Update): Unit = { MornyCoeur.extra exec new SendSticker( event.message.chat.id, TelegramStickers ID_PROGYNOVA @@ -55,7 +55,7 @@ object 喵呜 { } } - private def replyingSet (event: Update, whileRec: String, whileNew: String): Unit = { + private def replyingSet (whileRec: String, whileNew: String)(using event: Update): Unit = { val isNew = event.message.replyToMessage == null; val target = if (isNew) event.message else event.message.replyToMessage MornyCoeur.extra exec new SendMessage( diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala index 67da3f5..0e249d4 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala @@ -8,10 +8,10 @@ import com.pengrad.telegrambot.request.SendMessage object 私わね extends ISimpleCommand { - override def getName: String = "me" - override def getAliases: Array[String] = Array() + override val name: String = "me" + override val aliases: Array[ICommandAlias] | Null = null - override def execute (command: InputCommand, event: Update): Unit = { + override def execute (using command: InputCommand, event: Update): Unit = { if (probabilityTrue(521)) { val text = "/打假" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala new file mode 100644 index 0000000..4f39ac7 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListenerManager + +object MornyEventListeners { + + def registerAllEvents(): Unit = { + + EventListenerManager.register( + // ACTIVITY_RECORDER + OnUpdateTimestampOffsetLock, + // KUOHUANHUAN_NEED_SLEEP + OnTelegramCommand, + OnUniMeowTrigger, + OnUserRandom, + OnQuestionMarkReply, + OnUserSlashAction, + OnInlineQuery, + OnCallMe, + OnCallMsgSend, + OnMedicationNotifyApply, + OnEventHackHandle + ) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala index 1f51e4d..d8c334a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -15,7 +15,7 @@ object OnCallMe extends EventListener { private val me = MornyCoeur.config.trustedMaster - override def onMessage (update: Update): Boolean = { + override def onMessage (using update: Update): Boolean = { if update.message.text == null then return false if update.message.chat.`type` != (Chat.Type Private) then return false diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala new file mode 100644 index 0000000..a6fee41 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala @@ -0,0 +1,153 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString +import com.pengrad.telegrambot.model.{Chat, Message, MessageEntity, Update} +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{GetChat, SendMessage, SendSticker} + +import scala.collection.mutable.ArrayBuffer +import scala.language.postfixOps +import scala.util.matching.Regex + +object OnCallMsgSend extends EventListener { + + private val REGEX_MSG_SENDREQ_DATA_HEAD: Regex = "^\\*msg(-?\\d+)(\\*\\S+)?(?:\\n([\\s\\S]+))?$"r + + case class MessageToSend ( + message: String|Null, + entities: Array[MessageEntity]|Null, + parseMode: ParseMode|Null, + targetId: Long + ) { + def toSendMessage (target_override: Long|Null = null): SendMessage = + val useTarget = if target_override == null then targetId else target_override + val sendMessage = SendMessage(useTarget, message) + if entities ne null then sendMessage.entities(entities:_*) + if parseMode ne null then sendMessage.parseMode(parseMode) + sendMessage + } + private object MessageToSend: + def from (raw: Message): MessageToSend = { + raw.text match + case REGEX_MSG_SENDREQ_DATA_HEAD(_target, _parseMode, _body) => + val target = _target toLong + val parseMode: ParseMode | Null = _parseMode match + case "*markdown" | "*md" | "*m↓" => ParseMode MarkdownV2 + case "*md1" => ParseMode Markdown + case "*html" => ParseMode HTML + case _ => null + val bodyOffset = "*msg".length + _target.length + (if _parseMode eq null then 0 else _parseMode.length) + 1 + val entities = ArrayBuffer.empty[MessageEntity] + if (raw.entities ne null) for (e <- raw.entities) + val _parsed = MessageEntity(e.`type`, e.offset - bodyOffset, e.length) + if e.url ne null then _parsed.url(e.url) + if e.user ne null then _parsed.user(e.user) + if e.language ne null then _parsed.language(e.language) + entities += _parsed + MessageToSend(_body, entities toArray, parseMode, target) + case _ => null + } + + override def onMessage (using update: Update): Boolean = { + + val message = update.message + + if message.chat.`type` != Chat.Type.Private then return false + if message.text eq null then return false + if !(message.text startsWith "*msg") then return false + + if (!(MornyCoeur.trustedInstance isTrusted message.from.id)) + MornyCoeur.extra exec SendSticker( + message.chat.id, + TelegramStickers ID_403 + ).replyToMessageId(message.messageId) + return true + + if (message.text == "*msgsend") { + + if (message.replyToMessage eq null) return answer404 + val messageToSend = MessageToSend from message.replyToMessage + if ((messageToSend eq null) || (messageToSend.message eq null)) return answer404 + val sendResponse = MornyCoeur.getAccount execute messageToSend.toSendMessage() + + if (sendResponse isOk) { + MornyCoeur.extra exec SendSticker( + update.message.chat.id, + TelegramStickers ID_SENT + ).replyToMessageId(update.message.messageId) + } else { + MornyCoeur.extra exec SendMessage( + update.message.chat.id, + // language=html + s"""${sendResponse.errorCode} FAILED + |${sendResponse.description}""" + .stripMargin + ).replyToMessageId(update.message.messageId).parseMode(ParseMode HTML) + } + + return true + + } + + val messageToSend: MessageToSend = + val raw: Message = + if (message.text == "*msg") + if message.replyToMessage eq null then return answer404 + else message.replyToMessage + else if (message.text startsWith "*msg") + message + else return answer404 + val _toSend = MessageToSend from raw + if _toSend eq null then return answer404 + else _toSend + + val targetChatResponse = MornyCoeur.getAccount execute GetChat(messageToSend.targetId) + if (targetChatResponse isOk) { + def getChatDescriptionHTML (chat: Chat): String = + import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + val _c = TGToString as chat + // language=html + s"""${h(chat.id toString)}@${h(chat.`type`.name)}${if (chat.`type` != Chat.Type.Private) ":::" else ""} + |${_c getTypeTag} ${h(_c getSafeName)} ${_c getSafeLinkHTML}""" + .stripMargin + MornyCoeur.extra exec SendMessage( + update.message.chat.id, + getChatDescriptionHTML(targetChatResponse.chat) + ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) + } else { + MornyCoeur.extra exec SendMessage( + update.message.chat.id, + // language=html + s"""${targetChatResponse.errorCode} FAILED + |${targetChatResponse.description}""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) + } + + if messageToSend.message eq null then return true + val testSendResponse = MornyCoeur.getAccount execute messageToSend.toSendMessage(update.message.chat.id) + .replyToMessageId(update.message.messageId) + if (!(testSendResponse isOk)) + MornyCoeur.extra exec SendMessage( + update.message.chat.id, + // language=html + s"""${testSendResponse.errorCode} FAILED + |${testSendResponse.description}""" + .stripMargin + ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) + + true + + } + + private def answer404 (using update: Update): Boolean = + MornyCoeur.extra exec SendSticker( + update.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(update.message.messageId) + true + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala new file mode 100644 index 0000000..d618ddc --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala @@ -0,0 +1,80 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener + +import scala.collection.mutable +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import com.google.gson.GsonBuilder +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps + +object OnEventHackHandle extends EventListener { + + private case class Hacker (from_chat: Long, from_message: Long): + override def toString: String = s"$from_chat/$from_message" + enum HackType: + case USER + case GROUP + case ANY + + private val hackers = mutable.HashMap.empty[String, Hacker] + + def registerHack (from_message: Long, from_user: Long, from_chat: Long, t: HackType): Unit = + val record = t match + case HackType.USER => s"(($from_user))" + case HackType.GROUP => s"{{$from_chat}}" + case HackType.ANY => "[[]]" + hackers += (record -> Hacker(from_chat, from_message)) + logger debug s"add hacker track $record" + + private def onEventHacked (chat: Long, fromUser: Long)(using update: Update): Boolean = { + logger debug s"got event signed {{$chat}}(($fromUser))" + val x: Hacker = + if hackers contains s"(($fromUser))" then (hackers remove s"(($fromUser))")get + else if hackers contains s"{{$chat}}" then (hackers remove s"{{$chat}}")get + else if hackers contains "[[]]" then (hackers remove "[[]]")get + else return false + logger debug s"hacked event by $x" + import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + MornyCoeur.extra exec SendMessage( + x.from_chat, + // language=html + s"${h(GsonBuilder().setPrettyPrinting().create.toJson(update))}" + ).parseMode(ParseMode HTML).replyToMessageId(x.from_message toInt) + true + } + + override def onMessage (using update: Update): Boolean = + onEventHacked(update.message.chat.id, update.message.from.id) + override def onEditedMessage (using update: Update): Boolean = + onEventHacked(update.editedMessage.chat.id, update.editedMessage.from.id) + override def onChannelPost (using update: Update): Boolean = + onEventHacked(update.channelPost.chat.id, update.channelPost.from.id) + override def onEditedChannelPost (using update: Update): Boolean = + onEventHacked(update.editedChannelPost.chat.id, update.editedChannelPost.from.id) + override def onInlineQuery (using update: Update): Boolean = + onEventHacked(0, update.inlineQuery.from.id) + override def onChosenInlineResult (using update: Update): Boolean = + onEventHacked(0, update.chosenInlineResult.from.id) + override def onCallbackQuery (using update: Update): Boolean = + onEventHacked(0, update.callbackQuery.from.id) + override def onShippingQuery (using update: Update): Boolean = + onEventHacked(0, update.shippingQuery.from.id) + override def onPreCheckoutQuery (using update: Update): Boolean = + onEventHacked(0, update.preCheckoutQuery.from.id) + override def onPoll (using update: Update): Boolean = + onEventHacked(0, 0) + override def onPollAnswer (using update: Update): Boolean = + onEventHacked(0, update.pollAnswer.user.id) + override def onMyChatMemberUpdated (using update: Update): Boolean = + onEventHacked(update.myChatMember.chat.id, update.myChatMember.from.id) + override def onChatMemberUpdated (using update: Update): Boolean = + onEventHacked(update.chatMember.chat.id, update.chatMember.from.id) + override def onChatJoinRequest (using update: Update): Boolean = + onEventHacked(update.chatJoinRequest.chat.id, update.chatJoinRequest.from.id) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala index 96f2ad0..970aa2d 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala @@ -1,7 +1,8 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.MornyCoeur -import cc.sukazyo.cono.morny.bot.api.{EventListener, InlineQueryUnit} +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.bot.query.InlineQueryUnit import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.InlineQueryResult import com.pengrad.telegrambot.request.AnswerInlineQuery @@ -12,12 +13,12 @@ import scala.reflect.ClassTag object OnInlineQuery extends EventListener { - override def onInlineQuery (update: Update): Boolean = { + override def onInlineQuery (using update: Update): Boolean = { val results: List[InlineQueryUnit[_]] = MornyCoeur.queryManager query update var cacheTime = Int.MaxValue - var isPersonal = InlineQueryUnit.DEFAULT_INLINE_PERSONAL_RESP + var isPersonal = InlineQueryUnit.defaults.IS_PERSONAL val resultAnswers = ListBuffer[InlineQueryResult[_]]() for (r <- results) { if (cacheTime > r.cacheTime) cacheTime = r.cacheTime diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala new file mode 100644 index 0000000..5b2ab47 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala @@ -0,0 +1,21 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.daemon.MornyDaemons +import com.pengrad.telegrambot.model.{Message, Update} + +object OnMedicationNotifyApply extends EventListener { + + override def onEditedMessage (using event: Update): Boolean = + editedMessageProcess(event.editedMessage) + override def onEditedChannelPost (using event: Update): Boolean = + editedMessageProcess(event.editedChannelPost) + + private def editedMessageProcess (edited: Message): Boolean = { + if edited.chat.id != MornyCoeur.config.medicationNotifyToChat then return false + MornyDaemons.medicationTimerInstance.refreshNotificationWrite(edited) + true + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala new file mode 100644 index 0000000..825f2d5 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala @@ -0,0 +1,30 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.MornyCoeur +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.request.SendMessage + +import scala.language.postfixOps + +object OnQuestionMarkReply extends EventListener { + + private def QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓') + + override def onMessage (using event: Update): Boolean = { + + if event.message.text eq null then return false + + import cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue + if !probabilityTrue(8) then return false + for (c <- event.message.text toCharArray) + if !(QUESTION_MARKS contains c) then return false + + MornyCoeur.extra exec SendMessage( + event.message.chat.id, event.message.text + ).replyToMessageId(event.message.messageId) + true + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala new file mode 100644 index 0000000..526af48 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala @@ -0,0 +1,34 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.{Message, Update} +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.command.MornyCommands + +object OnTelegramCommand extends EventListener { + + override def onMessage (using update: Update): Boolean = { + + def _isCommandMessage(message: Message): Boolean = + if message.text eq null then false + else if !(message.text startsWith "/") then false + else if message.text startsWith "/ " then false + else true + + if !_isCommandMessage(update.message) then return false + val inputCommand = InputCommand(update.message.text drop 1) + if (!(inputCommand.getCommand matches "^\\w+$")) + logger debug "not command" + false + else if ((inputCommand.getTarget ne null) && (inputCommand.getTarget ne MornyCoeur.getUsername)) + logger debug "not morny command" + false + else + logger debug "is command" + MornyCommands.execute(using inputCommand) + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala new file mode 100644 index 0000000..c482b63 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala @@ -0,0 +1,23 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.bot.command.MornyCommands +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update + +object OnUniMeowTrigger extends EventListener { + + override def onMessage (using update: Update): Boolean = { + + if update.message.text eq null then return false + var ok = false + for ((name, command) <- MornyCommands.commands_uni) + val _name = "/"+name + if (_name == update.message.text) + command.execute(using InputCommand(_name)) + ok = true + ok + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.scala new file mode 100644 index 0000000..a1978a8 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.scala @@ -0,0 +1,17 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.MornyCoeur +import com.pengrad.telegrambot.model.Update + +object OnUpdateTimestampOffsetLock extends EventListener { + + private def isOutdated (timestamp: Int): Boolean = + timestamp < (MornyCoeur.config.eventOutdatedTimestamp/1000) + + override def onMessage (using update: Update): Boolean = isOutdated(update.message.date) + override def onEditedMessage (using update: Update): Boolean = isOutdated(update.editedMessage.date) + override def onChannelPost (using update: Update): Boolean = isOutdated(update.channelPost.date) + override def onEditedChannelPost (using update: Update): Boolean = isOutdated(update.editedChannelPost.date) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala index 2e21f63..1657169 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala @@ -12,7 +12,7 @@ object OnUserRandom extends EventListener { private val USER_OR_QUERY = "(.+)(?:还是|or)(.+)"r private val USER_IF_QUERY = "(.+)[吗?|?]+$"r - override def onMessage(update: Update): Boolean = { + override def onMessage(using update: Update): Boolean = { if update.message.text == null then return false if update.message.text startsWith "/" then return false diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala index 1970726..9447a91 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala @@ -15,9 +15,9 @@ object OnUserSlashAction extends EventListener { private val TG_FORMAT = "^\\w+(@\\w+)?$"r - override def onMessage (update: Update): Boolean = { + override def onMessage (using update: Update): Boolean = { - val text = update.message.text; + val text = update.message.text if text == null then return false if (text startsWith "/") { @@ -31,6 +31,7 @@ object OnUserSlashAction extends EventListener { case TG_FORMAT(_) => return false case x if x contains "/" => return false + case _ => val isHardParse = actions(0) isBlank def hp_len(i: Int) = if isHardParse then i+1 else i @@ -39,7 +40,7 @@ object OnUserSlashAction extends EventListener { val hasObject = actions.length != hp_len(1) val v_object = if hasObject then - actions slice(hp_len(1), actions.length) mkString(" ") + actions slice(hp_len(1), actions.length) mkString " " else "" val origin = update.message val target = diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala index d03a05c..70c4739 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala @@ -1,7 +1,6 @@ package cc.sukazyo.cono.morny.bot.query import javax.annotation.Nullable -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit import com.pengrad.telegrambot.model.Update trait ITelegramQuery { diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/InlineQueryUnit.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/InlineQueryUnit.scala new file mode 100644 index 0000000..b15f4b6 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/InlineQueryUnit.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.bot.query + +import cc.sukazyo.cono.morny.bot.query.InlineQueryUnit.defaults +import com.pengrad.telegrambot.model.request.InlineQueryResult + +object InlineQueryUnit { + + object defaults: + val CACHE_TIME = 300 + val IS_PERSONAL = false + +} + +class InlineQueryUnit[T <: InlineQueryResult[T]](val result: T) { + + var cacheTime: Int = defaults.CACHE_TIME + var isPersonal: Boolean = defaults.IS_PERSONAL + + def cacheTime (v: Int): InlineQueryUnit[T] = + this.cacheTime = v + this + + def isPersonal (v: Boolean): InlineQueryUnit[T] = + this.isPersonal = v + this + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala index faeaefb..5f13657 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala @@ -1,6 +1,5 @@ package cc.sukazyo.cono.morny.bot.query -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit import cc.sukazyo.cono.morny.bot.query import com.pengrad.telegrambot.model.Update diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala index c98feb6..14a4e81 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala @@ -1,6 +1,5 @@ package cc.sukazyo.cono.morny.bot.query -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation import com.pengrad.telegrambot.model.Update diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala index 2d19149..aec52f6 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala @@ -1,5 +1,4 @@ package cc.sukazyo.cono.morny.bot.query -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala index 90288e2..bc1a4e7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala @@ -1,7 +1,6 @@ package cc.sukazyo.cono.morny.bot.query import cc.sukazyo.cono.morny.Log.logger -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit import cc.sukazyo.cono.morny.util.BiliTool import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds import com.pengrad.telegrambot.model.Update diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala index 9427dca..ffc7f0e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala @@ -1,6 +1,5 @@ package cc.sukazyo.cono.morny.bot.query -import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.InlineQueryResultArticle diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala new file mode 100644 index 0000000..87f3364 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala @@ -0,0 +1,17 @@ +package cc.sukazyo.cono.morny.data + +import com.pengrad.telegrambot.model.User + +import scala.language.postfixOps + +object MornyJrrp { + + def jrrp_of_telegramUser (user: User, timestamp: Long): Double = + jrrp_v_xmomi(user.id, timestamp/(1000*60*60*24)) * 100.0 + + private def jrrp_v_xmomi (identifier: Long, dayStamp: Long): Double = + import cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex + import cc.sukazyo.cono.morny.util.CommonEncrypt.hashMd5 + (java.lang.Long parseLong byteArrayToHex(hashMd5(s"$identifier@$dayStamp")).substring(0, 4)) / (0xffff toDouble) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/NbnhhshQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/data/NbnhhshQuery.scala new file mode 100644 index 0000000..9c2049a --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/NbnhhshQuery.scala @@ -0,0 +1,37 @@ +package cc.sukazyo.cono.morny.data + +import cc.sukazyo.cono.morny.util.OkHttpPublic.MediaTypes +import com.google.gson.Gson +import okhttp3.{OkHttpClient, Request, RequestBody, ResponseBody} + +import java.io.IOException +import scala.util.Using + +object NbnhhshQuery { + + case class Word (name: String, trans: Array[String], inputting: Array[String]) + case class GuessResult (words: Array[Word]) + + private case class GuessRequest (text: String) + + private val API_URL = "https://lab.magiconch.com/api/nbnhhsh/" + private val API_GUESS_METHOD = "guess/" + + private val httpClient = OkHttpClient() + + @throws[IOException] + def sendGuess (text: String): GuessResult = { + val requestJsonText = Gson().toJson(GuessRequest(text)) + val request = Request.Builder() + .url(API_URL + API_GUESS_METHOD) + .post(RequestBody.create(requestJsonText, MediaTypes.JSON)) + .build + Using (httpClient.newCall(request).execute) { response => + val body = response.body + if body eq null then throw IOException("Nbnhhsh Request: body is null.") + val x = s"{ 'words': ${body.string} }" + Gson().fromJson(x, classOf[GuessResult]) + }.get + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala index 65abaca..f9dc533 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala @@ -30,12 +30,11 @@ object IP186QueryHandler { @throws[IOException] private def commonQuery (requestUrl: String, queryParam: String): IP186Response = { val request = Request.Builder().url(requestUrl + "?" + queryParam).build - var _body_string: String|Null = null Using ((httpClient newCall request) execute) { response => - if response.body ne null then _body_string = response.body.string - } - if _body_string eq null then throw IOException("Response of ip186: body is empty!") - IP186Response(requestUrl, _body_string) + val _body = response.body + if _body eq null then throw IOException("Response of ip186: body is empty!") + IP186Response(requestUrl, _body.string) + }.get } } From ddfe77350e17cce79e8affa5b74531d3d061b05a Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 10 Sep 2023 22:43:39 +0800 Subject: [PATCH 31/40] scala port stage3 (not tested) --- build.gradle | 41 +-- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/util/BiliTool.java | 0 .../cono/morny/util/CommonConvert.java | 0 .../cono/morny/util/CommonEncrypt.java | 0 .../sukazyo/cono/morny/util/CommonFormat.java | 0 .../sukazyo/cono/morny/util/CommonRandom.java | 0 .../cc/sukazyo/cono/morny/util/FileUtils.java | 0 .../sukazyo/cono/morny/util/OkHttpPublic.java | 0 .../cono/morny/util/UniversalCommand.java | 0 .../cono/morny/util/tgapi/ExtraAction.java | 0 .../cono/morny/util/tgapi/InputCommand.java | 0 .../cono/morny/util/tgapi/Standardize.java | 0 .../tgapi/event/EventRuntimeException.java | 0 .../util/tgapi/formatting/MsgEscape.java | 0 .../util/tgapi/formatting/NamedUtils.java | 0 .../util/tgapi/formatting/TGToString.java | 0 .../tgapi/formatting/TGToStringFromChat.java | 0 .../formatting/TGToStringFromMessage.java | 0 .../tgapi/formatting/TGToStringFromUser.java | 0 .../formatting/TelegramUserInformation.java | 0 src/main/old/cc/sukazyo/cono/morny/Log.java | 58 ---- .../old/cc/sukazyo/cono/morny/MornyAbout.java | 31 -- .../cc/sukazyo/cono/morny/MornyAssets.java | 19 -- .../old/cc/sukazyo/cono/morny/MornyCoeur.java | 310 ------------------ .../cc/sukazyo/cono/morny/MornySystem.java | 145 -------- .../cc/sukazyo/cono/morny/MornyTrusted.java | 42 --- .../sukazyo/cono/morny/bot/command/Roll.java | 4 - .../cono/morny/bot/command/package-info.java | 7 - .../morny/bot/event/OnActivityRecord.java | 28 -- .../bot/event/OnKuohuanhuanNeedSleep.java | 39 --- .../morny/bot/event/OnRandomlyTriggered.java | 27 -- .../cono/morny/daemon/MedicationTimer.java | 98 ------ .../cono/morny/daemon/MornyDaemons.java | 34 -- .../cono/morny/daemon/MornyReport.java | 159 --------- .../cono/morny/daemon/TrackerDataManager.java | 174 ---------- .../cono/morny/data/TelegramImages.java | 81 ----- .../cono/morny/data/TelegramStickers.java | 87 ----- .../scala/cc/sukazyo/cono/morny/Log.scala | 29 ++ .../cc/sukazyo/cono/morny/MornyAbout.scala | 17 + .../cc/sukazyo/cono/morny/MornyAssets.scala | 9 + .../cc/sukazyo/cono/morny/MornyCoeur.scala | 149 +++++++++ .../cc/sukazyo/cono/morny/MornyConfig.java | 6 +- .../cc/sukazyo/cono/morny/MornySystem.scala | 48 +++ .../cc/sukazyo/cono/morny/MornyTrusted.scala | 16 + .../cc/sukazyo/cono/morny/ServerMain.scala | 6 +- .../morny/bot/command/DirectMsgClear.scala | 4 +- .../cono/morny/bot/command/Encryptor.scala | 4 +- .../cono/morny/bot/command/EventHack.scala | 2 +- .../morny/bot/command/GetUsernameAndId.scala | 2 +- .../cono/morny/bot/command/IP186Query.scala | 4 +- .../morny/bot/command/MornyCommands.scala | 13 +- .../morny/bot/command/MornyInformation.scala | 57 ++-- .../morny/bot/command/MornyManagers.scala | 4 +- .../cono/morny/bot/event/OnCallMe.scala | 4 +- .../cono/morny/bot/event/OnCallMsgSend.scala | 9 +- .../cono/morny/bot/event/OnInlineQuery.scala | 4 +- .../bot/event/OnMedicationNotifyApply.scala | 4 +- .../morny/bot/event/OnTelegramCommand.scala | 2 +- .../cono/morny/bot/query/MornyQueries.scala | 2 +- .../cono/morny/bot/query/MyInformation.scala | 2 +- .../cono/morny/daemon/MedicationTimer.scala | 90 +++++ .../cono/morny/daemon/MornyDaemons.scala | 29 ++ .../cono/morny/daemon/MornyReport.scala | 122 +++++++ .../cono/morny/data/TelegramImages.scala | 38 +++ .../cono/morny/data/TelegramStickers.java | 53 +++ .../cono/morny/internal/BuildConfigField.java | 0 .../cono/morny/internal/ScalaJavaConv.scala | 12 + .../java/cc/sukazyo/cono/morny/MornyCLI.java | 2 +- .../morny/daemon/TestMedicationTimer.java | 6 +- 70 files changed, 712 insertions(+), 1423 deletions(-) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/BiliTool.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/CommonConvert.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/CommonEncrypt.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/CommonFormat.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/CommonRandom.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/FileUtils.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/OkHttpPublic.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/UniversalCommand.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/Standardize.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java (100%) rename src/main/{old => java}/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java (100%) delete mode 100644 src/main/old/cc/sukazyo/cono/morny/Log.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/MornyAbout.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/MornyAssets.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/MornySystem.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/MornyTrusted.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/command/Roll.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/command/package-info.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/daemon/MedicationTimer.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/daemon/MornyDaemons.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/data/TelegramImages.java delete mode 100644 src/main/old/cc/sukazyo/cono/morny/data/TelegramStickers.java create mode 100644 src/main/scala/cc/sukazyo/cono/morny/Log.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/MornyAbout.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala rename src/main/{old => scala}/cc/sukazyo/cono/morny/MornyConfig.java (97%) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java rename src/main/{old => scala}/cc/sukazyo/cono/morny/internal/BuildConfigField.java (100%) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/internal/ScalaJavaConv.scala diff --git a/build.gradle b/build.gradle index 4d45291..f0041ee 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ final JavaVersion proj_java = JavaVersion.VERSION_17 final Charset proj_file_encoding = StandardCharsets.UTF_8 final proj_scala_api = 3 //final proj_scala_lib = proj_scala_api+'.4.0-RC1-bin-20230901-89e8dba-NIGHTLY' -final proj_scala_lib = proj_scala_api+'.3.1-RC7' +final proj_scala_lib = proj_scala_api+'.3.1' String publish_local_url = null String publish_remote_url = null String publish_remote_username = null @@ -89,35 +89,26 @@ dependencies { } -sourceSets { - main { - scala { srcDirs = ['src/main/scala', 'src/main/old'] } - } +tasks.withType(JavaCompile).configureEach { + + sourceCompatibility proj_java.getMajorVersion() + targetCompatibility proj_java.getMajorVersion() + + options.encoding = proj_file_encoding.name() + } -scala { +tasks.withType(ScalaCompile).configureEach { - compileJava { - - sourceCompatibility proj_java.getMajorVersion() - targetCompatibility proj_java.getMajorVersion() - - options.encoding = proj_file_encoding.name() - - } + sourceCompatibility proj_java.getMajorVersion() + targetCompatibility proj_java.getMajorVersion() - compileScala { - - sourceCompatibility proj_java.getMajorVersion() - targetCompatibility proj_java.getMajorVersion() - - options.encoding = proj_file_encoding.name() - scalaCompileOptions.encoding = proj_file_encoding.name() - -// scalaCompileOptions.additionalParameters.add("-Yexplicit-nulls") - - } + options.encoding = proj_file_encoding.name() + scalaCompileOptions.encoding = proj_file_encoding.name() + scalaCompileOptions.additionalParameters.add "-language:postfixOps" +// scalaCompileOptions.additionalParameters.add("-Yexplicit-nulls") + } test { diff --git a/gradle.properties b/gradle.properties index 4f1df34..9ce1455 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s VERSION = 1.0.0-RC4 USE_DELTA = true -VERSION_DELTA = scalaport2 +VERSION_DELTA = scalaport3 CODENAME = beiping diff --git a/src/main/old/cc/sukazyo/cono/morny/util/BiliTool.java b/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/BiliTool.java rename to src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/CommonConvert.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/CommonConvert.java rename to src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/CommonEncrypt.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/CommonEncrypt.java rename to src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/CommonFormat.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/CommonFormat.java rename to src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/CommonRandom.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/CommonRandom.java rename to src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/FileUtils.java b/src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/FileUtils.java rename to src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/OkHttpPublic.java b/src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/OkHttpPublic.java rename to src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/UniversalCommand.java b/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/UniversalCommand.java rename to src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/Standardize.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/Standardize.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/Standardize.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/Standardize.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java diff --git a/src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java rename to src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java diff --git a/src/main/old/cc/sukazyo/cono/morny/Log.java b/src/main/old/cc/sukazyo/cono/morny/Log.java deleted file mode 100644 index 57ab124..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/Log.java +++ /dev/null @@ -1,58 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.messiva.formatter.SimpleFormatter; -import cc.sukazyo.messiva.log.LogLevel; -import cc.sukazyo.messiva.logger.Logger; -import cc.sukazyo.messiva.appender.ConsoleAppender; - -import javax.annotation.Nonnull; -import java.io.PrintWriter; -import java.io.StringWriter; - -/** - * Morny 的 log 管理器 - */ -public class Log { - - /** - * Morny 的 Logger 实例, - * messiva 更新 - * @since 0.4.1.1 - */ - public static final Logger logger = new Logger(new ConsoleAppender(new SimpleFormatter())).minLevel(LogLevel.INFO); - - /** - * Is the Debug mode enabled. - * - * @return if the minimal log level is equal or lower than DEBUG level. - */ - public static boolean debug () { - return logger.levelSetting.minLevel().level <= LogLevel.DEBUG.level; - } - - /** - * Switch the Debug log output enabled. - *

      - * if enable the debug log output, all the Log regardless of LogLevel will be output. - * As default, if the debug log output is disabled, Logger will ignore the Logs level lower than INFO. - * - * @param debug switch enable the debug log output as true, or disable it as false. - */ - public static void debug (boolean debug) { - if (debug) logger.minLevel(LogLevel.ALL); - else logger.minLevel(LogLevel.INFO); - } - - /** - * 获取异常的堆栈信息. - * @param e 异常体 - * @return {@link String} 格式的异常的堆栈报告信息. - * @see 1.0.0-alpha5 - */ - public static String exceptionLog (@Nonnull Throwable e) { - final StringWriter stackTrace = new StringWriter(); - e.printStackTrace(new PrintWriter(stackTrace)); - return stackTrace.toString(); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/MornyAbout.java b/src/main/old/cc/sukazyo/cono/morny/MornyAbout.java deleted file mode 100644 index 3933d9b..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/MornyAbout.java +++ /dev/null @@ -1,31 +0,0 @@ -package cc.sukazyo.cono.morny; - -import java.io.IOException; - -/** - * Some of the static information of Morny. - */ -public class MornyAbout { - - /** - * ASCII art of Morny Featured Image. - *

      - * used for Coeur starting welcome screen. - *

      - * stored at /assets_morny/texts/server-hello.txt - */ - public static final String MORNY_PREVIEW_IMAGE_ASCII; - static { - try { - MORNY_PREVIEW_IMAGE_ASCII = MornyAssets.pack.getResource("texts/server-hello.txt").readAsString(); - } catch (IOException e) { - throw new RuntimeException("Cannot read MORNY_PREVIEW_IMAGE_ASCII from assets pack", e); - } - } - - public static final String MORNY_SOURCECODE_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono"; - public static final String MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK = "https://storage.sukazyo.cc/Eyre_S/Coeur-Morny-Cono"; - public static final String MORNY_ISSUE_TRACKER_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono/issues"; - public static final String MORNY_USER_GUIDE_LINK = "https://book.sukazyo.cc/morny"; - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/MornyAssets.java b/src/main/old/cc/sukazyo/cono/morny/MornyAssets.java deleted file mode 100644 index 1fdfe7f..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/MornyAssets.java +++ /dev/null @@ -1,19 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.restools.ResourcesPackage; - -/** - * Morny assets manager. - * - * @see #pack - * @since 1.0.0-RC4 - */ -public class MornyAssets { - - /** - * Instance mirror of the Morny assets, the assets root is /src/main/resources/assets_morny/. - * @since 1.0.0-RC4 - */ - public static final ResourcesPackage pack = new ResourcesPackage(MornyAssets.class, "assets_morny"); - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java b/src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java deleted file mode 100644 index e6d201d..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/MornyCoeur.java +++ /dev/null @@ -1,310 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener$; -import cc.sukazyo.cono.morny.bot.command.MornyCommands; -import cc.sukazyo.cono.morny.bot.event.MornyEventListeners; -import cc.sukazyo.cono.morny.bot.query.MornyQueries; -import cc.sukazyo.cono.morny.daemon.MornyDaemons; -import cc.sukazyo.cono.morny.daemon.TrackerDataManager; -import cc.sukazyo.cono.morny.util.tgapi.ExtraAction; -import com.pengrad.telegrambot.TelegramBot; -import com.pengrad.telegrambot.impl.FileApi; -import com.pengrad.telegrambot.model.User; -import com.pengrad.telegrambot.request.GetMe; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static cc.sukazyo.cono.morny.Log.logger; - -/** - * Morny Cono 核心
      - * - 的程序化入口类,保管着 morny 的核心属性
      - */ -public class MornyCoeur { - - /** 当前程序的 Morny Coeur 实例 */ - private static MornyCoeur INSTANCE; - - /** 当前 Morny 的启动配置 */ - public final MornyConfig config; - - /** 当前 Morny 的{@link MornyTrusted 信任验证机}实例 */ - private final MornyTrusted trusted; - private final MornyQueries queryManager = new MornyQueries(); - - /** morny 的 bot 账户 */ - private final TelegramBot account; - private final ExtraAction extraActionInstance; - /** - * morny 的 bot 账户的用户名
      - *
      - * 这个字段将会在登陆成功后赋值为登录到的 bot 的 username。 - * 它应该是和 {@link #account} 的 username 同步的
      - *
      - * 如果在登陆之前就定义了此字段,则登陆代码会验证登陆的 bot 的 username - * 是否与定义的 username 符合。如果不符合则会报错。 - */ - public final String username; - /** - * morny 的 bot 账户的 telegram id
      - *
      - * 这个字段将会在登陆成功后赋值为登录到的 bot 的 id。 - */ - public final long userid; - /** - * morny 主程序启动时间
      - * 用于统计数据 - */ - public static final long coeurStartTimestamp = ServerMain.systemStartupTime(); - - private Object whileExitReason = null; - - private record LogInResult(TelegramBot account, String username, long userid) { } - - /** - * 执行 bot 初始化 - * - * @param config Morny 实例的配置选项数据 - */ - private MornyCoeur (MornyConfig config) { - - this.config = config; - - configureSafeExit(); - - logger.info("args key:\n " + config.telegramBotKey); - if (config.telegramBotUsername != null) { - logger.info("login as:\n " + config.telegramBotUsername); - } - - try { - final LogInResult loginResult = login(config.telegramBotApiServer, config.telegramBotApiServer4File, config.telegramBotKey, config.telegramBotUsername); - this.account = loginResult.account; - this.username = loginResult.username; - this.userid = loginResult.userid; - this.trusted = new MornyTrusted(this); - StringBuilder trustedReadersDinnerIds = new StringBuilder(); - trusted.getTrustedReadersOfDinnerSet().forEach(id -> trustedReadersDinnerIds.append("\n ").append(id)); - logger.info(String.format(""" - trusted param set: - - master (id) - %d - - trusted chat (id) - %d - - trusted reader-of-dinner (id)%s""", - config.trustedMaster, config.trustedChat, trustedReadersDinnerIds - )); - } - catch (Exception e) { - RuntimeException ex = new RuntimeException("Cannot login to bot/api. :\n " + e.getMessage()); - logger.error(ex.getMessage()); - throw ex; - } - - this.extraActionInstance = ExtraAction.as(account); - - logger.info("Bot login succeed."); - - } - - /** - * 向外界暴露的 morny 初始化入口. - *

      - * 如果 morny 已经初始化,则不会进行初始化,抛出错误消息并直接退出方法。 - * - * @see #MornyCoeur 程序初始化方法 - * @param config morny 实例的配置选项数据 - */ - public static void init (MornyConfig config) { - if (INSTANCE == null) { - - logger.info("Coeur Starting"); - INSTANCE = new MornyCoeur(config); - - MornyDaemons.start(); - - logger.info("start telegram events listening"); - MornyEventListeners.registerAllEvents(); - INSTANCE.account.setUpdatesListener(TelegramUpdatesListener$.MODULE$); - - if (config.commandLoginRefresh) { - logger.info("resetting telegram command list"); - MornyCommands.automaticTGListUpdate(); - } - - logger.info("Coeur start complete"); - return; - - } - logger.error("Coeur already started!!!"); - } - - /** - * 向所有的数据管理器发起保存数据的指令 - * @since 0.4.3.0 - */ - public void saveDataAll () { - TrackerDataManager.save(); - } - - /** - * 用于退出时进行缓存的任务处理等进行安全退出 - */ - private void exitCleanup () { - MornyDaemons.stop(); - if (config.commandLogoutClear) { - MornyCommands.automaticTGListRemove(); - } - } - - /** - * 为程序在虚拟机上添加退出钩子 - */ - private void configureSafeExit () { - Runtime.getRuntime().addShutdownHook(new Thread(this::exitCleanup, "exit-cleaning")); - } - - /** - * 登录 bot. - *

      - * 会反复尝试三次进行登录。如果登录失败,则会直接抛出 RuntimeException 结束处理。 - * 会通过 GetMe 动作验证是否连接上了 telegram api 服务器, - * 同时也要求登录获得的 username 和 {@link #username} 声明值相等 - * - * @param api bot client 将会连接到的 telegram bot api 位置。 - * 填入 {@code null} 则使用默认的 {@code "https://api.telegram.org/bot"} - * @param api4File bot client 将会连接到的 telegram file api 位置。 - * 如果传入 {@code null} 则会跟随 {@param api} 选项的设定(具体为在 {@param api} 路径的后面添加 {@code /file} 路径)。 - * 如果两者都为 {@code null},则跟随默认的 {@value FileApi#FILE_API} - * @param key bot 的 api-token. 必要值 - * @param requireName 要求登录到的需要的 username,如果登陆后的 username 与此不同则会报错退出。 - * 填入 {@code null} 则表示不对 username 作要求 - * @return 成功登录后的 {@link TelegramBot} 对象 - */ - @Nonnull - private static LogInResult login ( - @Nullable String api, @Nullable String api4File, - @Nonnull String key, @Nullable String requireName - ) { - final TelegramBot.Builder accountConfig = new TelegramBot.Builder(key); - boolean isCustomApi = false; - String apiUrlSet = "https://api.telegram.org/bot"; - String api4FileUrlSet = FileApi.FILE_API; - if (api != null) { - api = api.endsWith("/") ? api.substring(0, api.length() - 1) : api; - accountConfig.apiUrl(apiUrlSet = api.endsWith("/bot")? api : api + "/bot"); - isCustomApi = true; - } - if (api4File != null) { - api4File = api4File.endsWith("/") ? api4File : api4File + "/"; - accountConfig.fileApiUrl(api4FileUrlSet = api4File.endsWith("/file/bot")? api4File : api4File + "/file/bot"); - isCustomApi = true; - } else if (api != null && !api.endsWith("/bot")) { - accountConfig.fileApiUrl(api4FileUrlSet = api + "/file/bot"); - } - if (isCustomApi) { - logger.info(String.format(""" - Telegram Bot API set to : - - %s - - %s""", - apiUrlSet, api4FileUrlSet - )); - } - final TelegramBot account = accountConfig.build(); - logger.info("Trying to login..."); - for (int i = 1; i < 4; i++) { - if (i != 1) logger.info("retrying..."); - try { - final User remote = account.execute(new GetMe()).user(); - if (requireName != null && !requireName.equals(remote.username())) - throw new RuntimeException("Required the bot @" + requireName + " but @" + remote.username() + " logged in!"); - logger.info("Succeed login to @" + remote.username()); - return new LogInResult(account, remote.username(), remote.id()); - } catch (Exception e) { - logger.error(Log.exceptionLog(e)); - logger.error("login failed."); - } - } - throw new RuntimeException("Login failed.."); - } - - /** - * @see #saveDataAll() - * @since 0.4.3.0 - */ - public static void callSaveData () { - INSTANCE.saveDataAll(); - logger.info("done all save action."); - } - - /** - * 检查 Coeur 是否已经完成初始化. - * @since 1.0.0-alpha5 - */ - public static boolean available() { - return INSTANCE != null; - } - - /** - * 获取登录成功后的 telegram bot 对象 - * - * @return {@link #account MornyCoeur.account} - */ - @Nonnull - public static TelegramBot getAccount () { - return INSTANCE.account; - } - - /** - * 获取登录 bot 的 username - * - * @return {@link #username MornyCoeur.username} - */ - @Nonnull - public static String getUsername () { - return INSTANCE.username; - } - - /** - * 获取当前 morny 的配置数据 - * - * @return {@link #config MornyCoeur.config} - */ - @Nonnull - public static MornyConfig config () { - return INSTANCE.config; - } - - /** - * 获取 Morny 的{@link MornyTrusted 信任验证机} - * - * @return {@link #trusted MornyCoeur.trusted} - */ - @Nonnull - public static MornyTrusted trustedInstance () { - return INSTANCE.trusted; - } - - @Nonnull - public static MornyQueries queryManager () { - return INSTANCE.queryManager; - } - - @Nonnull - public static ExtraAction extra () { - return INSTANCE.extraActionInstance; - } - - public static long getUserid () { return INSTANCE.userid; } - - public static void exit (int status, Object reason) { - INSTANCE.whileExitReason = reason; - System.exit(status); - } - - public static Object getExitReason () { - return INSTANCE.whileExitReason; - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/MornySystem.java b/src/main/old/cc/sukazyo/cono/morny/MornySystem.java deleted file mode 100644 index edd4719..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/MornySystem.java +++ /dev/null @@ -1,145 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.cono.morny.daemon.MornyReport; -import cc.sukazyo.cono.morny.internal.BuildConfigField; -import cc.sukazyo.cono.morny.util.FileUtils; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.IOException; -import java.net.URISyntaxException; -import java.security.NoSuchAlgorithmException; - -/** - * Morny Cono 的 Coeur 的程序属性存放类 - */ -public class MornySystem { - - /** - * 程序的语义化版本号. - *

      - * 这个版本号包含了以下的 {@link #VERSION_BASE}, {@link #VERSION_DELTA} 字段, - * 但不包含作为附加属性的构建时的{@link BuildConfig#COMMIT git 状态}属性 - *

      - * 这个格式的版本号也是在 maven 包仓库中所使用的版本号 - * @since 1.0.0-alpha4 - */ - @BuildConfigField @Nonnull public static final String VERSION = BuildConfig.VERSION; - /** - * 程序的完整语义化版本号. - *

      - * 包含了全部的 {@link #VERSION_BASE}, {@link #VERSION_DELTA}, 以及{@link BuildConfig#COMMIT git 状态}属性。 - * 虽然仍旧不包含{@link #CODENAME}属性 - *

      - * 这个格式的版本号也是 gradle 构建配置使用的版本号,也在普通打包时生成文件时使用 - * @since 1.0.0-alpha4.2 - */ - @BuildConfigField @Nonnull public static final String VERSION_FULL = BuildConfig.VERSION_FULL; - /** - * 程序的基础版本号. - *

      - * 它只包含了版本号中的主要信息:例如 {@code 0.8.0.5}, {@code 1.0.0-alpha-3}, - * 而不会有用于精确定义的 {@link #VERSION_DELTA} 字段和作为附加使用的 {@link BuildConfig#COMMIT git commit 信息} - * @since 1.0.0-alpha4 - */ - @BuildConfigField @Nonnull public static final String VERSION_BASE = BuildConfig.VERSION_BASE; - /** - * 程序的版本 delta. - * —— 设计上用于在一个基版本当中分出不同构建的版本. - *

      - * {@link null} 作为值,表示这个字段没有被使用. - *

      - * 版本 delta 会以 {@code -δversion-delta} 的形式附着在 {@link #VERSION_BASE} 之后. - * 两者合并后的版本号格式即为 {@link #VERSION} - *

      - * 在发行版本中一般不应该被使用. - *

      - * 目前并不多被使用. - * @since 1.0.0-alpha4 - */ - @BuildConfigField @Nullable public static final String VERSION_DELTA = BuildConfig.VERSION_DELTA; - - /** - * Morny Coeur 当前的版本代号. - *

      - * 一个单个单词,一般作为一个大版本的名称,只在重大更新改变
      - * 格式保持为仅由小写字母和数字组成
      - * 有时也可能是复合词或特殊的词句
      - *
      - */ - @BuildConfigField @Nonnull public static final String CODENAME = BuildConfig.CODENAME; - - /** - * Coeur 的代码仓库的链接. 它应该链接到当前程序的源码主页. - *

      - * {@link null} 表示这个属性在构建时未被设置(或没有源码主页) - * @since 1.0.0-alpha4 - */ - @BuildConfigField @Nullable public static final String CODE_STORE = BuildConfig.CODE_STORE; - /** - * Coeur 的 git commit 链接. - *

      - * 它应该是一个可以通过 {@link String#format(String, Object...)} 要求格式的链接模板,带有一个 {@link String} 类型的槽位 —— - * 通过 String.format(COMMIT_PATH, {@link BuildConfig#COMMIT}) 即可取得当前当前程序所基于的 commit 的链接。 - * @since 1.0.0-alpha4 - */ - @BuildConfigField @Nullable public static final String COMMIT_PATH = BuildConfig.COMMIT_PATH; - - /** @see #VERSION_DELTA */ - @BuildConfigField - public static boolean isUseDelta () { return VERSION_DELTA != null; } - - /** @see BuildConfig#COMMIT */ - @BuildConfigField - @SuppressWarnings("ConstantConditions") - public static boolean isGitBuild () { return BuildConfig.COMMIT != null; } - - /** @see BuildConfig#COMMIT */ - @BuildConfigField - public static boolean isCleanBuild () { return BuildConfig.CLEAN_BUILD; } - - /** - * 获取程序的当前构建所基于的 git commit 的链接. - *

      - * 如果 {@link #COMMIT_PATH}(一般表示没有公开储存库) - * 或是 {@link BuildConfig#COMMIT}(一般表示程序的构建环境没有使用 git) - * 任何一个不可用,则此方法也不可用。 - * - * @return 当前构建的 git commit 链接,为空则表示不可用。 - * @see #COMMIT_PATH - * @since 1.0.0-alpha4 - */ - @Nullable - @BuildConfigField - @SuppressWarnings("ConstantConditions") - public static String currentCodePath () { - if (COMMIT_PATH == null || !isGitBuild()) return null; - return String.format(COMMIT_PATH, BuildConfig.COMMIT); - } - - - - /** - * 获取程序 jar 文件的 md5-hash 值
      - *
      - * 只支持 jar 文件方式启动的程序 —— - * 如果是通过 classpath 来启动,程序无法找到本体jar文件,则会返回 {@code } 文本 - *
      - * 值格式为 {@link java.lang.String} - * - * @return 程序jar文件的 md5-hash 值字符串,或 {@code } 如果出现错误 - */ - @Nonnull - public static String getJarMd5() { - try { - return FileUtils.getMD5Three(MornyCoeur.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); - } catch (IOException | URISyntaxException e) { - return ""; - } catch (NoSuchAlgorithmException e) { - Log.logger.error(Log.exceptionLog(e)); - MornyReport.exception(e, ""); - return ""; - } - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/MornyTrusted.java b/src/main/old/cc/sukazyo/cono/morny/MornyTrusted.java deleted file mode 100644 index 7a43fa0..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/MornyTrusted.java +++ /dev/null @@ -1,42 +0,0 @@ -package cc.sukazyo.cono.morny; - -import com.pengrad.telegrambot.model.ChatMember.Status; - -import java.util.Set; - -/** - * 对用户进行身份权限验证的管理类 - */ -public class MornyTrusted { - - private final MornyCoeur instance; - - public MornyTrusted (MornyCoeur instance) { - this.instance = instance; - } - - /** - * 用于检查一个 telegram-user 是否受信任
      - *
      - * 用户需要受信任才能执行一些对程序甚至是宿主环境而言危险的操作,例如关闭程序
      - *
      - * 它的逻辑(目前)是检查群聊 {@link MornyConfig#trustedChat} 中这个用户是否为群组管理员 - * - * @param userId 需要检查的用户的id - * @return 所传递的用户id对应的用户是否受信任 - */ - public boolean isTrusted (long userId) { - if (userId == instance.config.trustedMaster) return true; - if (instance.config.trustedChat == -1) return false; - return MornyCoeur.extra().isUserInGroup(userId, instance.config.trustedChat, Status.administrator); - } - - public boolean isTrustedForDinnerRead (long userId) { - return instance.config.dinnerTrustedReaders.contains(userId); - } - - public Set getTrustedReadersOfDinnerSet () { - return Set.copyOf(instance.config.dinnerTrustedReaders); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/command/Roll.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/Roll.java deleted file mode 100644 index c3c865f..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/command/Roll.java +++ /dev/null @@ -1,4 +0,0 @@ -package cc.sukazyo.cono.morny.bot.command; - -public class Roll { -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/command/package-info.java b/src/main/old/cc/sukazyo/cono/morny/bot/command/package-info.java deleted file mode 100644 index a2d0379..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/command/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 一系列的 telegram bot 命令的声明. - *

      - * 命令将在 {@link cc.sukazyo.cono.morny.bot.command.MornyCommands} 当中实例化并注册管理,并通过事件 - * {@link cc.sukazyo.cono.morny.bot.event.OnTelegramCommand} 调用. - */ -package cc.sukazyo.cono.morny.bot.command; diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java deleted file mode 100644 index e099fb7..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnActivityRecord.java +++ /dev/null @@ -1,28 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.bot.api.EventListener; -import cc.sukazyo.cono.morny.daemon.TrackerDataManager; -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Update; - -import javax.annotation.Nonnull; - -@Deprecated -public class OnActivityRecord implements EventListener { - - @Override - public boolean onMessage (@Nonnull Update update) { - if ( - update.message().chat().type() == Chat.Type.supergroup || - update.message().chat().type() == Chat.Type.group - ) { - TrackerDataManager.record( - update.message().chat().id(), - update.message().from().id(), - (long)update.message().date() * 1000 - ); - } - return false; - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java deleted file mode 100644 index 8af5bc5..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnKuohuanhuanNeedSleep.java +++ /dev/null @@ -1,39 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.bot.api.EventListener; -import com.pengrad.telegrambot.model.Update; -import com.pengrad.telegrambot.request.DeleteMessage; - -import javax.annotation.Nonnull; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.Locale; - -@Deprecated -public class OnKuohuanhuanNeedSleep implements EventListener { - - @Override - public boolean onMessage (@Nonnull Update update) { - final GregorianCalendar time = new GregorianCalendar(Locale.TAIWAN); - time.setTimeInMillis(System.currentTimeMillis()); - if ( - ( update.message().from().id() == 786563752L && ( - time.get(Calendar.HOUR_OF_DAY) >= 23 || - time.get(Calendar.HOUR_OF_DAY) < 5 - )) || ( update.message().from().id() == 1075871712L && ( - (time.get(Calendar.HOUR_OF_DAY) >= 22 && time.get(Calendar.MINUTE) >= 30) || - time.get(Calendar.HOUR_OF_DAY) >= 23 || - time.get(Calendar.HOUR_OF_DAY) < 5 - )) - ) { - MornyCoeur.extra().exec( - new DeleteMessage(update.message().chat().id(), - update.message().messageId()) - ); - return true; - } - return false; - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java b/src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java deleted file mode 100644 index 24c8c2b..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/bot/event/OnRandomlyTriggered.java +++ /dev/null @@ -1,27 +0,0 @@ -package cc.sukazyo.cono.morny.bot.event; - -import cc.sukazyo.cono.morny.bot.api.EventListener; - -@Deprecated -public class OnRandomlyTriggered implements EventListener { - -// /** -// * function CODE_IK0XA1 -// */ -// // @Override -// public boolean onMessage (@Nonnull Update update) { -// -// if (update.message().text() == null) return false; -// -// if (update.message().text().contains("急") && Math.random()<(1d/20)) { -// MornyCoeur.extra().exec(new SendMessage( -// update.message().chat().id(), -// "急也没用" -// )); -// } -// -// return false; -// -// } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/daemon/MedicationTimer.java b/src/main/old/cc/sukazyo/cono/morny/daemon/MedicationTimer.java deleted file mode 100644 index 6760b4e..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/daemon/MedicationTimer.java +++ /dev/null @@ -1,98 +0,0 @@ -package cc.sukazyo.cono.morny.daemon; - -import cc.sukazyo.cono.morny.MornyCoeur; -import cc.sukazyo.cono.morny.util.CommonFormat; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.MessageEntity; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.EditMessageText; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.response.SendResponse; - -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import static cc.sukazyo.cono.morny.Log.exceptionLog; -import static cc.sukazyo.cono.morny.Log.logger; - -public class MedicationTimer extends Thread { - - private final ZoneOffset USE_TIME_ZONE = MornyCoeur.config().medicationTimerUseTimezone; - private final Set NOTIFY_AT_HOUR = MornyCoeur.config().medicationNotifyAt; - private final long NOTIFY_CHAT = MornyCoeur.config().medicationNotifyToChat; - public static final String NOTIFY_MESSAGE = "\uD83C\uDF65⏲"; - private static final String DAEMON_THREAD_NAME = "TIMER_Medication"; - - private static final long LAST_NOTIFY_ID_NULL = -1L; - private long lastNotify = LAST_NOTIFY_ID_NULL; - - public static class NoNotifyTimeTag extends Throwable { private NoNotifyTimeTag(){} } - - MedicationTimer () { - super(DAEMON_THREAD_NAME); - } - - @Override - public void run () { - logger.info("MedicationTimer started"); - while (!interrupted()) { - try { - waitToNextRoutine(); - sendNotification(); - } catch (InterruptedException e) { - interrupt(); - logger.info("MedicationTimer was interrupted, will be exit now"); - } catch (NoNotifyTimeTag ignored) { - logger.warn("Notify Time not Set and the MedicationTimer will not working!\nMedicationTimer will be exit now."); - interrupt(); - } catch (Exception e) { - logger.error("Unexpected error occurred"); - logger.error(exceptionLog(e)); - MornyReport.exception(e); - } - } - logger.info("MedicationTimer stopped"); - } - - private void sendNotification () { - final SendResponse resp = MornyCoeur.extra().exec(new SendMessage(NOTIFY_CHAT, NOTIFY_MESSAGE)); - if (resp.isOk()) lastNotify = resp.message().messageId(); - else lastNotify = LAST_NOTIFY_ID_NULL; - } - - public void refreshNotificationWrite (Message edited) { - if (edited.messageId() != lastNotify) return; - final String editTime = CommonFormat.formatDate(edited.editDate()*1000, 8); - ArrayList entities = new ArrayList<>(); - if (edited.entities() != null) entities.addAll(List.of(edited.entities())); - entities.add(new MessageEntity(MessageEntity.Type.italic, edited.text().length() + "\n-- ".length(), editTime.length())); - EditMessageText sending = new EditMessageText( - NOTIFY_CHAT, - edited.messageId(), - edited.text() + "\n-- " + editTime + " --" - ).parseMode(ParseMode.HTML).entities(entities.toArray(MessageEntity[]::new)); - MornyCoeur.extra().exec(sending); - lastNotify = LAST_NOTIFY_ID_NULL; - } - - public static long calcNextRoutineTimestamp (long baseTimeMillis, ZoneOffset useTimeZone, Set atHours) - throws NoNotifyTimeTag { - if (atHours.isEmpty()) throw new NoNotifyTimeTag(); - LocalDateTime time = LocalDateTime.ofEpochSecond( - baseTimeMillis/1000, (int)(baseTimeMillis%1000)*1000*1000, - useTimeZone - ).withMinute(0).withSecond(0).withNano(0); - do { - time = time.plusHours(1); - } while (!atHours.contains(time.getHour())); - return time.withMinute(0).withSecond(0).withNano(0).toInstant(useTimeZone).toEpochMilli(); - } - - private void waitToNextRoutine () throws InterruptedException, NoNotifyTimeTag { - sleep(calcNextRoutineTimestamp(System.currentTimeMillis(), USE_TIME_ZONE, NOTIFY_AT_HOUR) - System.currentTimeMillis()); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/daemon/MornyDaemons.java b/src/main/old/cc/sukazyo/cono/morny/daemon/MornyDaemons.java deleted file mode 100644 index 710a07a..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/daemon/MornyDaemons.java +++ /dev/null @@ -1,34 +0,0 @@ -package cc.sukazyo.cono.morny.daemon; - -import cc.sukazyo.cono.morny.MornyCoeur; - -import static cc.sukazyo.cono.morny.Log.logger; - -public class MornyDaemons { - - public static final MedicationTimer medicationTimerInstance = new MedicationTimer(); - - public static void start () { - logger.info("ALL Morny Daemons starting..."); -// TrackerDataManager.init(); - medicationTimerInstance.start(); - MornyReport.onMornyLogIn(); - logger.info("Morny Daemons started."); - } - - public static void stop () { - - logger.info("ALL Morny Daemons stopping..."); - -// TrackerDataManager.DAEMON.interrupt(); - medicationTimerInstance.interrupt(); - -// TrackerDataManager.trackingLock.lock(); - try { medicationTimerInstance.join(); } catch (InterruptedException e) { e.printStackTrace(System.out); } - - MornyReport.onMornyExit(MornyCoeur.getExitReason()); - logger.info("ALL Morny Daemons STOPPED."); - - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java b/src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java deleted file mode 100644 index 8f49964..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/daemon/MornyReport.java +++ /dev/null @@ -1,159 +0,0 @@ -package cc.sukazyo.cono.morny.daemon; - -import cc.sukazyo.cono.morny.*; -import cc.sukazyo.cono.morny.bot.command.MornyInformation; -import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString; -import com.google.gson.GsonBuilder; -import com.pengrad.telegrambot.model.User; -import com.pengrad.telegrambot.model.request.ParseMode; -import com.pengrad.telegrambot.request.BaseRequest; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.response.BaseResponse; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import java.lang.reflect.Field; - -import static cc.sukazyo.cono.morny.Log.logger; -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class MornyReport { - - private static boolean unsupported () { - return !MornyCoeur.available() || MornyCoeur.config().reportToChat == -1; - } - - private static , R extends BaseResponse> void executeReport (@Nonnull T report) { - if (unsupported()) return; - try { - MornyCoeur.extra().exec(report); - } catch (EventRuntimeException.ActionFailed e) { - logger.warn("cannot execute report to telegram:"); - logger.warn(Log.exceptionLog(e).indent(4)); - logger.warn("tg-api response:"); - logger.warn(e.getResponse().toString().indent(4)); - } - } - - public static void exception (@Nonnull Throwable e, @Nullable String description) { - if (unsupported()) return; - executeReport(new SendMessage( - MornyCoeur.config().reportToChat, - String.format(""" - ▌Coeur Unexpected Exception - %s -

      %s
      %s - """, - description == null ? "" : escapeHtml(description)+"\n", - escapeHtml(Log.exceptionLog(e)), - e instanceof EventRuntimeException.ActionFailed ? (String.format( - "\n\ntg-api error:\n
      %s
      ", - new GsonBuilder().setPrettyPrinting().create().toJson(((EventRuntimeException.ActionFailed)e).getResponse())) - ) : "" - ) - ).parseMode(ParseMode.HTML)); - } - - public static void exception (@Nonnull Exception e) { exception(e, null); } - - public static void unauthenticatedAction (@Nonnull String action, @Nonnull User user) { - if (unsupported()) return; - executeReport(new SendMessage( - MornyCoeur.config().reportToChat, - String.format(""" - ▌User unauthenticated action - action: %s - by user %s - """, - escapeHtml(action), - TGToString.as(user).fullnameRefHtml() - ) - ).parseMode(ParseMode.HTML)); - } - - /** - * morny 登陆时的报告发送,包含已登录的账号 id 以及启动配置。 - * @since 1.0.0-alpha6 - */ - static void onMornyLogIn () { - executeReport(new SendMessage( - MornyCoeur.config().reportToChat, - String.format(""" - ▌Morny Logged in - -v %s - as user @%s - - as config fields: - %s - """, - MornyInformation.getVersionAllFullTagHTML(), - MornyCoeur.getUsername(), - sectionConfigFields(MornyCoeur.config()) - ) - ).parseMode(ParseMode.HTML)); - } - - /** - * 返回一个 config 字段与值的列表,可以作为 telegram html 格式输出 - * @since 1.0.0-alpha6 - */ - private static String sectionConfigFields (@Nonnull MornyConfig config) { - final StringBuilder echo = new StringBuilder(); - for (Field field : config.getClass().getFields()) { - echo.append("- ").append(field.getName()).append(" "); - try { - if (field.isAnnotationPresent(MornyConfig.Sensitive.class)) { - echo.append(": sensitive_field"); - } else { - final Object fieldValue = field.get(config); - echo.append("= "); - if (fieldValue == null) - echo.append("null"); - else echo.append("").append(escapeHtml(fieldValue.toString())).append(""); - } - } catch (IllegalAccessException | IllegalArgumentException | NullPointerException e) { - echo.append(": ").append(escapeHtml("")).append(""); - logger.error("error while reading config field " + field.getName()); - logger.error(Log.exceptionLog(e)); - exception(e, "error while reading config field " + field.getName()); - } - echo.append("\n"); - } - return echo.substring(0, echo.length()-1); - } - - /** - * morny 关闭/登出时发送的报告. - *

      - * 基于 java 的程序关闭钩子,因此仍然无法在意外宕机的情况下发送报告. - * @param causedBy - * 关闭的原因。 - * 可以使用 {@link User Telegram 用户对象} 表示由一个用户执行了关闭, - * 传入其它数据将使用 {@code #toString} 输出其内容。 - * 传入 {@link null} 则表示不表明原因。 - */ - static void onMornyExit (@Nullable Object causedBy) { - if (unsupported()) return; - String causedTag = null; - if (causedBy != null) { - if (causedBy instanceof User) - causedTag = TGToString.as((User)causedBy).fullnameRefHtml(); - else - causedTag = "" + escapeHtml(causedBy.toString()) + ""; - } - executeReport(new SendMessage( - MornyCoeur.config().reportToChat, - String.format(""" - ▌Morny Exited - from user @%s - %s - """, - MornyCoeur.getUsername(), - causedBy == null ? "with UNKNOWN reason" : "\nby " + causedTag - ) - ).parseMode(ParseMode.HTML)); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java b/src/main/old/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java deleted file mode 100644 index f8a99fc..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/daemon/TrackerDataManager.java +++ /dev/null @@ -1,174 +0,0 @@ -package cc.sukazyo.cono.morny.daemon; - -import java.io.File; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.charset.StandardCharsets; -import java.nio.file.StandardOpenOption; -import java.util.HashMap; -import java.util.TreeSet; -import java.util.concurrent.locks.ReentrantLock; - -import static cc.sukazyo.cono.morny.Log.exceptionLog; -import static cc.sukazyo.cono.morny.Log.logger; - -public class TrackerDataManager { - - /** {@link TrackerDaemon} 的锁。保证在程序中只有一个 TrackerDaemon 会运行。 */ - public static final ReentrantLock trackingLock = new ReentrantLock(); - /** {@link #record Tracker 缓存}的锁

      为保证对 Tracker 缓存的操作不会造成线程冲突,在操作缓存数据前应先取得此锁。 */ - private static final ReentrantLock recordLock = new ReentrantLock(); - /** Tracker 数据的内存缓存

      进行数据操作前请先取得对应的{@link #recordLock 锁} */ - private static HashMap>> record = new HashMap<>(); - - public static final TrackerDaemon DAEMON = new TrackerDaemon(); - public static class TrackerDaemon extends Thread { - - public TrackerDaemon () { this.setName("TRACKER"); } - - @Override - public void run () { - trackingLock.lock(); - logger.info("Tracker started."); - long lastWaitTimestamp = System.currentTimeMillis(); - boolean postProcess = false; - do { - lastWaitTimestamp += 10 * 60 * 1000; - long sleeping = lastWaitTimestamp - System.currentTimeMillis(); - if (sleeping > 0) { - try { Thread.sleep(sleeping); } catch (InterruptedException e) { interrupt(); } - } else { - logger.warn("Tracker may be too busy to process data!!"); - lastWaitTimestamp = System.currentTimeMillis(); - } - if (interrupted()) { - postProcess = true; - logger.info("CALLED TO EXIT! writing cache."); - } - if (record.size() != 0) { - save(); - } - else logger.info("nothing to do yet"); - } while (!postProcess); - trackingLock.unlock(); - logger.info("Tracker exited."); - } - - } - - /** - * 向 Tracker 缓存写入一条 tracker 数据. - *

      - * 这个方法对于 Tracker 缓存是原子化的。 - * - * @param chat tracker 所属的 Telegram Chat ID - * @param user tracker 所记录的 Telegram User ID - * @param timestamp tracker 被生成时的 UTC 时间戳 - */ - public static void record (long chat, long user, long timestamp) { - recordLock.lock(); - if (!record.containsKey(chat)) record.put(chat, new HashMap<>()); - HashMap> chatUsers = record.get(chat); - if (!chatUsers.containsKey(user)) chatUsers.put(user, new TreeSet<>()); - TreeSet userRecords = chatUsers.get(user); - userRecords.add(timestamp); - recordLock.unlock(); - } - - /** - * 开启 {@link TrackerDaemon}. - *

      - * 由于 Tracker 已废弃,这个方法已无作用。 - */ - @SuppressWarnings("unused") - public static void init () { - DAEMON.start(); - } - - /** - * 执行 Tracker 的保存逻辑. - * @see #reset() 弹出 Tracker 缓存 - * @see #save(HashMap) 执行硬盘写入操作 - */ - public static void save () { - logger.info("start writing tracker data."); - save(reset()); - logger.info("done writing tracker data."); - } - - /** - * 将 Tracker 的缓存数据弹出. - *

      - * 这个方法将返回现在 Tracker 的所有缓存数据,然后清除缓存。 - *

      - * 这个方法对于 Tracker 缓存是原子化的。 - * - * @return 当前 Tracker 所包含的内容 - */ - private static HashMap>> reset () { - recordLock.lock(); - HashMap>> recordOld = record; - record = new HashMap<>(); - recordLock.unlock(); - return recordOld; - } - - /** - * 将 Tracker 数据写入到硬盘. - * - * @param record 需要保存的 Tracker 数据集 - */ - private static void save (HashMap>> record) { - - { - if (!record.containsKey(0L)) record.put(0L, new HashMap<>()); - HashMap> chatUsers = record.get(0L); - if (!chatUsers.containsKey(0L)) chatUsers.put(0L, new TreeSet<>()); - TreeSet userRecords = chatUsers.get(0L); - userRecords.add(System.currentTimeMillis()); - } - - record.forEach((chat, chatUsers) -> chatUsers.forEach((user, userRecords) -> { - - long dayCurrent = -1; - FileChannel channelCurrent = null; - - for (long timestamp : userRecords) { - try { - - long day = timestamp / (24 * 60 * 60 * 1000); - if (dayCurrent != day) { - if (channelCurrent != null) channelCurrent.close(); - channelCurrent = openFile(chat, user, day); - dayCurrent = day; - } - - assert channelCurrent != null; - final int result = channelCurrent.write(ByteBuffer.wrap( - String.format("%d\n", timestamp).getBytes(StandardCharsets.UTF_8) - )); - - if (result == 0) logger.warn("writing tracker data %d/%d/%d: write only 0 bytes! is anything wrong?"); - - } catch (Exception e) { - final String message = String.format("exception in write tracker data: %d/%d/%d", chat, user, timestamp); - logger.error(message); - logger.error(exceptionLog(e)); - MornyReport.exception(e, message); - } - } - - })); - - } - - private static FileChannel openFile (long chat, long user, long day) throws IOException { - File data = new File(String.format("./data/tracker/%d/%d", chat, user)); - if (!data.isDirectory()) if (!data.mkdirs()) throw new IOException("Cannot create file directory " + data.getPath()); - File file = new File(data, String.valueOf(day)); - if (!file.isFile()) if (!file.createNewFile()) throw new IOException("Cannot create file " + file.getPath()); - return FileChannel.open(file.toPath(), StandardOpenOption.APPEND); - } - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/data/TelegramImages.java b/src/main/old/cc/sukazyo/cono/morny/data/TelegramImages.java deleted file mode 100644 index f433c65..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/data/TelegramImages.java +++ /dev/null @@ -1,81 +0,0 @@ -package cc.sukazyo.cono.morny.data; - -import cc.sukazyo.cono.morny.MornyAssets; -import cc.sukazyo.cono.morny.daemon.MornyReport; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.IOException; -import java.io.InputStream; - -import static cc.sukazyo.cono.morny.Log.exceptionLog; -import static cc.sukazyo.cono.morny.Log.logger; - -/** - * The images of morny will use. - * - * @since 1.0.0-RC4 - */ -public class TelegramImages { - - /** - * Image that stored in the {@link MornyAssets#pack}. - *

      - * It has a final {@link #assetsPath} record that is its store location, - * and has a {@link #cache} of the binary data. - * - * @since 1.0.0-RC4 - */ - public static class AssetsFileImage { - - /** the path where the image is stored in {@link MornyAssets#pack}. */ - @Nonnull private final String assetsPath; - /** the binary data cache of the image.

      {@link null} means it hasn't been cached. */ - @Nullable private byte[] cache = null; - - /** - * An {@link AssetsFileImage}. - * - * @param path the image path relative to {@link MornyAssets#pack}'s root. - */ - AssetsFileImage (@Nonnull String path) { - this.assetsPath = path; - } - - /** - * Get the binary data. - *

      - * Will read the {@link #cache} firstly. If read {@link null}, - * then it will try {@link #read load the file}, and read again. - * - * @return The binary data of the image. - * @throws IllegalStateException While the {@link #read()} failed to read data and - * the result {@link #cache} is still null - */ - @Nonnull public byte[] get() { - if (cache == null) read(); - if (cache == null) throw new IllegalStateException("Failed get assets file image."); - return cache; - } - - /** - * Load the file from {@link MornyAssets#pack}, and stored the binary data to {@link #cache}. - *

      - * If failed, it will output the exception to the log and the {@link MornyReport}, - * and remains the cache's current data. - */ - private void read() { - try (InputStream stream = MornyAssets.pack.getResource(assetsPath).read()) { - this.cache = stream.readAllBytes(); - } catch (IOException e) { - logger.error("Cannot read resource file"); - logger.error(exceptionLog(e)); - MornyReport.exception(e, "Cannot read resource file"); - } - } - - } - - public static final AssetsFileImage IMG_ABOUT = new AssetsFileImage("images/featured-image@0.5x.jpg"); - -} diff --git a/src/main/old/cc/sukazyo/cono/morny/data/TelegramStickers.java b/src/main/old/cc/sukazyo/cono/morny/data/TelegramStickers.java deleted file mode 100644 index d23d1fe..0000000 --- a/src/main/old/cc/sukazyo/cono/morny/data/TelegramStickers.java +++ /dev/null @@ -1,87 +0,0 @@ -package cc.sukazyo.cono.morny.data; - -import cc.sukazyo.cono.morny.util.tgapi.ExtraAction; -import com.pengrad.telegrambot.request.SendMessage; -import com.pengrad.telegrambot.request.SendSticker; -import com.pengrad.telegrambot.response.SendResponse; - -import javax.annotation.Nonnull; -import java.lang.reflect.Field; - -/** - * 存放 bot 使用到的贴纸 - * @since 0.4.2.0 - */ -public class TelegramStickers { - - public static final String ID_ONLINE_STATUS_RETURN = "CAACAgEAAx0CW-CvvgAC5eBhhhODGRuu0pxKLwoQ3yMsowjviAACcycAAnj8xgVVU666si1utiIE"; - public static final String ID_HELLO = "CAACAgEAAxkBAAMnYYYWKNXO4ibo9dlsmDctHhhV6fIAAqooAAJ4_MYFJJhrHS74xUAiBA"; - public static final String ID_EXIT = "CAACAgEAAxkBAAMoYYYWt8UjvP0N405SAyvg2SQZmokAAkMiAAJ4_MYFw6yZLu06b-MiBA"; - public static final String ID_403 = "CAACAgEAAxkBAAMqYYYa_7hpXH6hMOYMX4Nh8AVYd74AAnQnAAJ4_MYFRdmmsQKLDZgiBA"; - public static final String ID_404 = "CAACAgEAAx0CSQh32gABA966YbRJpbmi2lCHINBDuo1DknSTsbsAAqUoAAJ4_MYFUa8SIaZriAojBA"; - public static final String ID_WAITING = "CAACAgEAAx0CSQh32gABA-8DYbh7W2VhJ490ucfZMUMrgMR2FW4AAm4nAAJ4_MYFjx6zpxJPWsQjBA"; - public static final String ID_SENT = "CAACAgEAAx0CSQh32gABA--zYbiyU_wOijEitp-0tSl_k7W6l3gAAgMmAAJ4_MYF4GrompjXPx4jBA"; - public static final String ID_SAVED = "CAACAgEAAx0CSQh32gABBExuYdB_G0srfhQldRWkBYxWzCOv4-IAApooAAJ4_MYFcjuNZszfQcQjBA"; - public static final String ID_PROGYNOVA = "CAACAgUAAxkBAAICm2KEuL7UQqNP7vSPCg2DHJIND6UsAAKLAwACH4WSBszIo722aQ3jJAQ"; - public static final String ID_NETWORK_ERR = "CAACAgEAAxkBAAID0WNJgNEkD726KW4vZeFlw0FlVVyNAAIXJgACePzGBb50o7O1RbxoKgQ"; - - /** - * 向 telegram 输出当前的 {@link TelegramStickers} 中的所有 stickers. - * @param actionObject 要使用的 telegram account 包装实例 - * @param sentChat 目标 telegram chat id - * @param replyToMessageId 输出时回复指定的消息的 id。使用 {@link -1} 表示不回复消息 - * @since 0.8.0.6 - */ - public static void echoAllStickers (@Nonnull ExtraAction actionObject, long sentChat, int replyToMessageId) { - - for (Field object : TelegramStickers.class.getFields()) { - if (object.getType()==String.class && object.getName().startsWith("ID_")) { - try { - - final String stickerId = (String)object.get(""); - SendSticker echo = new SendSticker(sentChat, stickerId); - SendMessage echoName = new SendMessage(sentChat, object.getName()); - if (replyToMessageId!=-1) echo.replyToMessageId(replyToMessageId); - SendResponse echoedName = actionObject.exec(echoName); - actionObject.exec(echo.replyToMessageId(echoedName.message().messageId())); - - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - - } - - /** - * 向 telegram 输出当前的 {@link TelegramStickers} 中的某个特定 sticker. - * @param stickerFieldID 要输出的 sticker 在 {@link TelegramStickers} 当中的字段名 - * @param actionObject 要使用的 telegram account 包装实例 - * @param sentChat 目标 telegram chat id - * @param replyToMessageId 输出时回复指定的消息的 id。使用 {@link -1} 表示不回复消息 - * @since 0.8.0.6 - */ - public static void echoStickerByID ( - @Nonnull String stickerFieldID, - @Nonnull ExtraAction actionObject, long sentChat, int replyToMessageId - ) { - try { - // normally get the sticker and echo - Field sticker = TelegramStickers.class.getField(stickerFieldID); - SendMessage echoName = new SendMessage(sentChat, sticker.getName()); - SendSticker echo = new SendSticker(sentChat, (String)sticker.get("")); - if (replyToMessageId!=-1) echo.replyToMessageId(replyToMessageId); - SendResponse echoedName = actionObject.exec(echoName); - actionObject.exec(echo.replyToMessageId(echoedName.message().messageId())); - } catch (NoSuchFieldException e) { - // no such sticker found - SendSticker echo404 = new SendSticker(sentChat, TelegramStickers.ID_404); - if (replyToMessageId!=-1) echo404.replyToMessageId(replyToMessageId); - actionObject.exec(echo404); - } catch (IllegalAccessException e) { - // java-reflect get sticker FILE_ID failed - throw new RuntimeException(e); - } - } - -} diff --git a/src/main/scala/cc/sukazyo/cono/morny/Log.scala b/src/main/scala/cc/sukazyo/cono/morny/Log.scala new file mode 100644 index 0000000..5ca1a38 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/Log.scala @@ -0,0 +1,29 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.messiva.logger.Logger +import cc.sukazyo.messiva.appender.ConsoleAppender +import cc.sukazyo.messiva.formatter.SimpleFormatter +import cc.sukazyo.messiva.log.LogLevel + +import java.io.{PrintWriter, StringWriter} + +object Log { + + val logger: Logger = Logger( + ConsoleAppender( + SimpleFormatter() + ) + ).minLevel(LogLevel.INFO) + + def debug: Boolean = logger.levelSetting.minLevel.level <= LogLevel.DEBUG.level + + def debug(is: Boolean): Unit = + if is then logger.minLevel(LogLevel.ALL) + else logger.minLevel(LogLevel.INFO) + + def exceptionLog (e: Throwable): String = + val stackTrace = StringWriter() + e printStackTrace PrintWriter(stackTrace) + stackTrace toString + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyAbout.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyAbout.scala new file mode 100644 index 0000000..9d608ed --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyAbout.scala @@ -0,0 +1,17 @@ +package cc.sukazyo.cono.morny + +import java.io.IOException + +object MornyAbout { + + val MORNY_PREVIEW_IMAGE_ASCII: String = + try { MornyAssets.pack getResource "texts/server-hello.txt" readAsString } + catch case e: IOException => + throw RuntimeException("Cannot read MORNY_PREVIEW_IMAGE_ASCII from assets pack", e) + + val MORNY_SOURCECODE_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono" + val MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK = "https://storage.sukazyo.cc/Eyre_S/Coeur-Morny-Cono" + val MORNY_ISSUE_TRACKER_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono/issues" + val MORNY_USER_GUIDE_LINK = "https://book.sukazyo.cc/morny" + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala new file mode 100644 index 0000000..489ac36 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.restools.ResourcesPackage + +object MornyAssets { + + val pack: ResourcesPackage = ResourcesPackage(MornyAssets.getClass, "assets_morny") + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala new file mode 100644 index 0000000..6bfad74 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -0,0 +1,149 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.cono.morny.bot.command.MornyCommands +import cc.sukazyo.cono.morny.daemon.MornyDaemons +import cc.sukazyo.cono.morny.util.tgapi.ExtraAction +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.MornyCoeur.THREAD_MORNY_EXIT +import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener +import cc.sukazyo.cono.morny.bot.event.MornyEventListeners +import com.pengrad.telegrambot.TelegramBot +import com.pengrad.telegrambot.request.GetMe + +import scala.util.boundary +import scala.util.boundary.break + +object MornyCoeur { + + val THREAD_MORNY_EXIT = "morny-exiting" + + private var INSTANCE: MornyCoeur|Null = _ + + val coeurStartTimestamp: Long = ServerMain.systemStartupTime + + def account: TelegramBot = INSTANCE.account + def username: String = INSTANCE.username + def userid: Long = INSTANCE.userid + def config: MornyConfig = INSTANCE.my_config + def trusted: MornyTrusted = INSTANCE.trusted + def extra: ExtraAction = INSTANCE.extra + + def available: Boolean = INSTANCE != null + def callSaveData(): Unit = INSTANCE.saveDataAll() + + def exitReason: AnyRef|Null = INSTANCE.whileExit_reason + + def exit (status: Int, reason: AnyRef): Unit = + INSTANCE.whileExit_reason = reason + System exit status + + def init (using config: MornyConfig): Unit = { + + if (INSTANCE ne null) + logger error "Coeur already started!!!" + return; + + logger info "Coeur starting..." + INSTANCE = MornyCoeur() + + MornyDaemons.start() + + logger info "start telegram event listening" + MornyEventListeners.registerAllEvents() + INSTANCE.account.setUpdatesListener(TelegramUpdatesListener) + + if config.commandLoginRefresh then + logger info "resetting telegram command list" + MornyCommands.automaticTGListUpdate() + + logger info "Coeur start complete." + + } + +} + +class MornyCoeur (using config: MornyConfig) { + def my_config: MornyConfig = config + + logger info s"args key:\n ${config.telegramBotKey}" + if config.telegramBotUsername ne null then + logger info s"login as:\n ${config.telegramBotUsername}" + + private val __loginResult = login() + if (__loginResult eq null) + logger error "Login to bot failed." + System exit -1 + + configure_exitCleanup() + + val account: TelegramBot = __loginResult.account + val username: String = __loginResult.username + val userid: Long = __loginResult.userid + + val trusted: MornyTrusted = MornyTrusted(using this) + val extra: ExtraAction = ExtraAction as __loginResult.account + var whileExit_reason: AnyRef|Null = _ + + def saveDataAll(): Unit = { + // nothing to do + logger info "done all save action." + } + + private def exitCleanup (): Unit = { + MornyDaemons.stop() + if config.commandLogoutClear then + MornyCommands.automaticTGListRemove() + } + + private def configure_exitCleanup (): Unit = { + Runtime.getRuntime.addShutdownHook(new Thread(() => exitCleanup(), THREAD_MORNY_EXIT)) + } + + private case class LoginResult(account: TelegramBot, username: String, userid: Long) + + private def login (): LoginResult|Null = { + + val builder = TelegramBot.Builder(config.telegramBotKey) + var api_bot = config.telegramBotApiServer + var api_file = config.telegramBotApiServer4File + if (api_bot ne null) + if api_bot endsWith "/" then api_bot = api_bot dropRight 1 + if !(api_bot endsWith "/bot") then api_bot += "/bot" + builder.apiUrl(api_bot) + if (api_file ne null) + if api_file endsWith "/file/" then api_file = api_file dropRight 1 + if !(api_file endsWith "/file/bot") then api_file += "/file/bot" + builder.apiUrl(api_bot) + if ((api_bot ne null) || (api_file ne null)) + logger info + s"""Telegram bot api set to: + |- bot: $api_bot + |- file: $api_file""" + .stripMargin + + val account = builder build + + logger info "Trying to login..." + boundary[LoginResult|Null] { + for (i <- 0 to 3) { + if i > 0 then logger info "retrying..." + try { + val remote = (account execute GetMe()).user + if ((config.telegramBotUsername ne null) && config.telegramBotUsername != remote.username) + throw RuntimeException(s"Required the bot @${config.telegramBotUsername} but @${remote.username} logged in") + logger info s"Succeed logged in to @${remote.username}" + break(LoginResult(account, remote.username, remote.id)) + } catch + case r: boundary.Break[LoginResult|Null] => throw r + case e => + logger error + s"""${exceptionLog(e)} + |login failed""" + .stripMargin + } + null + } + + } + +} diff --git a/src/main/old/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java similarity index 97% rename from src/main/old/cc/sukazyo/cono/morny/MornyConfig.java rename to src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java index a3d9907..2702a61 100644 --- a/src/main/old/cc/sukazyo/cono/morny/MornyConfig.java +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java @@ -129,7 +129,7 @@ public class MornyConfig { * End Configs | ConfigBuilder * * ======================================= */ - public MornyConfig (@Nonnull Prototype prototype) throws CheckFailure { + private MornyConfig (@Nonnull Prototype prototype) throws CheckFailure { this.telegramBotApiServer = prototype.telegramBotApiServer; this.telegramBotApiServer4File = prototype.telegramBotApiServer4File; if (prototype.telegramBotKey == null) throw new CheckFailure.NullTelegramBotKey(); @@ -159,6 +159,10 @@ public class MornyConfig { public static class Prototype { + public MornyConfig build () { + return new MornyConfig(this); + } + @Nullable public String telegramBotApiServer = null; @Nullable public String telegramBotApiServer4File = null; @Nullable public String telegramBotKey = null; diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala new file mode 100644 index 0000000..69fb74f --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala @@ -0,0 +1,48 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.cono.morny.internal.BuildConfigField +import cc.sukazyo.cono.morny.util.FileUtils + +import java.io.IOException +import java.net.URISyntaxException +import java.security.NoSuchAlgorithmException +import Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.daemon.MornyReport + +object MornySystem { + + @BuildConfigField val VERSION: String = BuildConfig.VERSION + @BuildConfigField val VERSION_FULL: String = BuildConfig.VERSION_FULL + @BuildConfigField val VERSION_BASE: String = BuildConfig.VERSION_BASE + @BuildConfigField val VERSION_DELTA: String = BuildConfig.VERSION_DELTA + @BuildConfigField val CODENAME: String = BuildConfig.CODENAME + @BuildConfigField val CODE_STORE: String = BuildConfig.CODE_STORE + @BuildConfigField val COMMIT_PATH: String = BuildConfig.COMMIT_PATH + + @BuildConfigField + def isUseDelta: Boolean = VERSION_DELTA ne null + + @BuildConfigField + def isGitBuild: Boolean = BuildConfig.COMMIT ne null + + @BuildConfigField + def isCleanBuild: Boolean = BuildConfig.CLEAN_BUILD + + def currentCodePath: String|Null = + if ((COMMIT_PATH eq null) || (!isGitBuild)) null + else COMMIT_PATH.formatted(BuildConfig.COMMIT) + + def getJarMD5: String = { + try { + FileUtils.getMD5Three(MornySystem.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.getPath) + } catch + //noinspection ScalaUnnecessaryParentheses + case _: (IOException|URISyntaxException) => + "" + case n: NoSuchAlgorithmException => + logger error exceptionLog(n) + MornyReport.exception(n, "") + "" + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala new file mode 100644 index 0000000..d042e80 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala @@ -0,0 +1,16 @@ +package cc.sukazyo.cono.morny + +import com.pengrad.telegrambot.model.ChatMember.Status + +class MornyTrusted (using coeur: MornyCoeur)(using config: MornyConfig) { + + def isTrusted (userId: Long): Boolean = + if userId == config.trustedMaster then true + else if config.trustedChat == -1 then false + else coeur.extra isUserInGroup(userId, config.trustedChat, Status.administrator) + + def isTrusted_dinnerReader (userId: Long): Boolean = + if userId == config.trustedMaster then true + else config.dinnerTrustedReaders contains userId + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala index 91907ae..b481eb0 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala @@ -109,7 +109,7 @@ object ServerMain { | Morny ${MornySystem.CODENAME toUpperCase} | ${MornySystem.VERSION_BASE}${if (MornySystem.isUseDelta) "-δ"+MornySystem.VERSION_DELTA else ""} |- md5hash : - | ${MornySystem.getJarMd5} + | ${MornySystem.getJarMD5} |- gitstat : |${ if (MornySystem.isGitBuild) { s""" on commit ${if (MornySystem.isCleanBuild) "- clean-build" else "<δ/non-clean-build>"} @@ -128,7 +128,7 @@ object ServerMain { s"""ServerMain.java Loaded >>> |- version ${MornySystem.VERSION_FULL} |- Morny ${MornySystem.CODENAME toUpperCase} - |- <${MornySystem.getJarMd5}> [${BuildConfig.CODE_TIMESTAMP}]""".stripMargin + |- <${MornySystem.getJarMD5}> [${BuildConfig.CODE_TIMESTAMP}]""".stripMargin /// /// Check Coeur arguments @@ -143,7 +143,7 @@ object ServerMain { Thread.currentThread setName THREAD_MORNY_INIT try - MornyCoeur.init(new MornyConfig(config)) + MornyCoeur.init(using config build) catch { case _: CheckFailure.NullTelegramBotKey => logger.info("Parameter required has no value:\n --token.") diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala index a2e38b7..7971d95 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala @@ -19,12 +19,12 @@ object DirectMsgClear extends ISimpleCommand { logger debug "executing command /r" if (event.message.replyToMessage == null) return; logger trace "message is a reply" - if (event.message.replyToMessage.from.id != MornyCoeur.getUserid) return; + if (event.message.replyToMessage.from.id != MornyCoeur.userid) return; logger trace "message replied is from me" if (System.currentTimeMillis/1000 - event.message.replyToMessage.date > 48*60*60) return; logger trace "message is not outdated(48 hrs ago)" - val isTrusted = MornyCoeur.trustedInstance isTrusted event.message.from.id + val isTrusted = MornyCoeur.trusted isTrusted event.message.from.id def _isReplyTrusted: Boolean = if (event.message.replyToMessage.replyToMessage == null) false else if (event.message.replyToMessage.replyToMessage.from.id == event.message.from.id) true diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala index 8bb3987..975c47b 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala @@ -57,7 +57,7 @@ object Encryptor extends ITelegramCommand { val _r = event.message.replyToMessage if ((_r ne null) && (_r.document ne null)) { try {XFile( - MornyCoeur.getAccount getFileContent (MornyCoeur.extra exec GetFile(_r.document.fileId)).file, + MornyCoeur.account getFileContent (MornyCoeur.extra exec GetFile(_r.document.fileId)).file, _r.document.fileName )} catch case e: IOException => logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" @@ -74,7 +74,7 @@ object Encryptor extends ITelegramCommand { _photo_size = _size if (_photo_origin eq null) throw IllegalArgumentException("no photo from api.") XFile( - MornyCoeur.getAccount getFileContent (MornyCoeur.extra exec GetFile(_photo_origin.fileId)).file, + MornyCoeur.account getFileContent (MornyCoeur.extra exec GetFile(_photo_origin.fileId)).file, s"photo${byteArrayToHex(hashMd5(System.currentTimeMillis toString)) substring 32-12 toUpperCase}.png" ) } catch diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala index 6118412..5a6d9e8 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala @@ -40,7 +40,7 @@ object EventHack extends ITelegramCommand { ) x_mode match case "any" => - if (MornyCoeur.trustedInstance isTrusted event.message.from.id) + if (MornyCoeur.trusted isTrusted event.message.from.id) doRegister(HackType ANY) done_ok else done_forbiddenForAny diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala index f4c414b..7799e06 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala @@ -39,7 +39,7 @@ object GetUsernameAndId extends ITelegramCommand { } else if (event.message.replyToMessage eq null) event.message.from.id else event.message.replyToMessage.from.id - val response = MornyCoeur.getAccount execute GetChatMember(event.message.chat.id, userId) + val response = MornyCoeur.account execute GetChatMember(event.message.chat.id, userId) if (response.chatMember eq null) MornyCoeur.extra exec SendMessage( diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala index 5d78ea1..b49f50d 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala @@ -65,12 +65,12 @@ object IP186Query { } catch case e: Exception => import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h - MornyCoeur.extra().exec(new SendMessage( + MornyCoeur.extra exec new SendMessage( event.message().chat().id(), s"""[Exception] in query: |${h(e.getMessage)}""" .stripMargin - ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())); + ).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId()) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala index 7fa9876..a6fbdf8 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala @@ -3,20 +3,23 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.Log.logger import com.pengrad.telegrambot.model.{BotCommand, DeleteMyCommands, Update} import com.pengrad.telegrambot.request.{SendSticker, SetMyCommands} import scala.collection.{mutable, SeqMap} import scala.collection.mutable.ArrayBuffer import scala.language.postfixOps -import cc.sukazyo.cono.morny.Log.logger object MornyCommands { private type CommandMap = SeqMap[String, ISimpleCommand] private def CommandMap (commands: ISimpleCommand*): CommandMap = - val stash: mutable.SeqMap[String, ISimpleCommand] = mutable.SeqMap() - for (i <- commands) stash += ((i.name, i)) + val stash = mutable.SeqMap.empty[String, ISimpleCommand] + for (i <- commands) + stash += (i.name -> i) + if (i.aliases ne null) for (alias <- i.aliases) + stash += (alias.name -> i) stash private val commands: CommandMap = CommandMap( @@ -41,12 +44,14 @@ object MornyCommands { Testing, DirectMsgClear, + //noinspection NonAsciiCharacters 私わね, + //noinspection NonAsciiCharacters 喵呜.Progynova ) - @SuppressWarnings(Array("NonAsciiCharacters")) + //noinspection NonAsciiCharacters val commands_uni: CommandMap = CommandMap( 喵呜.抱抱, 喵呜.揉揉, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala index 9b3dae3..f9c46bb 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -1,10 +1,10 @@ package cc.sukazyo.cono.morny.bot.command +import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornyCoeur, MornySystem} import cc.sukazyo.cono.morny.data.{TelegramImages, TelegramStickers} import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h -import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornyCoeur, MornySystem} import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{SendMessage, SendPhoto, SendSticker} @@ -38,7 +38,7 @@ object MornyInformation extends ITelegramCommand { val action: String = command.getArgs()(0) action match { - case Subs.STICKERS => echoStickers + case s if s startsWith Subs.STICKERS => echoStickers case Subs.RUNTIME => echoRuntime case Subs.VERSION | Subs.VERSION_2 => echoVersion case _ => echo404 @@ -93,24 +93,41 @@ object MornyInformation extends ITelegramCommand { } private def echoStickers (using command: InputCommand, event: Update): Unit = { - val chat = event.message.chat.id - val replyTo = event.message.messageId - var sid: String|Null = null - if (command.getArgs()(0) == Subs.STICKERS) { - if (command.getArgs.length == 1) sid = "" - else if (command.getArgs.length == 2) sid = command.getArgs()(1) - } else if (command.getArgs.length == 1) { - if ((command.getArgs()(0) startsWith s"${Subs.STICKERS}.") || (command.getArgs()(0) startsWith s"${Subs.STICKERS}#")) { - sid = command.getArgs()(0) substring Subs.STICKERS.length+1 - } - } - if (sid == null) echo404 - else echoStickers(sid, chat, replyTo) + val mid: String|Null = + if (command.getArgs()(0) == Subs.STICKERS) { + if (command.getArgs.length == 1) "" + else if (command.getArgs.length == 2) command.getArgs()(1) + else null + } else if (command.getArgs.length == 1) { + if ((command.getArgs()(0) startsWith s"${Subs.STICKERS}.") || (command.getArgs()(0) startsWith s"${Subs.STICKERS}#")) { + command.getArgs()(0) substring Subs.STICKERS.length+1 + } else null + } else null + if (mid == null) echo404 + else echoStickers(mid)(using event.message.chat.id, event.message.messageId) } - private def echoStickers (sid: String, send_chat: Long, send_replyTo: Int): Unit = { - if (sid isEmpty) TelegramStickers echoAllStickers(MornyCoeur.extra, send_chat, send_replyTo) - else TelegramStickers echoStickerByID(sid, MornyCoeur.extra, send_chat, send_replyTo) + private def echoStickers (mid: String)(using send_chat: Long, send_replyTo: Int)(using Update): Unit = { + import scala.jdk.CollectionConverters.* + if (mid isEmpty) for ((_key, _file_id) <- TelegramStickers.map asScala) + echoSticker(_key, _file_id) + else { + try { + val sticker = TelegramStickers getById mid + echoSticker(sticker.getKey, sticker.getValue) + } catch case _: NoSuchFieldException => { + echo404 + } + } + } + + private def echoSticker (mid: String, file_id: String)(using send_chat: Long, send_replyTo: Int): Unit = { + val send_mid = SendMessage(send_chat, mid) + val send_sticker = SendSticker(send_chat, file_id) + if (send_replyTo != -1) send_mid.replyToMessageId(send_replyTo) + val result_send_mid = MornyCoeur.extra exec send_mid + send_sticker.replyToMessageId(result_send_mid.message.messageId) + MornyCoeur.extra exec send_sticker } private[command] def echoVersion (using event: Update): Unit = { @@ -123,7 +140,7 @@ object MornyInformation extends ITelegramCommand { |- Morny ${h(MornySystem.CODENAME toUpperCase)} |- ${h(MornySystem.VERSION_BASE)}$versionDeltaHTML${if (MornySystem.isGitBuild) "\n- " + versionGitHTML else ""} |coeur md5_hash: - |- ${h(MornySystem.getJarMd5)} + |- ${h(MornySystem.getJarMD5)} |coding timestamp: |- ${BuildConfig.CODE_TIMESTAMP} |- ${h(formatDate(BuildConfig.CODE_TIMESTAMP, 0))} [UTC] @@ -147,7 +164,7 @@ object MornyInformation extends ITelegramCommand { |- ${Runtime.getRuntime.availableProcessors} cores |coeur version: |- $getVersionAllFullTagHTML - |- ${h(MornySystem.getJarMd5)} + |- ${h(MornySystem.getJarMD5)} |- ${h(formatDate(BuildConfig.CODE_TIMESTAMP, 0))} [UTC] |- [${BuildConfig.CODE_TIMESTAMP}] |continuous: diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala index 7218dc9..bb69c48 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala @@ -24,7 +24,7 @@ object MornyManagers { val user = event.message.from - if (MornyCoeur.trustedInstance isTrusted user.id) { + if (MornyCoeur.trusted isTrusted user.id) { MornyCoeur.extra exec SendSticker( event.message.chat.id, @@ -59,7 +59,7 @@ object MornyManagers { val user = event.message.from - if (MornyCoeur.trustedInstance isTrusted user.id) { + if (MornyCoeur.trusted isTrusted user.id) { logger info s"call save from command by ${(TGToString as user) toStringLogTag}" MornyCoeur.callSaveData() diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala index d8c334a..68ad75c 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -1,6 +1,5 @@ package cc.sukazyo.cono.morny.bot.event -import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.data.TelegramStickers @@ -20,6 +19,7 @@ object OnCallMe extends EventListener { if update.message.text == null then return false if update.message.chat.`type` != (Chat.Type Private) then return false + //noinspection ScalaUnnecessaryParentheses (update.message.text toLowerCase) match case "steam" | "sbeam" | "sdeam" => requestItem(update.message.from, "STEAM LIBRARY") @@ -51,7 +51,7 @@ object OnCallMe extends EventListener { private def requestLastDinner (req: Message): Unit = { var isAllowed = false var lastDinnerData: Message|Null = null - if (MornyCoeur.trustedInstance isTrustedForDinnerRead req.from.id) { + if (MornyCoeur.trusted isTrusted_dinnerReader req.from.id) { lastDinnerData = (MornyCoeur.extra exec GetChat(MornyCoeur.config.dinnerChatId)).chat.pinnedMessage val sendResp = MornyCoeur.extra exec ForwardMessage( req.from.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala index a6fee41..d4bb8ca 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala @@ -46,6 +46,7 @@ object OnCallMsgSend extends EventListener { if e.url ne null then _parsed.url(e.url) if e.user ne null then _parsed.user(e.user) if e.language ne null then _parsed.language(e.language) + if e.customEmojiId ne null then _parsed.language(e.language) entities += _parsed MessageToSend(_body, entities toArray, parseMode, target) case _ => null @@ -59,7 +60,7 @@ object OnCallMsgSend extends EventListener { if message.text eq null then return false if !(message.text startsWith "*msg") then return false - if (!(MornyCoeur.trustedInstance isTrusted message.from.id)) + if (!(MornyCoeur.trusted isTrusted message.from.id)) MornyCoeur.extra exec SendSticker( message.chat.id, TelegramStickers ID_403 @@ -71,7 +72,7 @@ object OnCallMsgSend extends EventListener { if (message.replyToMessage eq null) return answer404 val messageToSend = MessageToSend from message.replyToMessage if ((messageToSend eq null) || (messageToSend.message eq null)) return answer404 - val sendResponse = MornyCoeur.getAccount execute messageToSend.toSendMessage() + val sendResponse = MornyCoeur.account execute messageToSend.toSendMessage() if (sendResponse isOk) { MornyCoeur.extra exec SendSticker( @@ -104,7 +105,7 @@ object OnCallMsgSend extends EventListener { if _toSend eq null then return answer404 else _toSend - val targetChatResponse = MornyCoeur.getAccount execute GetChat(messageToSend.targetId) + val targetChatResponse = MornyCoeur.account execute GetChat(messageToSend.targetId) if (targetChatResponse isOk) { def getChatDescriptionHTML (chat: Chat): String = import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h @@ -128,7 +129,7 @@ object OnCallMsgSend extends EventListener { } if messageToSend.message eq null then return true - val testSendResponse = MornyCoeur.getAccount execute messageToSend.toSendMessage(update.message.chat.id) + val testSendResponse = MornyCoeur.account execute messageToSend.toSendMessage(update.message.chat.id) .replyToMessageId(update.message.messageId) if (!(testSendResponse isOk)) MornyCoeur.extra exec SendMessage( diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala index 970aa2d..d4df473 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala @@ -2,7 +2,7 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.api.EventListener -import cc.sukazyo.cono.morny.bot.query.InlineQueryUnit +import cc.sukazyo.cono.morny.bot.query.{InlineQueryUnit, MornyQueries} import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.InlineQueryResult import com.pengrad.telegrambot.request.AnswerInlineQuery @@ -15,7 +15,7 @@ object OnInlineQuery extends EventListener { override def onInlineQuery (using update: Update): Boolean = { - val results: List[InlineQueryUnit[_]] = MornyCoeur.queryManager query update + val results: List[InlineQueryUnit[_]] = MornyQueries query update var cacheTime = Int.MaxValue var isPersonal = InlineQueryUnit.defaults.IS_PERSONAL diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala index 5b2ab47..4e90c9b 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala @@ -2,7 +2,7 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.MornyCoeur -import cc.sukazyo.cono.morny.daemon.MornyDaemons +import cc.sukazyo.cono.morny.daemon.{MedicationTimer, MornyDaemons} import com.pengrad.telegrambot.model.{Message, Update} object OnMedicationNotifyApply extends EventListener { @@ -14,7 +14,7 @@ object OnMedicationNotifyApply extends EventListener { private def editedMessageProcess (edited: Message): Boolean = { if edited.chat.id != MornyCoeur.config.medicationNotifyToChat then return false - MornyDaemons.medicationTimerInstance.refreshNotificationWrite(edited) + MedicationTimer.refreshNotificationWrite(edited) true } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala index 526af48..b82a66a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala @@ -22,7 +22,7 @@ object OnTelegramCommand extends EventListener { if (!(inputCommand.getCommand matches "^\\w+$")) logger debug "not command" false - else if ((inputCommand.getTarget ne null) && (inputCommand.getTarget ne MornyCoeur.getUsername)) + else if ((inputCommand.getTarget ne null) && (inputCommand.getTarget ne MornyCoeur.username)) logger debug "not morny command" false else diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala index 5f13657..0c06dc4 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala @@ -5,7 +5,7 @@ import com.pengrad.telegrambot.model.Update import scala.collection.mutable.ListBuffer -class MornyQueries { +object MornyQueries { private val queryInstances = Set[ITelegramQuery]( RawText, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala index 14a4e81..6275c2e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala @@ -14,7 +14,7 @@ object MyInformation extends ITelegramQuery { override def query (event: Update): List[InlineQueryUnit[_]] | Null = { - if (event.inlineQuery.query == null || (event.inlineQuery.query isBlank)) return null + if ((event.inlineQuery.query ne null) || (event.inlineQuery.query nonEmpty)) return null List( InlineQueryUnit(InlineQueryResultArticle( diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala new file mode 100644 index 0000000..644abe2 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala @@ -0,0 +1,90 @@ +package cc.sukazyo.cono.morny.daemon + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import com.pengrad.telegrambot.model.{Message, MessageEntity} +import com.pengrad.telegrambot.request.{EditMessageText, SendMessage} +import com.pengrad.telegrambot.response.SendResponse + +import java.time.{LocalDateTime, ZoneOffset} +import scala.collection.mutable.ArrayBuffer +import scala.language.implicitConversions + +object MedicationTimer extends Thread { + + private val NOTIFY_MESSAGE = "🍥⏲" + private val DAEMON_THREAD_NAME_DEF = "MedicationTimer" + + private val use_timeZone = MornyCoeur.config.medicationTimerUseTimezone + import scala.jdk.CollectionConverters.SetHasAsScala + private val notify_atHour: Set[Int] = MornyCoeur.config.medicationNotifyAt.asScala.toSet.map(_.intValue) + private val notify_toChat = MornyCoeur.config.medicationNotifyToChat + + this.setName(DAEMON_THREAD_NAME_DEF) + + private var lastNotify_messageId: Int|Null = _ + + override def run (): Unit = { + logger info "Medication Timer started." + while (!this.isInterrupted) { + try { + waitToNextRoutine() + sendNotification() + } catch + case _: InterruptedException => + interrupt() + logger info "MedicationTimer was interrupted, will be exit now" + case ill: IllegalArgumentException => + logger warn "MedicationTimer will not work due to: " + ill.getMessage + interrupt() + case e => + logger error + s"""unexpected error occurred on NotificationTimer + |${exceptionLog(e)}""" + .stripMargin + MornyReport.exception(e) + } + logger info "Medication Timer stopped." + } + + private def sendNotification(): Unit = { + val sendResponse: SendResponse = MornyCoeur.extra exec SendMessage(notify_toChat, NOTIFY_MESSAGE) + if sendResponse isOk then lastNotify_messageId = sendResponse.message.messageId + else lastNotify_messageId = null + } + + def refreshNotificationWrite (edited: Message): Unit = { + if lastNotify_messageId != (edited.messageId toInt) then return + import cc.sukazyo.cono.morny.util.CommonFormat.formatDate + val editTime = formatDate(edited.editDate*1000, use_timeZone.getTotalSeconds/60/60) + val entities = ArrayBuffer.empty[MessageEntity] + if edited.entities ne null then entities ++= edited.entities + entities += MessageEntity(MessageEntity.Type.italic, edited.text.length + "\n-- ".length, editTime.length) + MornyCoeur.extra exec EditMessageText( + notify_toChat, + edited.messageId, + edited.text + s"\n-- $editTime --" + ).entities(entities toArray:_*) + lastNotify_messageId = null + } + + @throws[IllegalArgumentException] + private[daemon] def calcNextRoutineTimestamp (baseTimeMillis: Long, zone: ZoneOffset, notifyAt: Set[Int]): Long = { + if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set") + var time = LocalDateTime.ofEpochSecond( + baseTimeMillis/1000, ((baseTimeMillis%1000)*1000*1000) toInt, + zone + ).withMinute(0).withSecond(0).withNano(0) + time = time plusHours 1 + while (!(notifyAt contains (time getHour))) { + time = time plusHours 1 + } + (time toInstant zone) toEpochMilli + } + + @throws[InterruptedException | IllegalArgumentException] + private def waitToNextRoutine (): Unit = { + Thread sleep calcNextRoutineTimestamp(System.currentTimeMillis, use_timeZone, notify_atHour) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala new file mode 100644 index 0000000..95c9415 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala @@ -0,0 +1,29 @@ +package cc.sukazyo.cono.morny.daemon + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur + +object MornyDaemons { + + def start (): Unit = { + logger info "ALL Morny Daemons starting..." + // TrackerDataManager.init(); + MedicationTimer.start() + MornyReport.onMornyLogin() + logger info "Morny Daemons started." + + } + + def stop (): Unit = { + logger.info("ALL Morny Daemons stopping...") + // TrackerDataManager.DAEMON.interrupt(); + MedicationTimer.interrupt() + // TrackerDataManager.trackingLock.lock(); + try { MedicationTimer.join() } + catch case e: InterruptedException => + e.printStackTrace(System.out) + MornyReport.onMornyExit(MornyCoeur.exitReason) + logger.info("ALL Morny Daemons STOPPED.") + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala new file mode 100644 index 0000000..cba4ccf --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala @@ -0,0 +1,122 @@ +package cc.sukazyo.cono.morny.daemon + +import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig} +import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException +import com.pengrad.telegrambot.request.{BaseRequest, SendMessage} +import com.pengrad.telegrambot.response.BaseResponse +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.bot.command.MornyInformation +import com.google.gson.GsonBuilder +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.model.User +import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h +import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString + +object MornyReport { + + private def unsupported: Boolean = (!MornyCoeur.available) || (MornyCoeur.config.reportToChat == -1) + + private def executeReport[T <: BaseRequest[T, R], R<: BaseResponse] (report: T): Unit = { + if unsupported then return + try { + MornyCoeur.extra exec report + } catch case e: EventRuntimeException.ActionFailed => { + logger warn + s"""cannot execute report to telegram: + |${exceptionLog(e) indent 4} + | tg-api response: + |${(e.getResponse toString) indent 4}""" + .stripMargin + } + } + + def exception (e: Throwable, description: String|Null = null): Unit = { + if unsupported then return + def _tgErrFormat: String = e match + case api: EventRuntimeException.ActionFailed => + // language=html + "\n\ntg-api error:\n

      %s
      " + .formatted(GsonBuilder().setPrettyPrinting().create.toJson(api.getResponse)) + case _ => "" + executeReport(SendMessage( + MornyCoeur.config.reportToChat, + // language=html + s"""▌Coeur Unexpected Exception + |${if description ne null then h(description)+"\n" else ""} + |
      ${h(exceptionLog(e))}
      $_tgErrFormat""" + .stripMargin + ).parseMode(ParseMode HTML)) + } + + def unauthenticatedAction (action: String, user: User): Unit = { + if unsupported then return + executeReport(SendMessage( + MornyCoeur.config.reportToChat, + // language=html + s"""▌User unauthenticated action + |action: ${h(action)} + |by user ${(TGToString as user) fullnameRefHtml}""" + .stripMargin + ).parseMode(ParseMode HTML)) + } + + def onMornyLogin(): Unit = { + executeReport(SendMessage( + MornyCoeur.config.reportToChat, + // language=html + s"""▌Morny Logged in + |-v ${MornyInformation.getVersionAllFullTagHTML} + |as user ${MornyCoeur.username} + | + |as config fields: + |${sectionConfigFields(MornyCoeur.config)}""" + .stripMargin + ).parseMode(ParseMode HTML)) + } + + def sectionConfigFields (config: MornyConfig): String = { + val echo = StringBuilder() + for (field <- config.getClass.getFields) { + // language=html + echo ++= s"- ${field.getName} " + try { + if (field.isAnnotationPresent(classOf[MornyConfig.Sensitive])) { + echo ++= /*language=html*/ ": sensitive_field" + } else { + val value = field.get(config) + // language=html + echo ++= "= " ++= (if value eq null then "null" else s"${h(value.toString)}") + } + + } catch + // noinspection ScalaUnnecessaryParentheses + case e: (IllegalAccessException|IllegalArgumentException|NullPointerException) => + // language=html + echo ++= s": ${h("")}" + logger error + s"""error while reading config field ${field.getName} + |${exceptionLog(e)}""".stripMargin + exception(e, s"error while reading config field ${field.getName}") + echo ++= "\n" + } + echo dropRight 1 toString + } + + def onMornyExit (causedBy: AnyRef|Null): Unit = { + if unsupported then return + val causedTag = causedBy match + case u: User => (TGToString as u) fullnameRefHtml + case n if n == null => "UNKNOWN reason" + case a: AnyRef => /*language=html*/ s"${h(a.toString)}" + executeReport(SendMessage( + MornyCoeur.config.reportToChat, + // language=html + s"""▌Morny Exited + |from user @${MornyCoeur.username} + | + |by: $causedTag""" + .stripMargin + ).parseMode(ParseMode HTML)) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala new file mode 100644 index 0000000..667d5d3 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala @@ -0,0 +1,38 @@ +package cc.sukazyo.cono.morny.data + +import cc.sukazyo.cono.morny.MornyAssets + +import scala.language.postfixOps +import scala.util.Using +import java.io.IOException +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.daemon.MornyReport + +object TelegramImages { + + class AssetsFileImage (assetsPath: String) { + + private var cache: Array[Byte]|Null = _ + + def get:Array[Byte] = + if cache eq null then read() + if cache eq null then throw IllegalStateException("Failed to get assets file image.") + cache + + private def read (): Unit = { + Using ((MornyAssets.pack getResource assetsPath)read) { stream => + try { this.cache = stream.readAllBytes() } + catch case e: IOException => { + logger error + s"""Cannot read resource file: + |${exceptionLog(e)}""".stripMargin + MornyReport.exception(e, "Cannot read resource file.") + } + } + } + + } + + val IMG_ABOUT: AssetsFileImage = AssetsFileImage("images/featured-image@0.5x.jpg") + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java new file mode 100644 index 0000000..10a017a --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java @@ -0,0 +1,53 @@ +package cc.sukazyo.cono.morny.data; + +import javax.annotation.Nonnull; +import java.lang.reflect.Field; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 存放 bot 使用到的贴纸 + * @since 0.4.2.0 + */ +public class TelegramStickers { + + public static final String ID_ONLINE_STATUS_RETURN = "CAACAgEAAx0CW-CvvgAC5eBhhhODGRuu0pxKLwoQ3yMsowjviAACcycAAnj8xgVVU666si1utiIE"; + public static final String ID_HELLO = "CAACAgEAAxkBAAMnYYYWKNXO4ibo9dlsmDctHhhV6fIAAqooAAJ4_MYFJJhrHS74xUAiBA"; + public static final String ID_EXIT = "CAACAgEAAxkBAAMoYYYWt8UjvP0N405SAyvg2SQZmokAAkMiAAJ4_MYFw6yZLu06b-MiBA"; + public static final String ID_403 = "CAACAgEAAxkBAAMqYYYa_7hpXH6hMOYMX4Nh8AVYd74AAnQnAAJ4_MYFRdmmsQKLDZgiBA"; + public static final String ID_404 = "CAACAgEAAx0CSQh32gABA966YbRJpbmi2lCHINBDuo1DknSTsbsAAqUoAAJ4_MYFUa8SIaZriAojBA"; + public static final String ID_WAITING = "CAACAgEAAx0CSQh32gABA-8DYbh7W2VhJ490ucfZMUMrgMR2FW4AAm4nAAJ4_MYFjx6zpxJPWsQjBA"; + public static final String ID_SENT = "CAACAgEAAx0CSQh32gABA--zYbiyU_wOijEitp-0tSl_k7W6l3gAAgMmAAJ4_MYF4GrompjXPx4jBA"; + public static final String ID_SAVED = "CAACAgEAAx0CSQh32gABBExuYdB_G0srfhQldRWkBYxWzCOv4-IAApooAAJ4_MYFcjuNZszfQcQjBA"; + public static final String ID_PROGYNOVA = "CAACAgUAAxkBAAICm2KEuL7UQqNP7vSPCg2DHJIND6UsAAKLAwACH4WSBszIo722aQ3jJAQ"; + public static final String ID_NETWORK_ERR = "CAACAgEAAxkBAAID0WNJgNEkD726KW4vZeFlw0FlVVyNAAIXJgACePzGBb50o7O1RbxoKgQ"; + + @Nonnull + public static Map map () { + final LinkedHashMap mapping = new LinkedHashMap<>(); + for (Field object : TelegramStickers.class.getFields()) { + if (object.getType()==String.class && object.getName().startsWith("ID_")) { + try { + mapping.put(object.getName(), (String)object.get("")); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + return mapping; + } + + @Nonnull + public static Map.Entry getById (@Nonnull String stickerFieldID) + throws NoSuchFieldException { + try { + // normally get the sticker and echo + Field field = TelegramStickers.class.getField(stickerFieldID); + return Map.entry(field.getName(), (String)field.get("")); + } catch (IllegalAccessException e) { + // java-reflect get sticker FILE_ID failed + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/old/cc/sukazyo/cono/morny/internal/BuildConfigField.java b/src/main/scala/cc/sukazyo/cono/morny/internal/BuildConfigField.java similarity index 100% rename from src/main/old/cc/sukazyo/cono/morny/internal/BuildConfigField.java rename to src/main/scala/cc/sukazyo/cono/morny/internal/BuildConfigField.java diff --git a/src/main/scala/cc/sukazyo/cono/morny/internal/ScalaJavaConv.scala b/src/main/scala/cc/sukazyo/cono/morny/internal/ScalaJavaConv.scala new file mode 100644 index 0000000..3547a2b --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/internal/ScalaJavaConv.scala @@ -0,0 +1,12 @@ +package cc.sukazyo.cono.morny.internal + +import scala.jdk.CollectionConverters._ +import scala.collection.immutable as simm +import java.util as j + +object ScalaJavaConv { + + def jSetInteger2simm (data: j.Set[Integer]): simm.Set[Int] = + data.asScala.toSet.map(_.intValue) + +} diff --git a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java index e634582..4a36d44 100644 --- a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java +++ b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java @@ -8,7 +8,7 @@ public class MornyCLI { public static void main (String[] args) { - System.out.print("$ java -jar morny-coeur-"+MornySystem.VERSION_FULL+".jar " ); + System.out.print("$ java -jar morny-coeur-"+MornySystem.VERSION_FULL()+".jar " ); String x; try (Scanner line = new Scanner(System.in)) { x = line.nextLine(); } ServerMain.main(UniversalCommand.format(x)); diff --git a/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java b/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java index 58600ce..d686ea0 100644 --- a/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java +++ b/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java @@ -8,6 +8,8 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Set; +import static cc.sukazyo.cono.morny.internal.ScalaJavaConv.jSetInteger2simm; + public class TestMedicationTimer { @ParameterizedTest @@ -21,12 +23,12 @@ public class TestMedicationTimer { 2125-11-18T23:45:27.062+00, +00, 2125-11-19T07:00:00+00 """) void testCalcNextRoutineTimestamp (ZonedDateTime base, ZoneOffset zoneHour, ZonedDateTime expected) - throws MedicationTimer.NoNotifyTimeTag { + throws IllegalArgumentException { final Set at = Set.of(7, 19, 21); System.out.println("base.toInstant().toEpochMilli() = " + base.toInstant().toEpochMilli()); Assertions.assertEquals( expected.toInstant().toEpochMilli(), - MedicationTimer.calcNextRoutineTimestamp(base.toInstant().toEpochMilli(), zoneHour, at) + MedicationTimer.calcNextRoutineTimestamp(base.toInstant().toEpochMilli(), zoneHour, jSetInteger2simm(at)) ); System.out.println(" ok"); } From a8b7562b51e0d5a857fcd96ffa6823b07ed66c36 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 16 Sep 2023 23:12:44 +0800 Subject: [PATCH 32/40] complete scala port --- .dockerignore | 2 - .editorconfig | 928 +++++++++++++++++- .gitignore | 2 - build.gradle | 23 +- gradle.properties | 5 +- .../cc/sukazyo/cono/morny/util/BiliTool.java | 98 -- .../cono/morny/util/CommonConvert.java | 61 -- .../cono/morny/util/CommonEncrypt.java | 144 --- .../sukazyo/cono/morny/util/CommonFormat.java | 30 - .../sukazyo/cono/morny/util/CommonRandom.java | 44 - .../cc/sukazyo/cono/morny/util/FileUtils.java | 28 - .../sukazyo/cono/morny/util/OkHttpPublic.java | 13 - .../cono/morny/util/UniversalCommand.java | 49 - .../cono/morny/util/tgapi/InputCommand.java | 66 -- .../cono/morny/util/tgapi/Standardize.java | 7 - .../tgapi/event/EventRuntimeException.java | 35 - .../util/tgapi/formatting/MsgEscape.java | 15 - .../util/tgapi/formatting/NamedUtils.java | 18 - .../util/tgapi/formatting/TGToString.java | 21 - .../tgapi/formatting/TGToStringFromChat.java | 60 -- .../formatting/TGToStringFromMessage.java | 27 - .../tgapi/formatting/TGToStringFromUser.java | 53 - .../formatting/TelegramUserInformation.java | 85 -- .../cc/sukazyo/cono/morny/MornyCoeur.scala | 2 +- .../cc/sukazyo/cono/morny/MornySystem.scala | 4 +- .../morny/bot/api/EventListenerManager.scala | 4 +- .../cono/morny/bot/command/Encryptor.scala | 91 +- .../cono/morny/bot/command/EventHack.scala | 6 +- .../morny/bot/command/GetUsernameAndId.scala | 4 +- .../cono/morny/bot/command/IP186Query.scala | 14 +- .../morny/bot/command/MornyCommands.scala | 8 +- .../cono/morny/bot/command/MornyHellos.scala | 2 +- .../morny/bot/command/MornyInformation.scala | 18 +- .../bot/command/MornyInformationOlds.scala | 1 + .../morny/bot/command/MornyManagers.scala | 16 +- .../cono/morny/bot/command/MornyOldJrrp.scala | 10 +- .../cono/morny/bot/command/Nbnhhsh.scala | 7 +- .../cono/morny/bot/command/私わね.scala | 5 +- .../cono/morny/bot/event/OnCallMe.scala | 8 +- .../cono/morny/bot/event/OnCallMsgSend.scala | 7 +- .../morny/bot/event/OnEventHackHandle.scala | 5 +- .../morny/bot/event/OnQuestionMarkReply.scala | 5 +- .../morny/bot/event/OnTelegramCommand.scala | 8 +- .../cono/morny/bot/event/OnUserRandom.scala | 6 +- .../morny/bot/event/OnUserSlashAction.scala | 12 +- .../cono/morny/bot/query/MyInformation.scala | 6 +- .../cono/morny/bot/query/RawText.scala | 4 +- .../morny/bot/query/ShareToolBilibili.scala | 6 +- .../morny/bot/query/ShareToolTwitter.scala | 7 +- .../cono/morny/daemon/MornyReport.scala | 18 +- .../sukazyo/cono/morny/data/MornyJrrp.scala | 6 +- .../cc/sukazyo/cono/morny/util/BiliTool.scala | 119 +++ .../cono/morny/util/CommonEncrypt.scala | 98 ++ .../cono/morny/util/CommonFormat.scala | 76 ++ .../cono/morny/util/ConvertByteHex.scala | 55 ++ .../sukazyo/cono/morny/util/FileUtils.scala | 28 + .../cono/morny/util/OkHttpPublic.scala | 13 + .../cono/morny/util/UniversalCommand.scala | 75 ++ .../cc/sukazyo/cono/morny/util/UseMath.scala | 19 + .../sukazyo/cono/morny/util/UseRandom.scala | 33 + .../cc/sukazyo/cono/morny/util/package.scala | 22 + .../cono/morny/util/tgapi/ExtraAction.java | 0 .../cono/morny/util/tgapi/InputCommand.scala | 31 + .../cono/morny/util/tgapi/Standardize.scala | 9 + .../tgapi/event/EventRuntimeException.scala | 9 + .../util/tgapi/formatting/NamingUtils.scala | 11 + .../tgapi/formatting/TelegramFormatter.scala | 82 ++ .../formatting/TelegramParseEscape.scala | 12 + .../formatting/TelegramUserInformation.scala | 67 ++ .../cono/morny/util/tgapi/package.scala | 4 + .../java/cc/sukazyo/cono/morny/MornyCLI.java | 18 - .../morny/daemon/TestMedicationTimer.java | 36 - .../sukazyo/cono/morny/util/TestBiliTool.java | 30 - .../cono/morny/util/TestCommonConvert.java | 47 - .../cono/morny/util/TestCommonEncrypt.java | 23 - .../cono/morny/util/TestCommonFormat.java | 36 - .../cc/sukazyo/cono/morny/MornyCLI.scala | 12 + .../sukazyo/cono/morny/test/MornyTests.scala | 10 + .../cono/morny/test/utils/BiliToolTest.scala | 68 ++ .../morny/test/utils/CommonEncryptTest.scala | 33 + .../morny/test/utils/CommonFormatTest.scala | 46 + .../morny/test/utils/ConvertByteHexTest.scala | 45 + .../cono/morny/test/utils/FileUtilsTest.scala | 9 + .../test/utils/UniversalCommandTest.scala | 75 ++ .../test/utils/tgapi/InputCommandTest.scala | 22 + .../tgapi/formatting/NamingUtilsTest.scala | 27 + .../formatting/TelegramFormatterTest.scala | 9 + .../formatting/TelegramParseEscapeTest.scala | 25 + .../TelegramUserInformationTest.scala | 26 + src/test/scala/live/LiveMain.scala | 9 + 90 files changed, 2287 insertions(+), 1186 deletions(-) delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/Standardize.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java delete mode 100644 src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/CommonEncrypt.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/FileUtils.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/OkHttpPublic.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/UseRandom.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/package.scala rename src/main/{java => scala}/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java (100%) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/Standardize.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/NamingUtils.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramParseEscape.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/package.scala delete mode 100644 src/test/java/cc/sukazyo/cono/morny/MornyCLI.java delete mode 100644 src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java delete mode 100644 src/test/java/cc/sukazyo/cono/morny/util/TestBiliTool.java delete mode 100644 src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java delete mode 100644 src/test/java/cc/sukazyo/cono/morny/util/TestCommonEncrypt.java delete mode 100644 src/test/java/cc/sukazyo/cono/morny/util/TestCommonFormat.java create mode 100644 src/test/scala/cc/sukazyo/cono/morny/MornyCLI.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonEncryptTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonFormatTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/ConvertByteHexTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/FileUtilsTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/InputCommandTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/NamingUtilsTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramFormatterTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramParseEscapeTest.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramUserInformationTest.scala create mode 100644 src/test/scala/live/LiveMain.scala diff --git a/.dockerignore b/.dockerignore index e23a3f5..f9ff432 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,8 +4,6 @@ .vscode/ .gradle/ .settings/ -/src/test/java/test/* -/src/test/resources/test/* #build /build/ diff --git a/.editorconfig b/.editorconfig index ee8e124..424d5bf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,3 @@ -root = true - [*] charset = utf-8 end_of_line = lf @@ -11,11 +9,50 @@ tab_width = 4 ij_continuation_indent_size = 8 ij_formatter_off_tag = @formatter:off ij_formatter_on_tag = @formatter:on -ij_formatter_tags_enabled = false +ij_formatter_tags_enabled = true ij_smart_tabs = false -ij_visual_guides = none +ij_visual_guides = ij_wrap_on_typing = false +[*.conf] +ij_smart_tabs = true +ij_hocon_keep_blank_lines_before_right_brace = 2 +ij_hocon_keep_indents_on_empty_lines = true +ij_hocon_keep_line_breaks = true +ij_hocon_space_after_colon = true +ij_hocon_space_after_comma = true +ij_hocon_space_before_colon = true +ij_hocon_space_before_comma = false +ij_hocon_spaces_within_braces = false +ij_hocon_spaces_within_brackets = false +ij_hocon_spaces_within_method_call_parentheses = false + +[*.css] +ij_smart_tabs = true +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = true +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = true +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] +indent_size = 2 +indent_style = space +ij_gherkin_keep_indents_on_empty_lines = false + [*.java] ij_java_align_consecutive_assignments = false ij_java_align_consecutive_variable_declarations = false @@ -25,6 +62,7 @@ ij_java_align_multiline_array_initializer_expression = false ij_java_align_multiline_assignment = true ij_java_align_multiline_binary_operation = false ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true ij_java_align_multiline_extends_list = false ij_java_align_multiline_for = false ij_java_align_multiline_method_parentheses = false @@ -38,6 +76,7 @@ ij_java_align_multiline_text_blocks = false ij_java_align_multiline_throws_list = false ij_java_align_subsequent_simple_methods = false ij_java_align_throws_keyword = true +ij_java_align_types_in_multi_catch = true ij_java_annotation_parameter_wrap = off ij_java_array_initializer_new_line_after_left_brace = true ij_java_array_initializer_right_brace_on_new_line = true @@ -64,7 +103,7 @@ ij_java_blank_lines_before_package = 0 ij_java_block_brace_style = end_of_line ij_java_block_comment_add_space = false ij_java_block_comment_at_first_column = true -ij_java_builder_methods = none +ij_java_builder_methods = ij_java_call_parameters_new_line_after_left_paren = true ij_java_call_parameters_right_paren_on_new_line = true ij_java_call_parameters_wrap = normal @@ -74,8 +113,10 @@ ij_java_class_annotation_wrap = split_into_lines ij_java_class_brace_style = end_of_line ij_java_class_count_to_use_import_on_demand = 5 ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = normal ij_java_do_not_indent_top_level_class_members = false ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false ij_java_do_while_brace_force = never ij_java_doc_add_blank_line_after_description = true ij_java_doc_add_blank_line_after_param_comments = false @@ -96,18 +137,31 @@ ij_java_doc_param_description_on_new_line = false ij_java_doc_preserve_line_breaks = true ij_java_doc_use_throws_not_exception_tag = true ij_java_else_on_new_line = false +ij_java_entity_dd_prefix = ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_prefix = ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_prefix = ij_java_entity_hi_suffix = Home ij_java_entity_lhi_prefix = Local ij_java_entity_lhi_suffix = Home ij_java_entity_li_prefix = Local +ij_java_entity_li_suffix = ij_java_entity_pk_class = java.lang.String +ij_java_entity_ri_prefix = +ij_java_entity_ri_suffix = +ij_java_entity_vo_prefix = ij_java_entity_vo_suffix = VO ij_java_enum_constants_wrap = on_every_item ij_java_extends_keyword_wrap = normal ij_java_extends_list_wrap = off ij_java_field_annotation_wrap = normal +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = ij_java_finally_on_new_line = false ij_java_for_brace_force = if_multiline ij_java_for_statement_new_line_after_left_paren = true @@ -139,8 +193,15 @@ ij_java_label_indent_size = 0 ij_java_lambda_brace_style = end_of_line ij_java_layout_static_imports_separately = true ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false ij_java_line_comment_at_first_column = true +ij_java_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_message_dd_prefix = ij_java_message_dd_suffix = EJB +ij_java_message_eb_prefix = ij_java_message_eb_suffix = Bean ij_java_method_annotation_wrap = split_into_lines ij_java_method_brace_style = end_of_line @@ -149,16 +210,22 @@ ij_java_method_parameters_new_line_after_left_paren = true ij_java_method_parameters_right_paren_on_new_line = true ij_java_method_parameters_wrap = normal ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal ij_java_names_count_to_use_import_on_demand = 3 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true ij_java_new_line_after_lparen_in_record_header = false ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* ij_java_parameter_annotation_wrap = normal +ij_java_parameter_name_prefix = +ij_java_parameter_name_suffix = ij_java_parentheses_expression_new_line_after_left_paren = false ij_java_parentheses_expression_right_paren_on_new_line = false ij_java_place_assignment_sign_on_next_line = false ij_java_prefer_longer_names = true ij_java_prefer_parameters_wrap = false ij_java_record_components_wrap = normal +ij_java_repeat_annotations = ij_java_repeat_synchronized = true ij_java_replace_instanceof_and_cast = false ij_java_replace_null_check = true @@ -166,13 +233,26 @@ ij_java_replace_sum_lambda_with_method_ref = true ij_java_resource_list_new_line_after_left_paren = true ij_java_resource_list_right_paren_on_new_line = true ij_java_resource_list_wrap = normal +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true ij_java_rparen_on_new_line_in_record_header = false +ij_java_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = ij_java_session_hi_suffix = Home ij_java_session_lhi_prefix = Local ij_java_session_lhi_suffix = Home ij_java_session_li_prefix = Local +ij_java_session_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = ij_java_session_si_suffix = Service ij_java_space_after_closing_angle_bracket_in_type_argument = false ij_java_space_after_colon = true @@ -191,6 +271,7 @@ ij_java_space_before_class_left_brace = true ij_java_space_before_colon = true ij_java_space_before_colon_in_foreach = true ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false ij_java_space_before_do_left_brace = true ij_java_space_before_else_keyword = true ij_java_space_before_else_left_brace = true @@ -221,6 +302,7 @@ ij_java_space_within_empty_array_initializer_braces = false ij_java_space_within_empty_method_call_parentheses = false ij_java_space_within_empty_method_parentheses = false ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true ij_java_spaces_around_assignment_operators = true ij_java_spaces_around_bitwise_operators = true ij_java_spaces_around_equality_operators = true @@ -239,6 +321,7 @@ ij_java_spaces_within_braces = false ij_java_spaces_within_brackets = false ij_java_spaces_within_cast_parentheses = false ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false ij_java_spaces_within_for_parentheses = false ij_java_spaces_within_if_parentheses = false ij_java_spaces_within_method_call_parentheses = false @@ -250,9 +333,13 @@ ij_java_spaces_within_synchronized_parentheses = false ij_java_spaces_within_try_parentheses = false ij_java_spaces_within_while_parentheses = false ij_java_special_else_if_treatment = true +ij_java_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = ij_java_subclass_name_suffix = Impl ij_java_ternary_operation_signs_on_next_line = false ij_java_ternary_operation_wrap = on_every_item +ij_java_test_name_prefix = ij_java_test_name_suffix = Test ij_java_throws_keyword_wrap = normal ij_java_throws_list_wrap = off @@ -268,6 +355,303 @@ ij_java_wrap_comments = false ij_java_wrap_first_method_in_call_chain = true ij_java_wrap_long_lines = false +[*.less] +indent_size = 2 +indent_style = space +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.proto] +indent_size = 2 +indent_style = space +tab_width = 2 +ij_continuation_indent_size = 4 +ij_protobuf_keep_blank_lines_in_code = 2 +ij_protobuf_keep_indents_on_empty_lines = false +ij_protobuf_keep_line_breaks = true +ij_protobuf_space_after_comma = true +ij_protobuf_space_before_comma = false +ij_protobuf_spaces_around_assignment_operators = true +ij_protobuf_spaces_within_braces = false +ij_protobuf_spaces_within_brackets = false + +[*.sass] +indent_size = 2 +indent_style = space +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_line_comment_add_space = false +ij_sass_line_comment_at_first_column = false +ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scala] +ij_continuation_indent_size = 4 +ij_smart_tabs = true +ij_scala_align_composite_pattern = true +ij_scala_align_extends_with = 0 +ij_scala_align_group_field_declarations = false +ij_scala_align_if_else = false +ij_scala_align_in_columns_case_branch = false +ij_scala_align_multiline_binary_operation = false +ij_scala_align_multiline_chained_methods = false +ij_scala_align_multiline_for = true +ij_scala_align_multiline_parameters = false +ij_scala_align_multiline_parameters_in_calls = false +ij_scala_align_multiline_parenthesized_expression = false +ij_scala_align_parameter_types_in_multiline_declarations = 0 +ij_scala_align_tuple_elements = false +ij_scala_alternate_continuation_indent_for_params = 4 +ij_scala_binary_operation_wrap = off +ij_scala_blank_lines_after_anonymous_class_header = 0 +ij_scala_blank_lines_after_class_header = 0 +ij_scala_blank_lines_after_imports = 1 +ij_scala_blank_lines_after_package = 1 +ij_scala_blank_lines_around_class = 1 +ij_scala_blank_lines_around_class_in_inner_scopes = 0 +ij_scala_blank_lines_around_field = 0 +ij_scala_blank_lines_around_field_in_inner_scopes = 0 +ij_scala_blank_lines_around_field_in_interface = 0 +ij_scala_blank_lines_around_method = 1 +ij_scala_blank_lines_around_method_in_inner_scopes = 1 +ij_scala_blank_lines_around_method_in_interface = 1 +ij_scala_blank_lines_before_class_end = 0 +ij_scala_blank_lines_before_imports = 1 +ij_scala_blank_lines_before_method_body = 0 +ij_scala_blank_lines_before_package = 0 +ij_scala_block_brace_style = end_of_line +ij_scala_block_comment_add_space = false +ij_scala_block_comment_at_first_column = true +ij_scala_call_parameters_new_line_after_lparen = 0 +ij_scala_call_parameters_right_paren_on_new_line = false +ij_scala_call_parameters_wrap = off +ij_scala_case_clause_brace_force = never +ij_scala_catch_on_new_line = false +ij_scala_class_annotation_wrap = split_into_lines +ij_scala_class_brace_style = end_of_line +ij_scala_closure_brace_force = never +ij_scala_do_not_align_block_expr_params = true +ij_scala_do_not_indent_case_clause_body = false +ij_scala_do_not_indent_tuples_close_brace = true +ij_scala_do_while_brace_force = never +ij_scala_else_on_new_line = false +ij_scala_enable_scaladoc_formatting = true +ij_scala_enforce_functional_syntax_for_unit = true +ij_scala_extends_keyword_wrap = off +ij_scala_extends_list_wrap = off +ij_scala_field_annotation_wrap = split_into_lines +ij_scala_finally_brace_force = never +ij_scala_finally_on_new_line = false +ij_scala_for_brace_force = never +ij_scala_for_statement_wrap = off +ij_scala_formatter = 0 +ij_scala_if_brace_force = never +ij_scala_implicit_value_class_prefix = +ij_scala_implicit_value_class_suffix = Ops +ij_scala_indent_braced_function_args = true +ij_scala_indent_case_from_switch = true +ij_scala_indent_fewer_braces_in_method_call_chains = false +ij_scala_indent_first_parameter = true +ij_scala_indent_first_parameter_clause = false +ij_scala_indent_type_arguments = true +ij_scala_indent_type_parameters = true +ij_scala_indent_yield_after_one_line_enumerators = true +ij_scala_keep_blank_lines_before_right_brace = 2 +ij_scala_keep_blank_lines_in_code = 2 +ij_scala_keep_blank_lines_in_declarations = 2 +ij_scala_keep_comments_on_same_line = true +ij_scala_keep_first_column_comment = false +ij_scala_keep_indents_on_empty_lines = true +ij_scala_keep_line_breaks = true +ij_scala_keep_one_line_lambdas_in_arg_list = false +ij_scala_keep_simple_blocks_in_one_line = false +ij_scala_keep_simple_methods_in_one_line = false +ij_scala_keep_xml_formatting = false +ij_scala_line_comment_add_space = false +ij_scala_line_comment_at_first_column = true +ij_scala_method_annotation_wrap = split_into_lines +ij_scala_method_brace_force = never +ij_scala_method_brace_style = end_of_line +ij_scala_method_call_chain_wrap = off +ij_scala_method_parameters_new_line_after_left_paren = true +ij_scala_method_parameters_right_paren_on_new_line = true +ij_scala_method_parameters_wrap = off +ij_scala_modifier_list_wrap = false +ij_scala_multiline_string_align_dangling_closing_quotes = false +ij_scala_multiline_string_closing_quotes_on_new_line = true +ij_scala_multiline_string_insert_margin_on_enter = true +ij_scala_multiline_string_margin_char = | +ij_scala_multiline_string_margin_indent = 2 +ij_scala_multiline_string_opening_quotes_on_new_line = true +ij_scala_multiline_string_process_margin_on_copy_paste = true +ij_scala_new_line_after_case_clause_arrow_when_multiline_body = false +ij_scala_newline_after_annotations = false +ij_scala_not_continuation_indent_for_params = false +ij_scala_parameter_annotation_wrap = off +ij_scala_parentheses_expression_new_line_after_left_paren = false +ij_scala_parentheses_expression_right_paren_on_new_line = false +ij_scala_place_closure_parameters_on_new_line = false +ij_scala_place_self_type_on_new_line = true +ij_scala_prefer_parameters_wrap = false +ij_scala_preserve_space_after_method_declaration_name = false +ij_scala_reformat_on_compile = false +ij_scala_replace_case_arrow_with_unicode_char = false +ij_scala_replace_for_generator_arrow_with_unicode_char = false +ij_scala_replace_lambda_with_greek_letter = false +ij_scala_replace_map_arrow_with_unicode_char = false +ij_scala_scalafmt_config_path = +ij_scala_scalafmt_fallback_to_default_settings = false +ij_scala_scalafmt_reformat_on_files_save = false +ij_scala_scalafmt_show_invalid_code_warnings = true +ij_scala_scalafmt_use_intellij_formatter_for_range_format = true +ij_scala_sd_align_exception_comments = true +ij_scala_sd_align_list_item_content = true +ij_scala_sd_align_other_tags_comments = true +ij_scala_sd_align_parameters_comments = true +ij_scala_sd_align_return_comments = true +ij_scala_sd_blank_line_after_parameters_comments = false +ij_scala_sd_blank_line_after_return_comments = false +ij_scala_sd_blank_line_before_parameters = false +ij_scala_sd_blank_line_before_tags = true +ij_scala_sd_blank_line_between_parameters = false +ij_scala_sd_keep_blank_lines_between_tags = true +ij_scala_sd_preserve_spaces_in_tags = false +ij_scala_space_after_comma = true +ij_scala_space_after_for_semicolon = true +ij_scala_space_after_modifiers_constructor = true +ij_scala_space_after_type_colon = true +ij_scala_space_before_brace_method_call = true +ij_scala_space_before_class_left_brace = true +ij_scala_space_before_for_parentheses = true +ij_scala_space_before_if_parentheses = true +ij_scala_space_before_infix_like_method_parentheses = false +ij_scala_space_before_infix_method_call_parentheses = false +ij_scala_space_before_infix_operator_like_method_call_parentheses = false +ij_scala_space_before_method_call_parentheses = false +ij_scala_space_before_method_left_brace = true +ij_scala_space_before_method_parentheses = true +ij_scala_space_before_type_colon = false +ij_scala_space_before_type_parameter_in_def_list = false +ij_scala_space_before_type_parameter_leading_context_bound_colon = false +ij_scala_space_before_type_parameter_leading_context_bound_colon_hk = true +ij_scala_space_before_type_parameter_list = false +ij_scala_space_before_type_parameter_rest_context_bound_colons = true +ij_scala_space_before_while_parentheses = true +ij_scala_space_inside_closure_braces = true +ij_scala_space_inside_self_type_braces = true +ij_scala_space_within_empty_method_call_parentheses = false +ij_scala_spaces_around_at_in_patterns = false +ij_scala_spaces_in_imports = false +ij_scala_spaces_in_one_line_blocks = false +ij_scala_spaces_within_brackets = false +ij_scala_spaces_within_for_parentheses = false +ij_scala_spaces_within_if_parentheses = false +ij_scala_spaces_within_method_call_parentheses = false +ij_scala_spaces_within_method_parentheses = false +ij_scala_spaces_within_parentheses = false +ij_scala_spaces_within_while_parentheses = false +ij_scala_special_else_if_treatment = true +ij_scala_trailing_comma_arg_list_enabled = true +ij_scala_trailing_comma_import_selector_enabled = false +ij_scala_trailing_comma_mode = trailing_comma_keep +ij_scala_trailing_comma_params_enabled = true +ij_scala_trailing_comma_pattern_arg_list_enabled = false +ij_scala_trailing_comma_tuple_enabled = false +ij_scala_trailing_comma_tuple_type_enabled = false +ij_scala_trailing_comma_type_params_enabled = false +ij_scala_try_brace_force = never +ij_scala_type_annotation_exclude_constant = true +ij_scala_type_annotation_exclude_in_dialect_sources = true +ij_scala_type_annotation_exclude_in_test_sources = false +ij_scala_type_annotation_exclude_member_of_anonymous_class = false +ij_scala_type_annotation_exclude_member_of_private_class = false +ij_scala_type_annotation_exclude_when_type_is_stable = true +ij_scala_type_annotation_function_parameter = false +ij_scala_type_annotation_implicit_modifier = true +ij_scala_type_annotation_local_definition = false +ij_scala_type_annotation_private_member = false +ij_scala_type_annotation_protected_member = true +ij_scala_type_annotation_public_member = true +ij_scala_type_annotation_structural_type = true +ij_scala_type_annotation_underscore_parameter = false +ij_scala_type_annotation_unit_type = true +ij_scala_use_alternate_continuation_indent_for_params = false +ij_scala_use_scala3_indentation_based_syntax = true +ij_scala_use_scaladoc2_formatting = true +ij_scala_variable_annotation_wrap = off +ij_scala_while_brace_force = never +ij_scala_while_on_new_line = false +ij_scala_wrap_before_with_keyword = false +ij_scala_wrap_first_method_in_call_chain = false +ij_scala_wrap_long_lines = false + +[*.scss] +indent_size = 2 +indent_style = space +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.vue] +indent_size = 2 +indent_style = space +tab_width = 2 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + [.editorconfig] ij_editorconfig_align_group_field_declarations = false ij_editorconfig_space_after_colon = false @@ -276,7 +660,7 @@ ij_editorconfig_space_before_colon = false ij_editorconfig_space_before_comma = false ij_editorconfig_spaces_around_assignment_operators = true -[{*.ant,*.fxml,*.isc,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdd,*.wsdl,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +[{*.ant,*.fxml,*.isc,*.jhm,*.jnlp,*.jrxml,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] ij_smart_tabs = true ij_xml_align_attributes = false ij_xml_align_text = false @@ -297,7 +681,367 @@ ij_xml_space_inside_empty_tag = true ij_xml_text_wrap = normal ij_xml_use_custom_settings = false -[{*.gant,*.groovy,*.gson,*.gy}] +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_smart_tabs = true +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = false +ij_typescript_align_multiline_parameters = false +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = true +ij_typescript_call_parameters_right_paren_on_new_line = true +ij_typescript_call_parameters_wrap = normal +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = true +ij_typescript_for_statement_right_paren_on_new_line = true +ij_typescript_for_statement_wrap = on_every_item +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = false +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = false +ij_typescript_keep_indents_on_empty_lines = true +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = true +ij_typescript_method_parameters_right_paren_on_new_line = true +ij_typescript_method_parameters_wrap = on_every_item +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = true +ij_typescript_parentheses_expression_right_paren_on_new_line = true +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = true +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = false +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = true +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = true +ij_javascript_array_initializer_right_brace_on_new_line = true +ij_javascript_array_initializer_wrap = on_every_item +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = true +ij_javascript_call_parameters_right_paren_on_new_line = true +ij_javascript_call_parameters_wrap = on_every_item +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = false +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = remove +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = if_multiline +ij_javascript_for_statement_new_line_after_left_paren = true +ij_javascript_for_statement_right_paren_on_new_line = true +ij_javascript_for_statement_wrap = on_every_item +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = false +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = true +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = true +ij_javascript_method_parameters_right_paren_on_new_line = true +ij_javascript_method_parameters_wrap = on_every_item +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = true +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = true +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = true +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = on_every_item +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = false +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = false +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.comp,*.frag,*.fsh,*.geom,*.glsl,*.tesc,*.tese,*.vert,*.vsh}] +ij_glsl_keep_indents_on_empty_lines = true + +[{*.ft,*.vm,*.vsl}] +ij_smart_tabs = true +ij_vtl_keep_indents_on_empty_lines = true + +[{*.gant,*.groovy,*.gy}] ij_smart_tabs = true ij_groovy_align_group_field_declarations = false ij_groovy_align_multiline_array_initializer_expression = false @@ -344,6 +1088,7 @@ ij_groovy_class_brace_style = end_of_line ij_groovy_class_count_to_use_import_on_demand = 5 ij_groovy_do_while_brace_force = never ij_groovy_else_on_new_line = true +ij_groovy_enable_groovydoc_formatting = true ij_groovy_enum_constants_wrap = on_every_item ij_groovy_extends_keyword_wrap = normal ij_groovy_extends_list_wrap = off @@ -353,6 +1098,12 @@ ij_groovy_for_brace_force = never ij_groovy_for_statement_new_line_after_left_paren = true ij_groovy_for_statement_right_paren_on_new_line = true ij_groovy_for_statement_wrap = on_every_item +ij_groovy_ginq_general_clause_wrap_policy = 2 +ij_groovy_ginq_having_wrap_policy = 1 +ij_groovy_ginq_indent_having_clause = true +ij_groovy_ginq_indent_on_clause = true +ij_groovy_ginq_on_wrap_policy = 1 +ij_groovy_ginq_space_after_keyword = true ij_groovy_if_brace_force = never ij_groovy_import_annotation_wrap = 2 ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* @@ -376,6 +1127,7 @@ ij_groovy_label_indent_size = 0 ij_groovy_lambda_brace_style = end_of_line ij_groovy_layout_static_imports_separately = true ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_add_space_on_reformat = false ij_groovy_line_comment_at_first_column = true ij_groovy_method_annotation_wrap = split_into_lines ij_groovy_method_brace_style = end_of_line @@ -481,6 +1233,109 @@ ij_groovy_while_on_new_line = false ij_groovy_wrap_chain_calls_after_dot = false ij_groovy_wrap_long_lines = false +[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.kts}] +ij_smart_tabs = true +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +ij_kotlin_align_multiline_extends_list = false +ij_kotlin_align_multiline_method_parentheses = false +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = true +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = normal +ij_kotlin_blank_lines_after_class_header = 1 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 +ij_kotlin_block_comment_add_space = false +ij_kotlin_block_comment_at_first_column = true +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = on_every_item +ij_kotlin_extends_list_wrap = on_every_item +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_import_nested_classes = false +ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_first_column_comment = false +ij_kotlin_keep_indents_on_empty_lines = true +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false +ij_kotlin_line_break_after_multiline_when_entry = true +ij_kotlin_line_comment_add_space = false +ij_kotlin_line_comment_add_space_on_reformat = false +ij_kotlin_line_comment_at_first_column = true +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_method_call_chain_wrap = on_every_item +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true +ij_kotlin_variable_annotation_wrap = off +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.conf,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config,mcmod.info,meatball_from_mutton.json,pack.mcmeta}] +ij_smart_tabs = true +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = true +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + [{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] ij_smart_tabs = true ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 @@ -491,7 +1346,7 @@ ij_html_block_comment_add_space = false ij_html_block_comment_at_first_column = true ij_html_do_not_align_children_of_min_lines = 0 ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p -ij_html_do_not_indent_children_of_tags = none +ij_html_do_not_indent_children_of_tags = ij_html_enforce_quotes = false ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var ij_html_keep_blank_lines = 0 @@ -510,19 +1365,56 @@ ij_html_space_around_equality_in_attribute = false ij_html_space_inside_empty_tag = true ij_html_text_wrap = off +[{*.http,*.rest}] +indent_size = 0 +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}] +ij_smart_tabs = true +ij_jsp_jsp_prefer_comma_separated_import_list = false +ij_jsp_keep_indents_on_empty_lines = true + +[{*.jspx,*.tagx}] +ij_smart_tabs = true +ij_jspx_keep_indents_on_empty_lines = true + [{*.markdown,*.md}] ij_smart_tabs = true ij_markdown_force_one_space_after_blockquote_symbol = true ij_markdown_force_one_space_after_header_symbol = true ij_markdown_force_one_space_after_list_bullet = true ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true ij_markdown_keep_indents_on_empty_lines = true +ij_markdown_keep_line_breaks_inside_text_blocks = true ij_markdown_max_lines_around_block_elements = 1 ij_markdown_max_lines_around_header = 1 ij_markdown_max_lines_between_paragraphs = 1 ij_markdown_min_lines_around_block_elements = 1 ij_markdown_min_lines_around_header = 1 ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.pb,*.textproto}] +indent_size = 2 +indent_style = space +tab_width = 2 +ij_continuation_indent_size = 4 +ij_prototext_keep_blank_lines_in_code = 2 +ij_prototext_keep_indents_on_empty_lines = false +ij_prototext_keep_line_breaks = true +ij_prototext_space_after_colon = true +ij_prototext_space_after_comma = true +ij_prototext_space_before_colon = false +ij_prototext_space_before_comma = false +ij_prototext_spaces_within_braces = true +ij_prototext_spaces_within_brackets = false [{*.properties,spring.handlers,spring.schemas}] ij_properties_align_group_field_declarations = false @@ -530,3 +1422,23 @@ ij_properties_keep_blank_lines = true ij_properties_key_value_delimiter = equals ij_properties_spaces_around_key_value_delimiter = true +[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] +indent_style = space +ij_qute_keep_indents_on_empty_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +indent_style = space +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = true +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.gitignore b/.gitignore index e23a3f5..f9ff432 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,6 @@ .vscode/ .gradle/ .settings/ -/src/test/java/test/* -/src/test/resources/test/* #build /build/ diff --git a/build.gradle b/build.gradle index f0041ee..deb0e57 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'java-library' id 'application' id 'maven-publish' + id "io.github.ysohda.scalatest" version "0.32.1" id 'com.github.johnrengelman.shadow' version '8.1.1' id 'com.github.gmazzo.buildconfig' version '4.1.2' id 'org.ajoberstar.grgit' version '5.2.0' @@ -84,8 +85,10 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:${lib_okhttp_v}" implementation "com.google.code.gson:gson:${lib_gson_v}" - testImplementation platform("org.junit:junit-bom:${lib_junit_v}") - testImplementation "org.junit.jupiter:junit-jupiter" + testImplementation "org.scalatest:scalatest_$proj_scala_api:${lib_scalatest_v}" + testImplementation "org.scalatest:scalatest-freespec_$proj_scala_api:${lib_scalatest_v}" + testRuntimeOnly "org.scala-lang.modules:scala-xml_$proj_scala_api:${lib_scalamodule_xml_v}" + testRuntimeOnly 'com.vladsch.flexmark:flexmark-all:0.64.6' // for generating HTML report // required by gradle-scalatest plugin } @@ -108,14 +111,18 @@ tasks.withType(ScalaCompile).configureEach { scalaCompileOptions.additionalParameters.add "-language:postfixOps" // scalaCompileOptions.additionalParameters.add("-Yexplicit-nulls") - +// scalaCompileOptions.additionalParameters.add "-language:experimental.saferExceptions" + } +tasks.withType(Javadoc).configureEach { + options.encoding = proj_file_encoding.name() +} + +//tasks.withType(ScalaDoc).configureEach { +//} + test { - useJUnitPlatform() - testLogging { - events "passed", "skipped", "failed" - } } application { @@ -152,7 +159,7 @@ shadowJar { } -@SuppressWarnings("all") +@SuppressWarnings('GrMethodMayBeStatic') boolean isCleanBuild () { if (grgit == null) return false Set changes = grgit.status().unstaged.allChanges + grgit.status().staged.allChanges diff --git a/gradle.properties b/gradle.properties index 9ce1455..1a9ee69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,13 +8,14 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s VERSION = 1.0.0-RC4 USE_DELTA = true -VERSION_DELTA = scalaport3 +VERSION_DELTA = scalaport4 CODENAME = beiping # dependencies lib_spotbugs_v = 4.7.3 +lib_scalamodule_xml_v = 2.2.0 lib_messiva_v = 0.1.1 lib_resourcetools_v = 0.2.2 @@ -24,4 +25,4 @@ lib_javatelegramapi_v = 6.2.0 lib_okhttp_v = 4.11.0 lib_gson_v = 2.10.1 -lib_junit_v = 5.10.0 +lib_scalatest_v = 3.2.17 diff --git a/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java b/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java deleted file mode 100644 index a7df8bc..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/BiliTool.java +++ /dev/null @@ -1,98 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnegative; -import javax.annotation.Nonnull; -import java.util.HashMap; -import java.util.Map; - -public class BiliTool { - - private static final long V_CONV_XOR = 177451812L; - private static final long V_CONV_ADD = 8728348608L; - private static final char[] BV_TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF".toCharArray(); - private static final int TABLE_INT = BV_TABLE.length; - private static final Map BV_TABLE_REVERSED = new HashMap<>(); - static { for (int i = 0; i < BV_TABLE.length; i++) BV_TABLE_REVERSED.put(BV_TABLE[i], i); } - private static final char[] BV_TEMPLATE = "1 4 1 7 ".toCharArray(); - private static final int[] BV_TEMPLATE_FILTER = new int[]{9, 8, 1, 6, 2, 4}; - - public static class IllegalFormatException extends RuntimeException { - - private IllegalFormatException (String bv, String reason) { - super("`%s` is not a valid 10 digits base58 BV id: %s".formatted(bv, reason)); - } - - private IllegalFormatException (String bv, int length) { - this(bv, "length is %d.".formatted(length)); - } - - private IllegalFormatException (String bv, char c, int location) { - this(bv, "char `%s` is not in base58 char table (in position %d)".formatted(c, location)); - } - - } - - /** - * Convert a Bilibili AV video id format to BV id format. - *

      - * the AV id is a number; the BV id is a special base58 number, it shows as String in programming.
      - * eg:
      - * while the link {@code https://www.bilibili.com/video/BV17x411w7KC/} - * shows the same with {@code https://www.bilibili.com/video/av170001/}, - * the AV id is {@code 170001}, the BV id is {@code 17x411w7KC} - *

      - * for now , the BV id has 10 digits. - * the method available while the av-id < 2^27, while it theoretically available when the av-id < 2^30. - *

      - * this method allows input only 10 digits base58 BV id, if the input is not formatted by this method, it will throw - * an Exception. - * - * @see mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」? - * - * @param bv the BV id, a string in (a special) base58 number format, without "BV" prefix. - * @return the AV id corresponding to this bv id in Bilibili, formatted as a number. - * @throws IllegalFormatException if the input BV id is not the 10 digits base58 String. - */ - @Nonnegative - public static long toAv (@Nonnull String bv) throws IllegalFormatException { - long av = 0; - if (bv.length() != 10) - throw new IllegalFormatException(bv, bv.length()); - for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) { - final Integer tableToken = BV_TABLE_REVERSED.get(bv.charAt(BV_TEMPLATE_FILTER[i])); - if (tableToken == null) - throw new IllegalFormatException(bv, bv.charAt(BV_TEMPLATE_FILTER[i]), BV_TEMPLATE_FILTER[i]); - av += tableToken * Math.pow(TABLE_INT,i); - } - return (av-V_CONV_ADD)^V_CONV_XOR; - } - - /** - * Convert a Bilibili BV video id format to AV id format. - *

      - * the AV id is a number; the BV id is a special base58 number, it shows as String in programming.
      - * eg:
      - * while the link {@code https://www.bilibili.com/video/BV17x411w7KC/} - * shows the same with {@code https://www.bilibili.com/video/av170001/}, - * the AV id is {@code 170001}, the BV id is {@code 17x411w7KC} - *

      - * for now , the BV id has 10 digits. - * the method available while the av-id < 2^27, while it theoretically available when the av-id < 2^30. - * - * @see mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」? - * - * @param av the (base10) AV id. - * @return the AV id corresponding to this bv id in Bilibili, - * as a (special) base 58 number format without "BV" prefix. - */ - @Nonnull - public static String toBv (@Nonnegative long av) { - av = (av^V_CONV_XOR)+V_CONV_ADD; - final char[] bv = BV_TEMPLATE.clone(); - for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) { - bv[BV_TEMPLATE_FILTER[i]] = BV_TABLE[(int)(Math.floor(av/(Math.pow(TABLE_INT, i)))%TABLE_INT)]; - } - return String.copyValueOf(bv); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java deleted file mode 100644 index 907cdd5..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/CommonConvert.java +++ /dev/null @@ -1,61 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnegative; -import javax.annotation.Nonnull; - -/** - * 进行简单类型转换等工作的类. - */ -public class CommonConvert { - - /** - * 将字节数组转换成 hex 字符串. - * @param b 字节数组 - * @return String 格式的字节数组的 hex 值(每个字节当中没有分隔符) - * @see #byteToHex(byte) - */ - @Nonnull - public static String byteArrayToHex(@Nonnull byte[] b){ - StringBuilder sb = new StringBuilder(); - for (byte value : b) { - sb.append(byteToHex(value)); - } - return sb.toString(); - } - - /** - * 将一个字节转换成十六进制 hex 字符串. - * @param b 字节值 - * @return String 格式的字节的 hex 值(小写) - */ - @Nonnull - public static String byteToHex(byte b) { - final String hex = Integer.toHexString(b & 0xff); - return hex.length()<2?"0"+hex:hex; - } - - /** - * 将一个字符串数组按照一定规则连接. - *

      - * 连接的方式类似于"数据1+分隔符+数据2+分隔符+...+数据n-1+分隔符+数据n" - * - * @param array 需要进行连接的字符串数组,数组中每一个元素会是一个数据 - * @param connector 在每两个传入数据中插入的分隔符 - * @param startIndex 从传入的数据组中的哪一个位置开始(第一个元素的位置是 {@code 0}) - * @param stopIndex 从传入的数据组中的哪一个位置停止(元素位置计算方式同上) - * @return 连接好的字符串 - */ - @Nonnull - public static String stringsConnecting ( - @Nonnull String[] array, @Nonnull String connector, @Nonnegative int startIndex, @Nonnegative int stopIndex - ) { - final StringBuilder builder = new StringBuilder(); - for (int i = startIndex; i < stopIndex; i++) { - builder.append(array[i]); - builder.append(connector); - } - builder.append(array[stopIndex]); - return builder.toString(); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java deleted file mode 100644 index 0e9c35d..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/CommonEncrypt.java +++ /dev/null @@ -1,144 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnull; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; - -/** - * 用于数据加密或编解码的工具类. - *

      - * 出于 java std 中 Base64 的 {@link Base64.Encoder encode}/{@link Base64.Decoder decode} 十分好用,在此不再进行包装。 - */ -public class CommonEncrypt { - - /** - * 在使用加密算法处理字符串时默认会使用的字符串编码. - *

      - * Morny 使用 UTF-8 编码因为这是一般而言加解密工具的默认行为 - */ - public static final Charset ENCRYPT_STANDARD_CHARSET = StandardCharsets.UTF_8; - - @Nonnull - private static byte[] hashAsJavaMessageDigest(String algorithm, @Nonnull byte[] data) { - try { - return MessageDigest.getInstance(algorithm).digest(data); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } - - /** - * 取得数据的 md5 散列值. - * - * @param data byte 数组形式的数据体 - * @return 二进制(byte数组)格式的数据的 md5 散列值 - */ - @Nonnull - public static byte[] hashMd5 (@Nonnull byte[] data) { - return hashAsJavaMessageDigest("md5", data); - } - - /** - * 取得一个字符串的 md5 散列值. - *

      - * 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析 - * - * @param originString 要进行散列的字符串 - * @return 二进制(byte数组)格式的 md5 散列值 - */ - @Nonnull - public static byte[] hashMd5 (String originString) { - return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET)); - } - - /** - * 取得数据的 sha1 散列值. - * - * @param data byte 数组形式的数据体 - * @return 二进制(byte数组)格式的数据的 sha1 散列值 - */ - @Nonnull - public static byte[] hashSha1 (@Nonnull byte[] data) { - return hashAsJavaMessageDigest("sha1", data); - } - - /** - * 取得一个字符串的 sha1 散列值. - *

      - * 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析 - * - * @param originString 要进行散列的字符串 - * @return 二进制(byte数组)格式的 sha1 散列值 - */ - @Nonnull - public static byte[] hashSha1 (String originString) { - return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET)); - } - - /** - * 取得数据的 sha256 散列值. - * - * @param data byte 数组形式的数据体 - * @return 二进制(byte数组)格式的数据的 sha256 散列值 - */ - @Nonnull - public static byte[] hashSha256 (@Nonnull byte[] data) { - return hashAsJavaMessageDigest("sha256", data); - } - - /** - * 取得一个字符串的 sha256 散列值. - *

      - * 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析 - * - * @param originString 要进行散列的字符串 - * @return 二进制(byte数组)格式的 sha256 散列值 - */ - @Nonnull - public static byte[] hashSha256 (String originString) { - return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET)); - } - - /** - * 取得数据的 sha512 散列值. - * - * @param data byte 数组形式的数据体 - * @return 二进制(byte数组)格式的数据的 sha512 散列值 - */ - @Nonnull - public static byte[] hashSha512 (@Nonnull byte[] data) { - return hashAsJavaMessageDigest("md5", data); - } - - /** - * 取得一个字符串的 sha512 散列值. - *

      - * 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析 - * - * @param originString 要进行散列的字符串 - * @return 二进制(byte数组)格式的 sha512 散列值 - */ - @Nonnull - public static byte[] hashSha512 (String originString) { - return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET)); - } - - @Nonnull - public static String base64FilenameLint (String inputName) { - if (inputName.endsWith(".b64")) { - return inputName.substring(0, inputName.length()-".b64".length()); - } else if (inputName.endsWith(".b64.txt")) { - return inputName.substring(0, inputName.length()-".b64.txt".length()); - } else if (inputName.endsWith(".base64")) { - return inputName.substring(0, inputName.length()-".base64".length()); - } else if (inputName.endsWith(".base64.txt")) { - return inputName.substring(0, inputName.length()-".base64.txt".length()); - } else { - return inputName; - } - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java deleted file mode 100644 index 0598ba6..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/CommonFormat.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; - -public class CommonFormat { - - public static final String DATE_TIME_PATTERN_FULL_MILLIS = "yyyy-MM-dd HH:mm:ss:SSS"; - - public static String formatDate (long timestamp, int utcOffset) { - return DateTimeFormatter.ofPattern(DATE_TIME_PATTERN_FULL_MILLIS).format(LocalDateTime.ofInstant( - Instant.ofEpochMilli(timestamp), - ZoneId.ofOffset("UTC", ZoneOffset.ofHours(utcOffset)) - )); - } - - public static String formatDuration (long duration) { - StringBuilder sb = new StringBuilder(); - if (duration > 1000 * 60 * 60 * 24) sb.append(duration / (1000*60*60*24)).append("d "); - if (duration > 1000 * 60 * 60) sb.append(duration / (1000*60*60) % 24).append("h "); - if (duration > 1000 * 60) sb.append(duration / (1000*60) % 60).append("min "); - if (duration > 1000) sb.append(duration / 1000 % 60).append("s "); - sb.append(duration % 1000).append("ms"); - return sb.toString(); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java b/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java deleted file mode 100644 index 22c9812..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/CommonRandom.java +++ /dev/null @@ -1,44 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnegative; -import java.util.concurrent.ThreadLocalRandom; - -public class CommonRandom { - - /** - * 通过 {@link ThreadLocalRandom} 以指定的一定几率返回 true. - * @param probability 一个正整数,决定在样本空间中有多大的可能性为 true。 - * @param base 一个正整数,决定样本空间有多大。 - * @return 有 {@code base} 分之 {@code probability} 的几率,返回值为 {@link true}. - * 如果 {@code probability} 大于 {@code base},也就是为 true 的可能性大于 100%,则会永远为 true。 - * @throws IllegalArgumentException - * 当参数 base 或是 probability 不为正整数时 - * @since 1.0.0-RC3.2 - */ - public static boolean probabilityTrue (@Nonnegative int probability, @Nonnegative int base) { - if (probability < 1) throw new IllegalArgumentException("the probability must be a positive value!"); - if (base < 1) throw new IllegalArgumentException("the probability base must be a positive value!"); - return probability > ThreadLocalRandom.current().nextInt(base); - } - - /** - * 以一定几率返回 true. - * @return {@code probabilityIn} 分之 {@link 1} 的几率为 {@link true}. - * @see #probabilityTrue(int, int) - * @since 1.0.0-RC3.2 - */ - public static boolean probabilityTrue (@Nonnegative int probabilityIn) { - return (probabilityTrue(1, probabilityIn)); - } - - /** - * 通过 {@link ThreadLocalRandom} 实现的随机 boolean 取值. - * @return 随机的 {@link true} 或 {@link false},各占(近似)一半可能性. - * @see ThreadLocalRandom#nextBoolean() - * @since 1.0.0-RC3.2 - */ - public static boolean iif () { - return ThreadLocalRandom.current().nextBoolean(); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java b/src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java deleted file mode 100644 index 57ff657..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/FileUtils.java +++ /dev/null @@ -1,28 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnull; -import java.io.FileInputStream; -import java.io.IOException; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class FileUtils { - - @Nonnull - public static String getMD5Three (@Nonnull String path) throws IOException, NoSuchAlgorithmException { - final BigInteger bi; - final byte[] buffer = new byte[8192]; - int len; - final MessageDigest md = MessageDigest.getInstance("MD5"); - final FileInputStream fis = new FileInputStream(path); - while ((len = fis.read(buffer)) != -1) { - md.update(buffer, 0, len); - } - fis.close(); - final byte[] b = md.digest(); - bi = new BigInteger(1, b); - return bi.toString(16); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java b/src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java deleted file mode 100644 index fb6b32a..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/OkHttpPublic.java +++ /dev/null @@ -1,13 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import okhttp3.MediaType; - -public class OkHttpPublic { - - public static class MediaTypes { - - public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java b/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java deleted file mode 100644 index 2bf2e8e..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/UniversalCommand.java +++ /dev/null @@ -1,49 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import javax.annotation.Nonnull; -import java.util.ArrayList; - -public class UniversalCommand { - - @Nonnull - public static String[] format (@Nonnull String com) { - - final ArrayList arr = new ArrayList<>(); - - final StringBuilder tmp = new StringBuilder(); - final char[] coma = com.toCharArray(); - for (int i = 0; i < coma.length; i++) { - if (coma[i] == ' ') { - if (!tmp.toString().equals("")) { arr.add(tmp.toString()); } - tmp.setLength(0); - } else if (coma[i] == '"') { - while (true) { - i++; - if (i >= coma.length) { - break; - } else if (coma[i] == '"') { - break; - } else if (coma[i] == '\\' && i+1 < coma.length && (coma[i+1] == '"' || coma[i+1] == '\\')) { - i++; - tmp.append(coma[i]); - } else { - tmp.append(coma[i]); - } - } - } else if (coma[i] == '\\' && i+1 < coma.length && (coma[i+1] == ' ' || coma[i+1] == '"' || coma[i+1] == '\\')) { - i++; - tmp.append(coma[i]); - } else { - tmp.append(coma[i]); - } - } - if (!tmp.toString().equals("")) { arr.add(tmp.toString()); } - tmp.setLength(0); - - final String[] out = new String[arr.size()]; - arr.toArray(out); - return out; - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java deleted file mode 100644 index a62a3c9..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/InputCommand.java +++ /dev/null @@ -1,66 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi; - -import cc.sukazyo.cono.morny.util.UniversalCommand; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Arrays; - -public class InputCommand { - - - private final String target; - private final String command; - private final String[] args; - - private InputCommand (@Nullable String target, @Nonnull String command, @Nonnull String[] args) { - this.target = target; - this.command = command; - this.args = args; - } - - public InputCommand (@Nonnull String[] inputArray) { - this(parseInputArray(inputArray)); - } - - public InputCommand (@Nonnull String input) { - this(UniversalCommand.format(input)); - } - - public InputCommand (@Nonnull InputCommand source) { - this(source.target, source.command, source.args); - } - - public static InputCommand parseInputArray (@Nonnull String[] inputArray) { - final String[] cx = inputArray[0].split("@", 2); - final String[] args = new String[inputArray.length-1]; - System.arraycopy(inputArray, 1, args, 0, inputArray.length - 1); - return new InputCommand(cx.length == 1 ? null : cx[1], cx[0], args); - } - - @Nullable - public String getTarget () { - return target; - } - - @Nonnull - public String getCommand () { - return command; - } - - @Nonnull - public String[] getArgs () { - return args; - } - - public boolean hasArgs () { - return args.length != 0; - } - - @Override - @Nonnull - public String toString() { - return String.format("{{%s}@{%s}#{%s}}", command, target, Arrays.toString(args)); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/Standardize.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/Standardize.java deleted file mode 100644 index ae3efa3..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/Standardize.java +++ /dev/null @@ -1,7 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi; - -public class Standardize { - - public static final int CHANNEL_SPEAKER_MAGIC_ID = 136817688; - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java deleted file mode 100644 index 368d3b9..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.java +++ /dev/null @@ -1,35 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.event; - -import com.pengrad.telegrambot.response.BaseResponse; - -public class EventRuntimeException extends RuntimeException { - - public EventRuntimeException () { - super(); - } - - public EventRuntimeException (String message) { - super(message); - } - - public static class ActionFailed extends EventRuntimeException { - - private final BaseResponse response; - - public ActionFailed (BaseResponse response) { - super(); - this.response = response; - } - - public ActionFailed (String message, BaseResponse response) { - super(message); - this.response = response; - } - - public BaseResponse getResponse() { - return response; - } - - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java deleted file mode 100644 index 06703a4..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/MsgEscape.java +++ /dev/null @@ -1,15 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import javax.annotation.Nonnull; - -public class MsgEscape { - - @Nonnull - public static String escapeHtml (@Nonnull String raw) { - raw = raw.replaceAll("&", "&"); - raw = raw.replaceAll("<", "<"); - raw = raw.replaceAll(">", ">"); - return raw; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java deleted file mode 100644 index b046f30..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/NamedUtils.java +++ /dev/null @@ -1,18 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import cc.sukazyo.cono.morny.util.CommonConvert; -import cc.sukazyo.cono.morny.util.CommonEncrypt; - -import javax.annotation.Nonnull; - -public class NamedUtils { - - public static String inlineIds (@Nonnull String tag) { - return inlineIds(tag, ""); - } - - public static String inlineIds (@Nonnull String tag, @Nonnull String taggedData) { - return CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(tag+taggedData)); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java deleted file mode 100644 index 027f515..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToString.java +++ /dev/null @@ -1,21 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.Message; -import com.pengrad.telegrambot.model.User; - -public class TGToString { - - public static TGToStringFromChat as (Chat chat) { - return new TGToStringFromChat(chat); - } - - public static TGToStringFromUser as (User user) { - return new TGToStringFromUser(user); - } - - public static TGToStringFromMessage as (Message message) { - return new TGToStringFromMessage(message); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java deleted file mode 100644 index e327279..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromChat.java +++ /dev/null @@ -1,60 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.Chat; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public class TGToStringFromChat { - - - public static final long MASK_BOTAPI_ID = -1000000000000L; - - private final Chat data; - - public TGToStringFromChat(Chat chat) { - this.data = chat; - } - - public String toStringFullNameId() { - if (data.title() == null) { - throw new IllegalArgumentException("Cannot format private chat to group Name+Id format."); - } - return (data.username() == null) ? - (String.format("%s [%d]", data.title(), data.id())) : - (String.format("%s {%s}[%d]", data.title(), data.username(), data.id())); - } - - @Nonnull - public String getSafeName () { - if (data.type() == Chat.Type.Private) - return data.firstName() + (data.lastName()==null ? "" : " "+data.lastName()); - else return data.title(); - } - - @Nullable - public String getSafeLinkHTML () { - if (data.username() == null) { - if (data.type() == Chat.Type.Private) - // language=html - return String.format("@[u:%d]", data.id(), data.id()); - // language=html - else return String.format("@[c/%d]", id_tdLib(), id_tdLib()); - } else return "@"+data.username(); - } - - public long id_tdLib () { - return data.id() < 0 ? Math.abs(data.id() - MASK_BOTAPI_ID) : data.id(); - } - - @Nonnull - public String getTypeTag () { - return switch (data.type()) { - case Private -> "🔒"; - case group -> "💭"; - case supergroup -> "💬"; - case channel -> "📢"; - }; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java deleted file mode 100644 index 08b2d94..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromMessage.java +++ /dev/null @@ -1,27 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.Message; - -import javax.annotation.Nonnull; - -public class TGToStringFromMessage extends TGToString { - - @Nonnull - private final Message message; - - public TGToStringFromMessage (@Nonnull Message message) { this.message = message; } - - @Nonnull - public String getSenderFirstNameRefHtml () { - return message.senderChat()==null ? TGToString.as(message.from()).firstnameRefHtml() : String.format( - "%s", - message.senderChat().id(), - MsgEscape.escapeHtml(message.senderChat().title()) - ); - } - - public long getSenderId () { - return message.senderChat()==null ? message.from().id() : message.senderChat().id(); - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java deleted file mode 100644 index fa41d3c..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TGToStringFromUser.java +++ /dev/null @@ -1,53 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.User; - -public class TGToStringFromUser { - - private final User data; - - public TGToStringFromUser (User user) { - this.data = user; - } - - public String fullname () { - return data.firstName() + (data.lastName()==null ? "" : " "+data.lastName()); - } - - public String fullnameRefHtml () { - return String.format( - "%s", - data.id(), - MsgEscape.escapeHtml(fullname()) - ); - } - - public String fullnameRefMarkdown () { - return String.format( - "[%s](tg://user?id=%d)", - fullname(), - data.id() - ); - } - - public String firstnameRefHtml () { - return String.format( - "%s", - data.id(), - MsgEscape.escapeHtml(data.firstName()) - ); - } - - public String firstnameRefMarkdown () { - return String.format( - "[%s](tg://user?id=%d)", - data.firstName(), - data.id() - ); - } - - public String toStringLogTag () { - return (data.username()==null ? fullname()+" " : "@"+data.username()) + "[" + data.id() + "]"; - } - -} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java b/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java deleted file mode 100644 index e1703e4..0000000 --- a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.java +++ /dev/null @@ -1,85 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi.formatting; - -import com.pengrad.telegrambot.model.User; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; - -public class TelegramUserInformation { - - public static final String DC_QUERY_SOURCE_SITE = "https://t.me/"; - public static final Pattern DC_QUERY_PROCESSOR_REGEX = Pattern.compile("(cdn[1-9]).tele(sco.pe|gram-cdn.org)"); - - private static final OkHttpClient httpClient = new OkHttpClient(); - - @Nullable - public static String getDataCenterFromUsername (String username) { - final Request request = new Request.Builder().url(DC_QUERY_SOURCE_SITE + username).build(); - try (Response response = httpClient.newCall(request).execute()) { - final ResponseBody body = response.body(); - if (body == null) return "empty upstream response"; - final Matcher matcher = DC_QUERY_PROCESSOR_REGEX.matcher(body.string()); - if (matcher.find()) { - return matcher.group(1); - } - } catch (IOException e) { - return e.getMessage(); - } - return null; - } - - public static String informationOutputHTML (User user) { - - final StringBuilder userInformation = new StringBuilder(); - userInformation.append(String.format( - """ - userid : - - %d""", - user.id() - )); - if (user.username() == null) { - userInformation.append("\nusername : null\ndatacenter : null"); - } else { - userInformation.append(String.format( - """ - - username : - - %s""", - escapeHtml(user.username()) - )); - // 依赖 username 的 datacenter 查询 - final String dataCenter = getDataCenterFromUsername(user.username()); - if (dataCenter == null) { userInformation.append("\ndatacenter : null"); } - else { userInformation.append(String.format("\ndatacenter : %s", escapeHtml(dataCenter))); } - } - userInformation.append(String.format( - """ - - display name : - - %s%s""", - escapeHtml(user.firstName()), - user.lastName()==null ? "" : String.format("\n- %s", escapeHtml(user.lastName())) - )); - if (user.languageCode() != null) { - userInformation.append(String.format( - """ - - language-code : - - %s""", - escapeHtml(user.languageCode()) - )); - } - - return userInformation.toString(); - - } - -} diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala index 6bfad74..1860236 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -2,11 +2,11 @@ package cc.sukazyo.cono.morny import cc.sukazyo.cono.morny.bot.command.MornyCommands import cc.sukazyo.cono.morny.daemon.MornyDaemons -import cc.sukazyo.cono.morny.util.tgapi.ExtraAction import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.MornyCoeur.THREAD_MORNY_EXIT import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener import cc.sukazyo.cono.morny.bot.event.MornyEventListeners +import cc.sukazyo.cono.morny.util.tgapi.ExtraAction import com.pengrad.telegrambot.TelegramBot import com.pengrad.telegrambot.request.GetMe diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala index 69fb74f..7e052eb 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala @@ -1,13 +1,13 @@ package cc.sukazyo.cono.morny import cc.sukazyo.cono.morny.internal.BuildConfigField +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.daemon.MornyReport import cc.sukazyo.cono.morny.util.FileUtils import java.io.IOException import java.net.URISyntaxException import java.security.NoSuchAlgorithmException -import Log.{exceptionLog, logger} -import cc.sukazyo.cono.morny.daemon.MornyReport object MornySystem { diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala index 9eed851..98b7852 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala @@ -1,9 +1,9 @@ package cc.sukazyo.cono.morny.bot.api import cc.sukazyo.cono.morny.Log -import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.daemon.MornyReport +import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException import com.google.gson.GsonBuilder import com.pengrad.telegrambot.model.Update @@ -65,7 +65,7 @@ object EventListenerManager { case actionFailed: EventRuntimeException.ActionFailed => errorMessage ++= "\ntg-api action: response track: " errorMessage ++= (GsonBuilder().setPrettyPrinting().create().toJson( - actionFailed.getResponse + actionFailed.response ) indent 4) ++= "\n" case _ => logger error errorMessage.toString diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala index 975c47b..f1faef6 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala @@ -4,10 +4,10 @@ import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.daemon.MornyReport import cc.sukazyo.cono.morny.data.TelegramStickers -import cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex +import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.CommonEncrypt import cc.sukazyo.cono.morny.util.CommonEncrypt.* -import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex import com.pengrad.telegrambot.model.{PhotoSize, Update} import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{GetFile, SendDocument, SendMessage, SendSticker} @@ -16,6 +16,7 @@ import java.io.IOException import java.util.Base64 import scala.language.postfixOps +/** Provides Telegram Command __`/encrypt`__. */ object Encryptor extends ITelegramCommand { override val name: String = "encrypt" @@ -25,12 +26,20 @@ object Encryptor extends ITelegramCommand { override def execute (using command: InputCommand, event: Update): Unit = { - val args = command.getArgs + val args = command.args + // show a simple help page if ((args isEmpty) || ((args(0) equals "l") && (args.length == 1))) echoHelp(event.message.chat.id, event.message.messageId) return + // for mod-params: + // mod-params is the args belongs to the encrypt algorithm. + // due to the algorithm is defined in the 1st (array(0)) arg, + // so the mod-params is which defined since the 2nd arg. also + // due to there's only one mod-param yet (it is uppercase), + // so the algorithm will be and must be in the 2nd arg. + /** inner function: is input `arg` means mod-param ''uppercase'' */ def _is_mod_u(arg: String): Boolean = if (arg equalsIgnoreCase "uppercase") return true if (arg equalsIgnoreCase "u") return true @@ -46,13 +55,21 @@ object Encryptor extends ITelegramCommand { return } else false - trait XEncryptable { val asByteArray: Array[Byte] } - case class XFile (data: Array[Byte], name: String) extends XEncryptable { + // BLOCK: get input + // for now, only support getting data from replied message, and + // this message CAN ONLY have texts or an universal file: if the + // universal files are not only one, only the first one can be get. + // - do NOT SUPPORT telegram inline image/video/autio yet + // - do NOT SUPPORT multi-file yet + // todo: support inline image/video/audio file and multi-files. + /** inner trait: the encryptable data abstract */ + trait XEncryptable { /** standards data to [[Array]]`[`[[Byte]]`]` for processing */ val asByteArray: Array[Byte] } + /** inner class: the [[XEncryptable]] implementation of binary([[Array]]`[`[[Byte]]`]`) data (file or something) */ + case class XFile (data: Array[Byte], name: String) extends XEncryptable: val asByteArray: Array[Byte] = data - } - case class XText (data: String) extends XEncryptable { + /** inner class: the [[XEncryptable]] implementation of [[String]] data */ + case class XText (data: String) extends XEncryptable: val asByteArray: Array[Byte] = data getBytes CommonEncrypt.ENCRYPT_STANDARD_CHARSET - } val input: XEncryptable = val _r = event.message.replyToMessage if ((_r ne null) && (_r.document ne null)) { @@ -73,9 +90,10 @@ object Encryptor extends ITelegramCommand { _photo_origin = size _photo_size = _size if (_photo_origin eq null) throw IllegalArgumentException("no photo from api.") + import cc.sukazyo.cono.morny.util.UseRandom.rand_id XFile( MornyCoeur.account getFileContent (MornyCoeur.extra exec GetFile(_photo_origin.fileId)).file, - s"photo${byteArrayToHex(hashMd5(System.currentTimeMillis toString)) substring 32-12 toUpperCase}.png" + s"photo$rand_id.png" ) } catch case e: IOException => @@ -95,19 +113,26 @@ object Encryptor extends ITelegramCommand { ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) return } + // END BLOCK: get input - + // BLOCK: encrypt + /** inner class: encrypt result implementation of text-like (can be described as [[String]]). */ trait EXTextLike { val text: String } + /** inner class: encrypt result implementation of a file */ case class EXFile (result: Array[Byte], resultName: String) - case class EXText (result: String) extends EXTextLike { override val text:String = result } - case class EXHash (result: String) extends EXTextLike { override val text:String = result } + /** inner class: [[EXTextLike]] implementation of just normal text */ + case class EXText (text: String) extends EXTextLike + /** inner class: [[EXTextLike]] implementation of a special type: hash value */ + case class EXHash (text: String) extends EXTextLike + /** generate encrypt result by making normal encrypt: output type == input type */ def genResult_encrypt (source: XEncryptable, processor: Array[Byte]=>Array[Byte], filenameProcessor: String=>String): EXFile|EXText = { source match case x_file: XFile => EXFile(processor(x_file asByteArray), filenameProcessor(x_file.name)) - case x: XText => EXText(String(processor(x asByteArray), ENCRYPT_STANDARD_CHARSET)) + case x: XText => EXText(String(processor(x asByteArray), CommonEncrypt.ENCRYPT_STANDARD_CHARSET)) } + /** generate encrypt result by making hash: output type == hash value */ def genResult_hash (source: XEncryptable, processor: Array[Byte]=>Array[Byte]): EXHash = - val hashed = byteArrayToHex(processor(source asByteArray)) + val hashed = processor(source asByteArray) toHex; EXHash(if mod_uppercase then hashed toUpperCase else hashed) val result: EXHash|EXFile|EXText = args(0) match case "base64" | "b64" | "base64url" | "base64u" | "b64u" => @@ -126,24 +151,26 @@ object Encryptor extends ITelegramCommand { try { genResult_encrypt( input, _tool_b64d.decode, - CommonEncrypt.base64FilenameLint + CommonEncrypt.lint_base64FileName ) } catch case _: IllegalArgumentException => MornyCoeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_404 // todo: is here better erro notify? ).replyToMessageId(event.message.messageId) return - case "md5" => genResult_hash(input, hashMd5) - case "sha1" => genResult_hash(input, hashSha1) - case "sha256" => genResult_hash(input, hashSha256) - case "sha512" => genResult_hash(input, hashSha512) + case "md5" => genResult_hash(input, MD5) + case "sha1" => genResult_hash(input, SHA1) + case "sha256" => genResult_hash(input, SHA256) + case "sha512" => genResult_hash(input, SHA512) case _ => MornyCoeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) return; + // END BLOCK: encrypt + // output result match case _file: EXFile => MornyCoeur.extra exec SendDocument( @@ -151,7 +178,7 @@ object Encryptor extends ITelegramCommand { _file.result ).fileName(_file.resultName).replyToMessageId(event.message.messageId) case _text: EXTextLike => - import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h MornyCoeur.extra exec SendMessage( event.message.chat.id, s"

      ${h(_text.text)}
      " @@ -159,6 +186,30 @@ object Encryptor extends ITelegramCommand { } + /** echo help to a specific message in a specific chat. + * + * === the help message === + * The first paragraph lists available encrypt algorithms and its alias, + * each line have one algorithm where the first name highlighted is the + * main name and following is aliases separated with `,`. + * with the separator `---`, the second paragraph lists available mods + * for algorithms, displays with the same rule of algorithms, with an extra + * italic text following describes its usage environment. + * + * when output to telegram just like: + *
      + * '''__base64__''', b64
      + * '''__base64url__''', base64u, b64u
      + * '''__base64decode__''', base64d, b64d
      + * '''__base64url-decode__''', base64ud, b64ud
      + * '''__sha1__'''
      + * '''__sha256__'''
      + * '''__sha512__'''
      + * '''__md5__'''
      + * ---
      + * '''__uppercase__''', upper, u ''(sha1/sha256/sha512/md5 only)'' + *
      + */ private def echoHelp(chat: Long, replyTo: Int): Unit = MornyCoeur.extra exec SendMessage( chat, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala index 5a6d9e8..963c80a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala @@ -1,10 +1,10 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle +import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle.{registerHack, HackType} +import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.Update -import OnEventHackHandle.{HackType, registerHack} -import cc.sukazyo.cono.morny.data.TelegramStickers import com.pengrad.telegrambot.request.SendSticker import scala.language.postfixOps @@ -18,7 +18,7 @@ object EventHack extends ITelegramCommand { override def execute (using command: InputCommand, event: Update): Unit = { - val x_mode = if (command.hasArgs) command.getArgs()(0) else "" + val x_mode = if (command.args nonEmpty) command.args(0) else "" def done_ok = MornyCoeur.extra exec SendSticker( diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala index 7799e06..78013bd 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala @@ -18,7 +18,7 @@ object GetUsernameAndId extends ITelegramCommand { override def execute (using command: InputCommand, event: Update): Unit = { - val args = command.getArgs + val args = command.args if (args.length > 1) MornyCoeur.extra exec SendMessage( @@ -59,7 +59,7 @@ object GetUsernameAndId extends ITelegramCommand { MornyCoeur.extra exec SendMessage( event.message.chat.id, - TelegramUserInformation informationOutputHTML user + TelegramUserInformation getFormattedInformation user ).replyToMessageId(event.message.messageId()).parseMode(ParseMode HTML) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala index b49f50d..a5d17b6 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala @@ -31,15 +31,15 @@ object IP186Query { private def query (using event: Update, command: InputCommand): Unit = { val target: String|Null = - if (command.getArgs isEmpty) + if (command.args isEmpty) if event.message.replyToMessage eq null then null else event.message.replyToMessage.text - else if (command.getArgs.length > 1) + else if (command.args.length > 1) MornyCoeur.extra exec SendMessage( event.message.chat.id, "[Unavailable] Too much arguments." ).replyToMessageId(event.message.messageId) return - else command.getArgs()(0) + else command.args(0) if (target eq null) MornyCoeur.extra exec new SendMessage( @@ -48,14 +48,15 @@ object IP186Query { ).replyToMessageId(event.message.messageId) return; + + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h try { - val response = command.getCommand match + val response = command.command match case Subs.IP.cmd => IP186QueryHandler.query_ip(target) case Subs.WHOIS.cmd => IP186QueryHandler.query_whoisPretty(target) - case _ => throw IllegalArgumentException(s"Unknown 186-IP query method ${command.getCommand}") + case _ => throw IllegalArgumentException(s"Unknown 186-IP query method ${command.command}") - import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h MornyCoeur.extra exec SendMessage( event.message.chat.id, s"""${h(response.url)} @@ -64,7 +65,6 @@ object IP186Query { ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) } catch case e: Exception => - import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h MornyCoeur.extra exec new SendMessage( event.message().chat().id(), s"""[Exception] in query: diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala index a6fbdf8..4776d68 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala @@ -1,9 +1,9 @@ package cc.sukazyo.cono.morny.bot.command -import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.{BotCommand, DeleteMyCommands, Update} import com.pengrad.telegrambot.request.{SendSticker, SetMyCommands} @@ -60,14 +60,14 @@ object MornyCommands { ) def execute (using command: InputCommand, event: Update): Boolean = { - if (commands contains command.getCommand) - commands(command.getCommand) execute; + if (commands contains command.command) + commands(command.command) execute; true else nonCommandExecutable } private def nonCommandExecutable (using command: InputCommand, event: Update): Boolean = { - if command.getTarget eq null then false + if command.target eq null then false else MornyCoeur.extra exec SendSticker( event.message.chat.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala index ea310dc..80c4519 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala @@ -1,8 +1,8 @@ package cc.sukazyo.cono.morny.bot.command -import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.command.ICommandAlias.ListedAlias import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendSticker diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala index f9c46bb..516ddd7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -3,8 +3,8 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornyCoeur, MornySystem} import cc.sukazyo.cono.morny.data.{TelegramImages, TelegramStickers} import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.tgapi.InputCommand -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{SendMessage, SendPhoto, SendSticker} @@ -30,12 +30,12 @@ object MornyInformation extends ITelegramCommand { override def execute (using command: InputCommand, event: Update): Unit = { - if (!command.hasArgs) { + if (command.args isEmpty) { echoInfo(event.message.chat.id, event.message.messageId) return } - val action: String = command.getArgs()(0) + val action: String = command.args(0) action match { case s if s startsWith Subs.STICKERS => echoStickers @@ -94,13 +94,13 @@ object MornyInformation extends ITelegramCommand { private def echoStickers (using command: InputCommand, event: Update): Unit = { val mid: String|Null = - if (command.getArgs()(0) == Subs.STICKERS) { - if (command.getArgs.length == 1) "" - else if (command.getArgs.length == 2) command.getArgs()(1) + if (command.args(0) == Subs.STICKERS) { + if (command.args.length == 1) "" + else if (command.args.length == 2) command.args(1) else null - } else if (command.getArgs.length == 1) { - if ((command.getArgs()(0) startsWith s"${Subs.STICKERS}.") || (command.getArgs()(0) startsWith s"${Subs.STICKERS}#")) { - command.getArgs()(0) substring Subs.STICKERS.length+1 + } else if (command.args.length == 1) { + if ((command.args(0) startsWith s"${Subs.STICKERS}.") || (command.args(0) startsWith s"${Subs.STICKERS}#")) { + command.args(0) substring Subs.STICKERS.length+1 } else null } else null if (mid == null) echo404 diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala index 6f44f16..cad2acf 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala @@ -1,4 +1,5 @@ package cc.sukazyo.cono.morny.bot.command + import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.Update diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala index bb69c48..8800c9f 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala @@ -1,15 +1,15 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.bot.command.ICommandAlias.HiddenAlias -import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.daemon.MornyReport +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendSticker import scala.language.postfixOps -import cc.sukazyo.cono.morny.Log.logger -import cc.sukazyo.cono.morny.daemon.MornyReport object MornyManagers { @@ -30,7 +30,7 @@ object MornyManagers { event.message.chat.id, TelegramStickers ID_EXIT ).replyToMessageId(event.message.messageId) - logger info s"Morny exited by user ${(TGToString as user) toStringLogTag}" + logger info s"Morny exited by user ${user toLogTag}" MornyCoeur.exit(0, user) } else { @@ -39,7 +39,7 @@ object MornyManagers { event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) - logger info s"403 exit caught from user ${(TGToString as user) toStringLogTag}" + logger info s"403 exit caught from user ${user toLogTag}" MornyReport.unauthenticatedAction("/exit", user) } @@ -61,7 +61,7 @@ object MornyManagers { if (MornyCoeur.trusted isTrusted user.id) { - logger info s"call save from command by ${(TGToString as user) toStringLogTag}" + logger info s"call save from command by ${user toLogTag}" MornyCoeur.callSaveData() MornyCoeur.extra exec SendSticker( event.message.chat.id, @@ -74,7 +74,7 @@ object MornyManagers { event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) - logger info s"403 save caught from user ${(TGToString as user) toStringLogTag}" + logger info s"403 save caught from user ${user toLogTag}" MornyReport.unauthenticatedAction("/save", user) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala index 23fa2fa..327d7df 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala @@ -1,9 +1,9 @@ package cc.sukazyo.cono.morny.bot.command -import cc.sukazyo.cono.morny.util.tgapi.InputCommand -import com.pengrad.telegrambot.model.Update import cc.sukazyo.cono.morny.data.MornyJrrp import cc.sukazyo.cono.morny.MornyCoeur -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendMessage @@ -24,11 +24,11 @@ object MornyOldJrrp extends ITelegramCommand { case a if a > 30 => ";" case _ => "..." - import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h MornyCoeur.extra exec SendMessage( event.message.chat.id, // language=html - f"${(TGToString as user) fullnameRefHtml} 在(utc的)今天的运气指数是———— $jrrp%.2f%%${h(ending)}" + f"${user.fullnameRefHTML} 在(utc的)今天的运气指数是———— $jrrp%.2f%%${h(ending)}" ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala index 80c0c46..4d7e343 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala @@ -2,8 +2,8 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.{NbnhhshQuery, TelegramStickers} +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.tgapi.InputCommand -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{SendMessage, SendSticker} @@ -25,11 +25,10 @@ object Nbnhhsh extends ITelegramCommand { override def execute (using command: InputCommand, event: Update): Unit = { val queryTarget: String|Null = - import cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting if (event.message.replyToMessage != null && event.message.replyToMessage.text != null) event.message.replyToMessage.text - else if command hasArgs then - stringsConnecting(command.getArgs, " ", 0, command.getArgs.length-1) + else if command.args nonEmpty then + command.args mkString " " else null if (queryTarget == null) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala index 0e249d4..8696bb8 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala @@ -2,7 +2,8 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.util.tgapi.InputCommand -import cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue +import cc.sukazyo.cono.morny.util.UseMath.over +import cc.sukazyo.cono.morny.util.UseRandom.* import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage @@ -13,7 +14,7 @@ object 私わね extends ISimpleCommand { override def execute (using command: InputCommand, event: Update): Unit = { - if (probabilityTrue(521)) { + if ((1 over 521) chance_is true) { val text = "/打假" MornyCoeur.extra exec new SendMessage( event.message.chat.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala index 68ad75c..a8d16b3 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -3,9 +3,9 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.data.TelegramStickers -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString -import com.pengrad.telegrambot.model.request.ParseMode +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* import com.pengrad.telegrambot.model.{Chat, Message, Update, User} +import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{ForwardMessage, GetChat, SendMessage, SendSticker} import scala.language.postfixOps @@ -44,7 +44,7 @@ object OnCallMe extends EventListener { MornyCoeur.extra exec SendMessage( me, s"""request $itemHTML - |from ${(TGToString as user) fullnameRefHtml}${if extra == null then "" else "\n"+extra}""" + |from ${user.fullnameRefHTML}${if extra == null then "" else "\n"+extra}""" .stripMargin ).parseMode(ParseMode HTML) @@ -59,7 +59,7 @@ object OnCallMe extends EventListener { lastDinnerData.forwardFromMessageId ) import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} - import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h def lastDinner_dateMillis: Long = lastDinnerData.forwardDate longValue; MornyCoeur.extra exec SendMessage( req.from.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala index d4bb8ca..1065a29 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala @@ -3,7 +3,6 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString import com.pengrad.telegrambot.model.{Chat, Message, MessageEntity, Update} import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{GetChat, SendMessage, SendSticker} @@ -108,11 +107,11 @@ object OnCallMsgSend extends EventListener { val targetChatResponse = MornyCoeur.account execute GetChat(messageToSend.targetId) if (targetChatResponse isOk) { def getChatDescriptionHTML (chat: Chat): String = - import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h - val _c = TGToString as chat + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h // language=html s"""${h(chat.id toString)}@${h(chat.`type`.name)}${if (chat.`type` != Chat.Type.Private) ":::" else ""} - |${_c getTypeTag} ${h(_c getSafeName)} ${_c getSafeLinkHTML}""" + |${chat.typeTag} ${h(chat.safe_name)} ${chat.safe_linkHTML}""" .stripMargin MornyCoeur.extra exec SendMessage( update.message.chat.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala index d618ddc..121b3ef 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala @@ -1,8 +1,6 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.bot.api.EventListener - -import scala.collection.mutable import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.MornyCoeur import com.google.gson.GsonBuilder @@ -10,6 +8,7 @@ import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendMessage +import scala.collection.mutable import scala.language.postfixOps object OnEventHackHandle extends EventListener { @@ -39,7 +38,7 @@ object OnEventHackHandle extends EventListener { else if hackers contains "[[]]" then (hackers remove "[[]]")get else return false logger debug s"hacked event by $x" - import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h MornyCoeur.extra exec SendMessage( x.from_chat, // language=html diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala index 825f2d5..601cafc 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala @@ -15,8 +15,9 @@ object OnQuestionMarkReply extends EventListener { if event.message.text eq null then return false - import cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue - if !probabilityTrue(8) then return false + import cc.sukazyo.cono.morny.util.UseMath.over + import cc.sukazyo.cono.morny.util.UseRandom.chance_is + if (1 over 8) chance_is false then return false for (c <- event.message.text toCharArray) if !(QUESTION_MARKS contains c) then return false diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala index b82a66a..a2ef6ea 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala @@ -1,11 +1,11 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.bot.api.EventListener -import cc.sukazyo.cono.morny.util.tgapi.InputCommand -import com.pengrad.telegrambot.model.{Message, Update} import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.command.MornyCommands +import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import com.pengrad.telegrambot.model.{Message, Update} object OnTelegramCommand extends EventListener { @@ -19,10 +19,10 @@ object OnTelegramCommand extends EventListener { if !_isCommandMessage(update.message) then return false val inputCommand = InputCommand(update.message.text drop 1) - if (!(inputCommand.getCommand matches "^\\w+$")) + if (!(inputCommand.command matches "^\\w+$")) logger debug "not command" false - else if ((inputCommand.getTarget ne null) && (inputCommand.getTarget ne MornyCoeur.username)) + else if ((inputCommand.target ne null) && (inputCommand.target ne MornyCoeur.username)) logger debug "not morny command" false else diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala index 1657169..df18cb7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala @@ -17,13 +17,13 @@ object OnUserRandom extends EventListener { if update.message.text == null then return false if update.message.text startsWith "/" then return false - import cc.sukazyo.cono.morny.util.CommonRandom.iif + import cc.sukazyo.cono.morny.util.UseRandom.rand_half val query = update.message.text substring 1 val result: String|Null = query match case USER_OR_QUERY(_con1, _con2) => - if iif then _con1 else _con2 + if rand_half then _con1 else _con2 case USER_IF_QUERY(_con) => - (if iif then "不" else "") + _con + (if rand_half then "不" else "") + _con case _ => null if result == null then return false diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala index 9447a91..1778dfc 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala @@ -2,9 +2,9 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.UniversalCommand -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendMessage @@ -22,7 +22,7 @@ object OnUserSlashAction extends EventListener { if (text startsWith "/") { - val actions = UniversalCommand format text + val actions = UniversalCommand(text) actions(0) = actions(0) substring 1 actions(0) @@ -51,11 +51,11 @@ object OnUserSlashAction extends EventListener { MornyCoeur.extra exec SendMessage( update.message.chat.id, "%s %s%s %s %s!".format( - (TGToString as origin) getSenderFirstNameRefHtml, + origin.sender_firstnameRefHTML, h(v_verb), if hasObject then "" else "了", if (origin == target) - s"自己" - else (TGToString as target) getSenderFirstNameRefHtml, + s"自己" + else origin.sender_firstnameRefHTML, if hasObject then h(v_object+" ") else "" ) ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala index 6275c2e..b6066e0 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala @@ -1,6 +1,6 @@ package cc.sukazyo.cono.morny.bot.query -import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} @@ -18,9 +18,9 @@ object MyInformation extends ITelegramQuery { List( InlineQueryUnit(InlineQueryResultArticle( - inlineIds(ID_PREFIX), TITLE, + inlineQueryId(ID_PREFIX), TITLE, new InputTextMessageContent( - TelegramUserInformation informationOutputHTML event.inlineQuery.from + TelegramUserInformation getFormattedInformation event.inlineQuery.from ).parseMode(ParseMode HTML) )).isPersonal(true).cacheTime(10) ) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala index aec52f6..4fe13c8 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala @@ -1,5 +1,5 @@ package cc.sukazyo.cono.morny.bot.query -import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent} @@ -16,7 +16,7 @@ object RawText extends ITelegramQuery { List( InlineQueryUnit(InlineQueryResultArticle( - inlineIds(ID_PREFIX, event.inlineQuery.query), TITLE, + inlineQueryId(ID_PREFIX, event.inlineQuery.query), TITLE, InputTextMessageContent(event.inlineQuery.query) )) ) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala index bc1a4e7..7b2f750 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala @@ -1,8 +1,8 @@ package cc.sukazyo.cono.morny.bot.query import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId import cc.sukazyo.cono.morny.util.BiliTool -import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} @@ -60,11 +60,11 @@ object ShareToolBilibili extends ITelegramQuery { List( InlineQueryUnit(InlineQueryResultArticle( - inlineIds(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av, + inlineQueryId(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av, InputTextMessageContent(SHARE_FORMAT_HTML.format(link_av, id_av)).parseMode(ParseMode HTML) )), InlineQueryUnit(InlineQueryResultArticle( - inlineIds(ID_PREFIX_BILI_BV + bv), TITLE_BILI_BV + bv, + inlineQueryId(ID_PREFIX_BILI_BV + bv), TITLE_BILI_BV + bv, InputTextMessageContent(SHARE_FORMAT_HTML.format(link_bv, id_bv)).parseMode(ParseMode HTML) )) ) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala index ffc7f0e..572c32e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala @@ -1,10 +1,9 @@ package cc.sukazyo.cono.morny.bot.query +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.InlineQueryResultArticle -import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds - import scala.language.postfixOps import scala.util.matching.Regex @@ -25,11 +24,11 @@ object ShareToolTwitter extends ITelegramQuery { case REGEX_TWEET_LINK(_1, _2, _) => List( InlineQueryUnit(InlineQueryResultArticle( - inlineIds(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX, + inlineQueryId(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX, s"https://vxtwitter.com/$_2" )), InlineQueryUnit(InlineQueryResultArticle( - inlineIds(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED, + inlineQueryId(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED, s"https://c.vxtwitter.com/$_2" )) ) diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala index cba4ccf..9591fb1 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala @@ -1,16 +1,16 @@ package cc.sukazyo.cono.morny.daemon import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig} -import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException -import com.pengrad.telegrambot.request.{BaseRequest, SendMessage} -import com.pengrad.telegrambot.response.BaseResponse import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.bot.command.MornyInformation +import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import com.google.gson.GsonBuilder import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.model.User -import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h -import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString +import com.pengrad.telegrambot.request.{BaseRequest, SendMessage} +import com.pengrad.telegrambot.response.BaseResponse object MornyReport { @@ -25,7 +25,7 @@ object MornyReport { s"""cannot execute report to telegram: |${exceptionLog(e) indent 4} | tg-api response: - |${(e.getResponse toString) indent 4}""" + |${(e.response toString) indent 4}""" .stripMargin } } @@ -36,7 +36,7 @@ object MornyReport { case api: EventRuntimeException.ActionFailed => // language=html "\n\ntg-api error:\n
      %s
      " - .formatted(GsonBuilder().setPrettyPrinting().create.toJson(api.getResponse)) + .formatted(GsonBuilder().setPrettyPrinting().create.toJson(api.response)) case _ => "" executeReport(SendMessage( MornyCoeur.config.reportToChat, @@ -55,7 +55,7 @@ object MornyReport { // language=html s"""▌User unauthenticated action |action: ${h(action)} - |by user ${(TGToString as user) fullnameRefHtml}""" + |by user ${user.fullnameRefHTML}""" .stripMargin ).parseMode(ParseMode HTML)) } @@ -105,7 +105,7 @@ object MornyReport { def onMornyExit (causedBy: AnyRef|Null): Unit = { if unsupported then return val causedTag = causedBy match - case u: User => (TGToString as u) fullnameRefHtml + case u: User => u.fullnameRefHTML case n if n == null => "UNKNOWN reason" case a: AnyRef => /*language=html*/ s"${h(a.toString)}" executeReport(SendMessage( diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala index 87f3364..d6ec709 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala @@ -10,8 +10,8 @@ object MornyJrrp { jrrp_v_xmomi(user.id, timestamp/(1000*60*60*24)) * 100.0 private def jrrp_v_xmomi (identifier: Long, dayStamp: Long): Double = - import cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex - import cc.sukazyo.cono.morny.util.CommonEncrypt.hashMd5 - (java.lang.Long parseLong byteArrayToHex(hashMd5(s"$identifier@$dayStamp")).substring(0, 4)) / (0xffff toDouble) + import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5 + import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + (java.lang.Long parseLong MD5(s"$identifier@$dayStamp").toHex.substring(0, 4)) / (0xffff toDouble) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala b/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala new file mode 100644 index 0000000..37a2473 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala @@ -0,0 +1,119 @@ +package cc.sukazyo.cono.morny.util + +import cc.sukazyo.cono.morny.util.UseMath.** + +import scala.collection.mutable + +/** Utils about $Bilibili + * + * contains utils: + * - av/BV converting: + * - [[toAv]] + * - [[toBv]] + * + * @define Bilibili [[https://bilibili.com Bilibili]] + * + * @define AvBvFormat + * === About AV/BV id format === + * the AV id is a number; the BV id is a special 10 digits base58 number, it shows as String + * in programming. + * + * e.g. while the link ''`https://www.bilibili.com/video/BV17x411w7KC/`'' shows + * the same with ''`https://www.bilibili.com/video/av170001/`'', the AV id + * is __`170001`__, the BV id is __`BV17x411w7KC`__. + * + * @define AvBvSeeAlso [[https://www.zhihu.com/question/381784377/answer/1099438784 mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」?]] + * @todo Maybe make a class `AV`/`BV` and implement the parse in the class + */ +object BiliTool { + + private val V_CONV_XOR = 177451812L + private val V_CONV_ADD = 8728348608L + + private val BV_TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF" + private val TABLE_INT = BV_TABLE.length + private val BV_TABLE_REVERSED = + val mapping = mutable.HashMap.empty[Char, Int] + for (i <- BV_TABLE.indices) mapping += (BV_TABLE(i) -> i) + mapping.toMap + private val BV_TEMPLATE = "1 4 1 7 " + private val BV_TEMPLATE_FILTER = Array(9, 8, 1, 6, 2, 4) + + /** Error of illegal BV id. + * + * @constructor Build a error with illegal BV details. + * @param bv the source illegal BV id. + * @param reason why it is illegal. + */ + class IllegalFormatException private (bv: String, reason: String) + extends RuntimeException (s"`$bv is not a valid 10 digits base58 BV id: $reason`") { + + /** Error of illegal BV id, where the reason is the BV id is not 10 digits. + * + * @param bv the source of illegal BV id. + * @param length the length of the illegal BV id. + */ + def this (bv: String, length: Int) = + this(bv, s"given length is $length") + + /** Error of illegal BV id, where the reason is the BV id contains non [[BV_TABLE base58 character]]. + * + * @param bv the source of illegal BV id. + * @param c the illegal character + * @param location the index of the illegal character in the illegal BV id. + */ + def this (bv: String, c: Char, location: Int) = + this(bv, s"char `$c` is not in base58 char table (in position $location)") + } + + /** Convert an AV video id format to BV video id format for $Bilibili + * + * $AvBvFormat + * + * this method '''available while the __av-id < 2^27^__''', while it theoretically + * available when the av-id < 2^30^. Meanwhile some digits of the BV id is a fixed + * value (like the [[BV_TEMPLATE]] shows) -- input __bv__ can do not follow the format, + * but it will almost certainly gives a wrong AV id (because the fixed number is not + * processed at all!) + * + * @see $AvBvSeeAlso + * + * @param bv a BV id, which should be exactly 10 digits and all chars should be + * a legal base58 char (which means can be found in [[BV_TABLE]]). + * otherwise, an [[IllegalFormatException]] will be thrown. + * @return an AV id which will shows the save video of input __bv__ in $Bilibili + * @throws IllegalFormatException when the input __bv__ is not a legal 10 digits base58 + * formatted BV id. + */ + @throws[IllegalFormatException] + def toAv (bv: String): Long = { + var av = 0L + if (bv.length != 10) throw IllegalFormatException(bv, bv.length) + for (i <- BV_TEMPLATE_FILTER.indices) { + val _get = BV_TEMPLATE_FILTER(i) + val tableToken = BV_TABLE_REVERSED get bv(_get) + if tableToken isEmpty then throw IllegalFormatException(bv, bv(_get), _get) + av = av + (tableToken.get * (TABLE_INT**i).toLong) + } + (av - V_CONV_ADD) ^ V_CONV_XOR + } + + /** Convert an AV video format to a BV video format for $Bilibili. + * + * this method '''available while the __av-id < 2^27^__''', while it theoretically + * available when the av-id < 2^30^. + * + * @param av an AV id. + * @return a BV id which will shows the save video of input __av__ in $Bilibili + */ + def toBv (av: Long): String = { + val _av = (av^V_CONV_XOR)+V_CONV_ADD + val bv = Array(BV_TEMPLATE:_*) + for (i <- BV_TEMPLATE_FILTER.indices) { + import Math.{floor, pow} + bv(BV_TEMPLATE_FILTER(i)) = BV_TABLE( (floor(_av/(TABLE_INT**i)) % TABLE_INT) toInt ) + } + String copyValueOf bv + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/CommonEncrypt.scala b/src/main/scala/cc/sukazyo/cono/morny/util/CommonEncrypt.scala new file mode 100644 index 0000000..740e048 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/CommonEncrypt.scala @@ -0,0 +1,98 @@ +package cc.sukazyo.cono.morny.util + +import java.nio.charset.{Charset, StandardCharsets} +import java.security.{MessageDigest, NoSuchAlgorithmException} + +/** Provides some re-encapsulated algorithm function, and some standard values in encrypting, + * and some normalized utils in processing something in encrypting. + * + * currently there's: + * - standard value: + * - [[ENCRYPT_STANDARD_CHARSET]] the standard [[Charset]] to parse between [[String]] + * and [[Bin]] in encrypting. + * - algorithm encapsulations: + * - [[MD5]] (MD5 Message-Digest Algorithm) + * - [[SHA1]] (Secure Hash Algorithm 1) + * - [[SHA256]] (Secure Hash Algorithm 2: 256bit) + * - [[SHA512]] (Secure Hash Algorithm 2: 512bit) + * - normalized utils + * - [[lint_base64FileName]] remove the .base64 file-extension for base64 text file + * + * @define WhenString2Bin + * [[String]] will encoded to [[Bin]] using [[Charset]] [[ENCRYPT_STANDARD_CHARSET]] + * + * @todo some tests + */ +object CommonEncrypt { + + /** the [[Charset]] should use when converting between [[String]] + * and [[Bin]] in encrypting */ + val ENCRYPT_STANDARD_CHARSET: Charset = StandardCharsets.UTF_8 + + /** the alias of [[Array]]`[`[[Byte]]`]`. + * means the binary data. + */ + //noinspection ScalaWeakerAccess + type Bin = Array[Byte] + + private def hash (data: Bin)(using algorithm: String): Bin = + try { + MessageDigest.getInstance(algorithm) digest data + } catch case n: NoSuchAlgorithmException => + throw IllegalStateException(n) + + /** the [[https://en.wikipedia.org/wiki/MD5 MD5]] hash value of input [[Bin]] `data`. */ + def MD5(data: Bin): Bin = hash(data)(using "md5") + /** the [[https://en.wikipedia.org/wiki/MD5 MD5]] hash value of input [[String]] `data`. + * + * $WhenString2Bin + */ + def MD5 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "md5") + + /** the [[https://en.wikipedia.org/wiki/SHA-1 SHA-1]] hash value of input [[Bin]] `data`. */ + def SHA1 (data: Bin): Bin = hash(data)(using "sha1") + /** the [[https://en.wikipedia.org/wiki/SHA-1 SHA-1]] hash value of input [[String]] `data`. + * + * $WhenString2Bin + */ + def SHA1 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "sha1") + + /** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/256]] hash value of input [[Bin]] `data`. */ + def SHA256 (data: Bin): Bin = hash(data)(using "sha256") + /** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/256]] hash value of input [[String]] `data`. + * + * $WhenString2Bin + */ + def SHA256 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "sha256") + + /** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/512]] hash value of input [[Bin]] `data`. */ + def SHA512 (data: Bin): Bin = hash(data)(using "sha512") + /** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/512]] hash value of input [[String]] `data`. + * + * $WhenString2Bin + */ + def SHA512 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "sha512") + + /** Try get the filename before it got encrypted. + * + * It assumes the base64 encrypted file should keep the original file, and plus + * a file-extension shows the file is base64 encrypted. + * + * Actually, the file will try find the following file-extension and drop it: + * - `.b64` + * - `.64.txt` + * - `.base64` + * - `.base64.txt` + * if none of those found, it will do no process anymore. + * + * @param encrypted the file fullname (means filename with file-extension) of base64 encrypted file. + * @return the file fullname removed the base64 file extension. + */ + def lint_base64FileName (encrypted: String): String = encrypted match + case i if i endsWith ".b64" => i dropRight ".b64".length + case ix if ix endsWith ".b64.txt" => ix dropRight ".b64.txt".length + case l if l endsWith ".base64" => l dropRight ".base64".length + case lx if lx endsWith ".base64.txt" => lx dropRight ".base64.txt".length + case u => u + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala b/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala new file mode 100644 index 0000000..0ab5d36 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala @@ -0,0 +1,76 @@ +package cc.sukazyo.cono.morny.util + +import java.time.{Instant, LocalDateTime, ZoneId, ZoneOffset} +import java.time.format.DateTimeFormatter + +/** Some formatting (convert some data to some standard output type) + * methods normalized based on Morny's usage + * + * contains: + * - [[DATE_TIME_PATTERN_FULL_MILLIS]] the standard date-time-millis [[String]] pattern format + * - [[formatDate]] convert UTC time millis (and hour-offset time zone) + * to normalized date-time-millis [[String]] + * - [[formatDuration]] convert millis duration to normalized duration [[String]] + * + */ +object CommonFormat { + + /** the standard date-time-millis [[String]] pattern format that Morny in use. + * + * pattern string is pattern of [[DateTimeFormatter]]. + */ + //noinspection ScalaWeakerAccess + val DATE_TIME_PATTERN_FULL_MILLIS = "yyyy-MM-dd HH:mm:ss:SSS" + + /** the formatted date-time-millis [[String]]. + * + * time is formatted by pattern [[DATE_TIME_PATTERN_FULL_MILLIS]]. + * + * @param timestamp millis timestamp. timestamp should be UTC alignment. + * + * @param utcOffset the hour offset of the time zone, the time-zone controls + * which local time describe will use. + * + * for example, timestamp [[0]] describes 1970-1-1 00:00:00 in + * UTC+0, so, use the `timestamp` `0` and `utfOffset` `0` will + * returns `"1970-1-1 00:00:00:000"`; however, at the same time, + * in UTC+8, the local time is 1970-1-1 08:00:00:000, so use + * the `timestamp` `0` and the `utcOffset` `8` will returns + * `"1970-1-1 08:00:00:000"` + * + * @return the time-zone local date-time-millis [[String]] describes the timestamp. + */ + def formatDate (timestamp: Long, utcOffset: Int): String = + DateTimeFormatter.ofPattern(DATE_TIME_PATTERN_FULL_MILLIS).format( + LocalDateTime.ofInstant( + Instant.ofEpochMilli(timestamp), + ZoneId.ofOffset("UTC", ZoneOffset.ofHours(utcOffset)) + ) + ) + + /** human readable [[String]] that describes the millis duration. + * + * {{{ + * scala> formatDuration(10) + * val res0: String = 10ms + * + * scala> formatDuration(3000001) + * val res1: String = 50min 0s 1ms + * + * scala> formatDuration(94179047901720L) + * val res2: String = 1090035d 6h 38min 21s 720ms + * }}} + * + * @param duration time duration, in milliseconds + * @return time duration, human readable + */ + def formatDuration (duration: Long): String = + val sb = new StringBuilder() + if (duration > 1000 * 60 * 60 * 24) sb ++= (duration / (1000 * 60 * 60 * 24)).toString ++= "d " + if (duration > 1000 * 60 * 60) sb ++= (duration / (1000 * 60 * 60) % 24).toString ++= "h " + if (duration > 1000 * 60) sb ++= (duration / (1000 * 60) % 60).toString ++= "min " + if (duration > 1000) sb ++= (duration / 1000 % 60).toString ++= "s " + sb ++= (duration % 1000).toString ++= "ms" + sb toString + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala b/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala new file mode 100644 index 0000000..0d469d4 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/ConvertByteHex.scala @@ -0,0 +1,55 @@ +package cc.sukazyo.cono.morny.util + +/** Added the [[toHex]] method to [[Byte]] and [[Array]]`[`[[Byte]]`]`. + * + * the [[toHex]] method will takes [[Byte]] as a binary byte and convert + * it to the hex [[String]] that can describe the binary byte. there are + * always 2 digits unsigned hex number. + * + * for example, byte `0` is binary `0000 0000`, it will be converted to + * `"00"`, and the byte `-1` is binary `1111 1111` which corresponding + * `"ff"`. + * {{{ + * scala> 0.toByte.toHex + * val res6: String = 00 + * + * scala> 15.toByte.toHex + * val res10: String = 0f + * + * scala> -1.toByte.toHex + * val res7: String = ff + * }}} + * + * while converting byte array, the order is: the 1st element of the array + * will be put most forward, then the following added to the tail of hex string. + * {{{ + * scala> Array[Byte](0, 1, 2, 3).toHex + * val res5: String = 00010203 + * }}} + * + */ +object ConvertByteHex { + + extension (b: Byte) { + + /** convert the binary of the [[Byte]] contains to hex string. + * @see [[ConvertByteHex]] + */ + def toHex: String = (b >> 4 & 0xf).toHexString + (b & 0xf).toHexString + + } + + extension (data: Array[Byte]) { + + /** convert the binary of the [[Array]]`[`[[Byte]]`]` contains to hex string. + * + * @see [[ConvertByteHex]] + */ + def toHex: String = + val sb = StringBuilder() + for (b <- data) sb ++= (b toHex) + sb toString + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/FileUtils.scala b/src/main/scala/cc/sukazyo/cono/morny/util/FileUtils.scala new file mode 100644 index 0000000..f1e146b --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/FileUtils.scala @@ -0,0 +1,28 @@ +package cc.sukazyo.cono.morny.util + +import java.io.{FileInputStream, IOException} +import java.security.{MessageDigest, NoSuchAlgorithmException} +import scala.util.Using + +/** + * @todo docs + * @todo some tests? + */ +object FileUtils { + + @throws[IOException|NoSuchAlgorithmException] + def getMD5Three (path: String): String = { + val buffer = Array.ofDim[Byte](8192) + var len = 0 + val algo = MessageDigest.getInstance("MD5") + Using (FileInputStream(path)) { stream => + len = stream.read(buffer) + while (len != -1) + algo update (buffer, 0, len) + len = stream.read(buffer) + } + import ConvertByteHex.toHex + algo.digest toHex + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/OkHttpPublic.scala b/src/main/scala/cc/sukazyo/cono/morny/util/OkHttpPublic.scala new file mode 100644 index 0000000..fd7d19e --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/OkHttpPublic.scala @@ -0,0 +1,13 @@ +package cc.sukazyo.cono.morny.util + +import okhttp3.MediaType + +/** some public values of [[okhttp3]] */ +object OkHttpPublic { + + /** predefined [[okhttp3]] [[MediaType]]s */ + object MediaTypes: + /** [[MediaType]] of [[https://en.wikipedia.org/wiki/JSON JSON]]. using encoding ''UTF-8'' */ + val JSON: MediaType = MediaType.get("application/json; charset=utf-8") + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala new file mode 100644 index 0000000..65a1e87 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala @@ -0,0 +1,75 @@ +package cc.sukazyo.cono.morny.util + +import scala.collection.mutable.ArrayBuffer +import scala.util.boundary + +/** + * @todo docs + * @todo maybe there can have some encapsulation + */ +object UniversalCommand { + + def apply (input: String): Array[String] = { + + val builder = ArrayBuffer.empty[String] + + extension (c: Char) { + private inline def isUnsupported: Boolean = + (c == '\n') || (c == '\r') + private inline def isSeparator: Boolean = + c == ' ' + private inline def isQuote: Boolean = + (c == '\'') || (c == '"') + private inline def isEscapeChar: Boolean = + c == '\\' + private inline def escapableInQuote: Boolean = + c.isQuote || c.isEscapeChar + private inline def escapable: Boolean = + c.escapableInQuote || c.isSeparator + } + + var arg = StringBuilder() + var i = 0 + while (i < input.length) { + if (input(i) isSeparator) { + if (arg nonEmpty) builder += arg.toString + arg = arg.empty + } else if (input(i) isQuote) { + val _inside_tag = input(i) + var _inside = true + boundary { while (_inside) { + i=i+1 + if (i >= input.length) throw IllegalArgumentException("UniversalCommand: unclosed quoted text") + if (input(i) == _inside_tag) + boundary.break() + else if (input(i) isUnsupported) + throw IllegalArgumentException("UniversalCommand: unsupported new-line") + else if (input(i) isQuote) + throw IllegalArgumentException("UniversalCommand: mixed \" and ' used") + else if (input(i) isEscapeChar) + if (i+1 >= input.length) throw IllegalArgumentException("UniversalCommand: \\ in the end") + if (input(i+1) escapableInQuote) + i=i+1 + arg += input(i) + else + arg += input(i) + }} + } else if (input(i) isUnsupported) { + throw IllegalArgumentException("UniversalCommand: unsupported new-line") + } else if (input(i) isEscapeChar) { + if (i + 1 >= input.length) throw IllegalArgumentException("UniversalCommand: \\ in the end") + if (input(i+1) escapable) + i=i+1 + arg += input(i) + } else { + arg += input(i) + } + i = i + 1 + } + if (arg nonEmpty) builder += arg.toString + + builder toArray + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala new file mode 100644 index 0000000..5f11c38 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala @@ -0,0 +1,19 @@ +package cc.sukazyo.cono.morny.util + +import scala.annotation.targetName + +/** @todo some tests */ +object UseMath { + + extension (self: Int) { + + def over (other: Int): Double = self.toDouble / other + + } + + extension (self: Int) { + @targetName("pow") + def ** (other: Int): Double = Math.pow(self, other) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UseRandom.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UseRandom.scala new file mode 100644 index 0000000..b6eb096 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UseRandom.scala @@ -0,0 +1,33 @@ +package cc.sukazyo.cono.morny.util + +import scala.language.implicitConversions +import scala.util.Random + +/** + * @todo some tests maybe? + * @todo use the using clauses to provide random instance + */ +object UseRandom { + + class ChancePossibility[T <: Any] (val one: T) (using possibility: Double) { + def nor[U] (another: U): T|U = + if Random.nextDouble < possibility then one else another + } + + given Conversion[ChancePossibility[Boolean], Boolean] with + def apply(in: ChancePossibility[Boolean]): Boolean = in nor !in.one + + extension (num: Double) { + + def chance_is[T <: Any] (one: T): ChancePossibility[T] = + ChancePossibility(one)(using num) + + } + + def rand_half: Boolean = Random.nextBoolean + + def rand_id: String = + import ConvertByteHex.toHex + Random nextBytes 6 toHex + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/package.scala b/src/main/scala/cc/sukazyo/cono/morny/util/package.scala new file mode 100644 index 0000000..43bf99c --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/package.scala @@ -0,0 +1,22 @@ +package cc.sukazyo.cono.morny + +/** Utils that [[cc.sukazyo.cono.morny]]'s code used. + * + * contains: + * - [[tgapi Telegram API/Utils Extras]] + * - extensions of language standard + * - [[CommonEncrypt]] re-encapsulated some encrypt algorithms, and some normalized while encrypting. + * - [[CommonFormat]] provides some format methods normalized based on Morny usage standard. + * - [[ConvertByteHex]] extensions [[Byte]] and so on, make it easier converting binary data in it to a hex string. + * - [[UseMath]] scala style to make Math function easier to use + * - [[UseRandom]] scala style to use Random to generate something + * - external library extras + * - [[OkHttpPublic]] defines some static value for [[okhttp3]] + * - useful misc utils + * - [[FileUtils]] contains some easy-to-use file action. + * - [[UniversalCommand]] provides a easy way to get an args array from a string input. + * - others + * - [[BiliTool about Bilibili]] + * + */ +package object util {} diff --git a/src/main/java/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java similarity index 100% rename from src/main/java/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java rename to src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala new file mode 100644 index 0000000..f6c7e26 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala @@ -0,0 +1,31 @@ +package cc.sukazyo.cono.morny.util.tgapi + +import cc.sukazyo.cono.morny.util.UniversalCommand + +class InputCommand private ( + val target: String|Null, + val command: String, + val args: Array[String] +) { + + override def toString: String = + s"{{$command}@{$target}#{${args.mkString}}" + +} + +object InputCommand { + + def apply (input: Array[String]): InputCommand = { + val _ex = input(0) split ("@", 2) + val _args = input drop 1 + new InputCommand( + if _ex.length == 1 then null else _ex(1), + _ex(0), + _args + ) + } + + def apply (input: String): InputCommand = + InputCommand(UniversalCommand(input)) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/Standardize.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/Standardize.scala new file mode 100644 index 0000000..2796c02 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/Standardize.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny.util.tgapi + +object Standardize { + + val CHANNEL_SPEAKER_MAGIC_ID = 136817688 + + val MASK_BOTAPI_ID: Long = -1000000000000 + +} \ No newline at end of file diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.scala new file mode 100644 index 0000000..b44e7be --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/event/EventRuntimeException.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny.util.tgapi.event + +import com.pengrad.telegrambot.response.BaseResponse + +class EventRuntimeException (message: String) extends RuntimeException(message) + +object EventRuntimeException { + class ActionFailed (message: String, val response: BaseResponse) extends EventRuntimeException(message) +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/NamingUtils.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/NamingUtils.scala new file mode 100644 index 0000000..c0bc94b --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/NamingUtils.scala @@ -0,0 +1,11 @@ +package cc.sukazyo.cono.morny.util.tgapi.formatting + +import cc.sukazyo.cono.morny.util.CommonEncrypt +import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + +object NamingUtils { + + def inlineQueryId (tag: String, taggedData: String = ""): String = + CommonEncrypt.MD5(tag+taggedData) toHex + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala new file mode 100644 index 0000000..acc0d49 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala @@ -0,0 +1,82 @@ +package cc.sukazyo.cono.morny.util.tgapi.formatting + +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h +import cc.sukazyo.cono.morny.util.tgapi.Standardize.MASK_BOTAPI_ID +import com.pengrad.telegrambot.model.{Chat, Message, User} +import com.pengrad.telegrambot.model.Chat.Type + +object TelegramFormatter { + + extension (chat: Chat) { + + def safe_name: String = chat.`type` match + case Type.Private => _connectName(chat.firstName, chat.lastName) + case _ => chat.title + + def safe_linkHTML: String = + if (chat.username == null) + chat.`type` match + // language=html + case Type.Private => s"@[u:${chat.id}]" + // language=html + case _ => s"@[c/${chat.id}]" + else s"@${h(chat.username)}" + + def safe_firstnameRefHTML: String = + chat.`type` match + // language=html + case Type.Private => s"${h(chat.firstName)}" + // language=html + case _ => s"${h(chat.title)}" + + def id_tdLib: Long = + if chat.id < 0 then (chat.id - MASK_BOTAPI_ID)abs else chat.id + + def typeTag: String = chat.`type` match + case Type.Private => "🔒" + case Type.group => "💭" + case Type.supergroup => "💬" + case Type.channel => "📢" + + } + + extension (user: User) { + + def fullname: String = _connectName(user.firstName, user.lastName) + + def fullnameRefHTML: String = + // language=html + s"${h(user.fullname)}" + + def firstnameRefHTML: String = + // language=html + s"${h(user.firstName)}" + + def toLogTag: String = + (if (user.username == null) user.fullname + " " else "@" + user.username) + + "[" + user.id + "]" + + } + + extension (m: Message) { + + def sender_id: Long = + if m.senderChat == null then m.from.id else m.senderChat.id + + def sender_firstnameRefHTML: String = + if (m.senderChat == null) + m.from.firstnameRefHTML + else m.senderChat.safe_firstnameRefHTML + + } + + private inline def _link_user (id: Long): String = + s"tg://user?id=$id" + + private inline def _link_chat (id: Long): String = + s"https://t.me/c/$id" + + private inline def _connectName (firstName: String, lastName: String): String = + firstName + (if lastName == null then "" else " " + lastName) + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramParseEscape.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramParseEscape.scala new file mode 100644 index 0000000..4b27795 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramParseEscape.scala @@ -0,0 +1,12 @@ +package cc.sukazyo.cono.morny.util.tgapi.formatting + +object TelegramParseEscape { + + def escapeHtml (input: String): String = + var process = input + process = process.replaceAll("&", "&") + process = process.replaceAll("<", "<") + process = process.replaceAll(">", ">") + process + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala new file mode 100644 index 0000000..81d87ee --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala @@ -0,0 +1,67 @@ +package cc.sukazyo.cono.morny.util.tgapi.formatting + +import com.pengrad.telegrambot.model.User +import okhttp3.{OkHttpClient, Request} + +import java.io.IOException +import scala.util.matching.Regex +import scala.util.Using + +object TelegramUserInformation { + + val DC_QUERY_SOURCE_SITE = "https://t.me/" + val DC_QUERY_PROCESSOR_REGEX: Regex = "(cdn[1-9]).tele(sco.pe|gram-cdn.org)"r + + private val httpClient = OkHttpClient() + + @throws[IllegalArgumentException|IOException] + def getDataCenterFromUser (username: String): String = { + val request = Request.Builder().url(DC_QUERY_SOURCE_SITE + username).build + Using (httpClient.newCall(request) execute) { response => + val body = response.body + if body eq null then "" + else DC_QUERY_PROCESSOR_REGEX.findFirstMatchIn(body.string) match + case Some(res) => res.group(1) + case None => "" + } get + } + + def getFormattedInformation (user: User): String = { + import TelegramParseEscape.escapeHtml as h + + val userInfo = StringBuilder() + + userInfo ++= // language=html + s"""userid : + |- ${user.id}""" + .stripMargin + userInfo ++= { + if (user.username eq null) // language=html + s""" + |username : null + |datacenter : not supported""" + .stripMargin + else // language=html + s""" + |username : + |- ${h(user.username)} + |datacenter : + |- ${h(getDataCenterFromUser(user.username))}""" + .stripMargin + } + userInfo ++= // language=html + s""" + |display name : + |- ${h(user.firstName)}${if user.lastName ne null then s"\n- ${h(user.lastName)}" else ""}""" + .stripMargin + if (user.languageCode ne null) userInfo ++= // language=html + s""" + |language-code : + |- ${user.languageCode}""" + .stripMargin + + userInfo toString + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/package.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/package.scala new file mode 100644 index 0000000..d646076 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/package.scala @@ -0,0 +1,4 @@ +package cc.sukazyo.cono.morny.util + +/** @todo docs */ +package object tgapi {} diff --git a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java deleted file mode 100644 index 4a36d44..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java +++ /dev/null @@ -1,18 +0,0 @@ -package cc.sukazyo.cono.morny; - -import cc.sukazyo.cono.morny.util.UniversalCommand; - -import java.util.*; - -public class MornyCLI { - - public static void main (String[] args) { - - System.out.print("$ java -jar morny-coeur-"+MornySystem.VERSION_FULL()+".jar " ); - String x; - try (Scanner line = new Scanner(System.in)) { x = line.nextLine(); } - ServerMain.main(UniversalCommand.format(x)); - - } - -} diff --git a/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java b/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java deleted file mode 100644 index d686ea0..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/daemon/TestMedicationTimer.java +++ /dev/null @@ -1,36 +0,0 @@ -package cc.sukazyo.cono.morny.daemon; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.Set; - -import static cc.sukazyo.cono.morny.internal.ScalaJavaConv.jSetInteger2simm; - -public class TestMedicationTimer { - - @ParameterizedTest - @CsvSource(textBlock = """ - 2022-11-13T13:14:35.000+08, +08, 2022-11-13T19:00:00+08 - 2022-11-13T13:14:35.174+02, +02, 2022-11-13T19:00:00+02 - 1998-02-01T08:14:35.871+08, +08, 1998-02-01T19:00:00+08 - 2022-11-13T00:00:00.000-01, -01, 2022-11-13T07:00:00-01 - 2022-11-21T19:00:00.000+00, +00, 2022-11-21T21:00:00+00 - 2022-12-31T21:00:00.000+00, +00, 2023-01-01T07:00:00+00 - 2125-11-18T23:45:27.062+00, +00, 2125-11-19T07:00:00+00 - """) - void testCalcNextRoutineTimestamp (ZonedDateTime base, ZoneOffset zoneHour, ZonedDateTime expected) - throws IllegalArgumentException { - final Set at = Set.of(7, 19, 21); - System.out.println("base.toInstant().toEpochMilli() = " + base.toInstant().toEpochMilli()); - Assertions.assertEquals( - expected.toInstant().toEpochMilli(), - MedicationTimer.calcNextRoutineTimestamp(base.toInstant().toEpochMilli(), zoneHour, jSetInteger2simm(at)) - ); - System.out.println(" ok"); - } - -} diff --git a/src/test/java/cc/sukazyo/cono/morny/util/TestBiliTool.java b/src/test/java/cc/sukazyo/cono/morny/util/TestBiliTool.java deleted file mode 100644 index 125d9c6..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/util/TestBiliTool.java +++ /dev/null @@ -1,30 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import static cc.sukazyo.cono.morny.util.BiliTool.*; - -public class TestBiliTool { - - private static final String AV_BV_DATA_CSV = """ - 17x411w7KC, 170001 - 1Q541167Qg, 455017605 - 1mK4y1C7Bz, 882584971 - 1T24y197V2, 688730800 - """; - - @ParameterizedTest - @CsvSource(textBlock = AV_BV_DATA_CSV) - void testAvToBv (String bv, int av) { - Assertions.assertEquals(bv, toBv(av)); - } - - @ParameterizedTest - @CsvSource(textBlock = AV_BV_DATA_CSV) - void testBvToAv (String bv, int av) { - Assertions.assertEquals(av, toAv(bv)); - } - -} diff --git a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java b/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java deleted file mode 100644 index d35e55f..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonConvert.java +++ /dev/null @@ -1,47 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.EnumSource; - -import static cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex; -import static cc.sukazyo.cono.morny.util.CommonConvert.byteToHex; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class TestCommonConvert { - - @ParameterizedTest - @CsvSource(textBlock = """ - 0x00, 00 - 0x01, 01 - 0x20, 20 - 0x77, 77 - -0x60, a0 - 0x0a, 0a - -0x01, ff - -0x05, fb - """ - ) - void testByteToHex(byte source, String expected) { - assertEquals(expected, byteToHex(source)); - } - - public enum TestByteArrayToHexSource { - $1(new T(new byte[]{0x00}, "00")), - $2(new T(new byte[]{(byte)0xff}, "ff")), - $3(new T(new byte[]{(byte)0xc3}, "c3")), - $4(new T(new byte[]{}, "")), - $5(new T(new byte[]{0x30,0x0a,0x00,0x04,(byte)0xb0,0x00}, "300a0004b000")), - $6(new T(new byte[]{0x00,0x00,0x0a,(byte)0xff,(byte)0xfc,(byte)0xab,(byte)0x00,0x04}, "00000afffcab0004")), - $7(new T(new byte[]{0x00,0x7c,0x11,0x28,(byte)0x88,(byte)0xa6,(byte)0xfc,0x30}, "007c112888a6fc30")); - public record T (byte[] raw, String expected) {} - public final T value; - TestByteArrayToHexSource (T value) { this.value = value; } - } - @ParameterizedTest - @EnumSource - void testByteArrayToHex (TestByteArrayToHexSource source) { - assertEquals(source.value.expected, byteArrayToHex(source.value.raw)); - } - -} diff --git a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonEncrypt.java b/src/test/java/cc/sukazyo/cono/morny/util/TestCommonEncrypt.java deleted file mode 100644 index 429b163..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonEncrypt.java +++ /dev/null @@ -1,23 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import static cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex; -import static cc.sukazyo.cono.morny.util.CommonEncrypt.*; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class TestCommonEncrypt { - - @ParameterizedTest - @SuppressWarnings("UnnecessaryStringEscape") - @CsvSource(textBlock = """ - 28be57d368b75051da76c068a6733284, '莲子' - 9644c5cbae223013228cd528817ba4f5, '莲子\n' - d41d8cd98f00b204e9800998ecf8427e, '' - """) - void testHashMd5_String (String md5, String text) { - assertEquals(md5, byteArrayToHex(hashMd5(text))); - } - -} diff --git a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonFormat.java b/src/test/java/cc/sukazyo/cono/morny/util/TestCommonFormat.java deleted file mode 100644 index d7b67fe..0000000 --- a/src/test/java/cc/sukazyo/cono/morny/util/TestCommonFormat.java +++ /dev/null @@ -1,36 +0,0 @@ -package cc.sukazyo.cono.morny.util; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import static cc.sukazyo.cono.morny.util.CommonFormat.*; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class TestCommonFormat { - - @ParameterizedTest - @CsvSource(textBlock = """ - 1664646870402, 8, 2022-10-02 01:54:30:402 - 1, 8, 1970-01-01 08:00:00:001 - 0, -1, 1969-12-31 23:00:00:000 - """ - ) - void testFormatDate (long timestamp, int utfOffset, String expectedHumanReadableTime) { - assertEquals(expectedHumanReadableTime, formatDate(timestamp, utfOffset)); - } - - @ParameterizedTest - @CsvSource(textBlock = """ - 100, '100ms' - 3000, '3s 0ms' - 326117522, '3d 18h 35min 17s 522ms' - 53373805, 14h 49min 33s 805ms - """) -// -1, '-1ms' // WARN: maybe sometime an unexpected usage -// -194271974291, '-291ms' // -// """) // - void testFormatDuration (long durationMillis, String humanReadableDuration) { - assertEquals(humanReadableDuration, formatDuration(durationMillis)); - } - -} diff --git a/src/test/scala/cc/sukazyo/cono/morny/MornyCLI.scala b/src/test/scala/cc/sukazyo/cono/morny/MornyCLI.scala new file mode 100644 index 0000000..a5c01f8 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/MornyCLI.scala @@ -0,0 +1,12 @@ +package cc.sukazyo.cono.morny + +import cc.sukazyo.cono.morny.util.UniversalCommand + +import scala.io.StdIn + +@main def MornyCLI (): Unit = { + + print("$ java -jar morny-coeur-\"+MornySystem.VERSION_FULL+\".jar ") + ServerMain main UniversalCommand(StdIn readLine) + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala b/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala new file mode 100644 index 0000000..8d62c60 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala @@ -0,0 +1,10 @@ +package cc.sukazyo.cono.morny.test + +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should + +abstract class MornyTests extends AnyFreeSpec with should.Matchers { + + val pending_val = "[not-implemented]" + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala new file mode 100644 index 0000000..1ae8264 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala @@ -0,0 +1,68 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.prop.TableDrivenPropertyChecks + +import scala.util.Random + +class BiliToolTest extends MornyTests with TableDrivenPropertyChecks { + + private val examples = Table( + ("bv", "av"), + ("17x411w7KC", 170001L), + ("1Q541167Qg", 455017605L), + ("1mK4y1C7Bz", 882584971L), + ("1T24y197V2", 688730800L), + ) + + forAll (examples) { (bv, av) => s"while using av$av/BV$bv :" - { + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, toBv} + "av to bv works" in { toBv(av) shouldEqual bv } + "bv to av works" in { toAv(bv) shouldEqual av } + }} + + "BV with unsupported length :" - { + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalFormatException} + val examples = Table( + "bv", + "12345", + "12345678", + "123456789", +// "1234567890", length 10 which is supported + "1234567890a", + "1234567890ab", + "1234567890abcdef" + ) + forAll(examples) { bv => + s"length ${bv.length} should throws IllegalFormatException" in: + an [IllegalFormatException] should be thrownBy toAv(bv) + } + } + + "BV with special character :" - { + val examples = Table( + ("bv" , "contains_special"), + ("1mK4O1C7Bz", "O"), + ("1m04m1C7Bz", "0"), + ("1mK4O1I7Bz", "I"), + ("1mK4O1C7Bl", "l"), + ("1--4O1C7Bl", "[symbols]") + ) + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalFormatException} + forAll(examples) { (bv, with_sp) => + s"'$with_sp' should throws IllegalFormatException" in: + an [IllegalFormatException] should be thrownBy toAv(bv) + } + } + + "av/bv converting should be reversible" in { + for (_ <- 1 to 20) { + val rand_av = Random.between(0, 999999999L) + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, toBv} + val my_bv = toBv(rand_av) + toAv(my_bv) shouldEqual rand_av + toBv(toAv(my_bv)) shouldEqual my_bv + } + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonEncryptTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonEncryptTest.scala new file mode 100644 index 0000000..8557661 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonEncryptTest.scala @@ -0,0 +1,33 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.prop.TableDrivenPropertyChecks + +class CommonEncryptTest extends MornyTests with TableDrivenPropertyChecks { + + "while doing hash :" - { + + val examples = Table( + ("md5" , "text"), + ("28be57d368b75051da76c068a6733284", "莲子"), + ("9644c5cbae223013228cd528817ba4f5", "莲子\n"), + ("d41d8cd98f00b204e9800998ecf8427e", "") + ) + + import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5 + import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + forAll (examples) { (md5, text) => + s"while hashing text \"$text\" :" - { + + s"the MD5 value should be $md5" in { MD5(text).toHex shouldEqual md5 } + + "other algorithms" in pending + + } + } + + s"while hashing binary file $pending_val" in pending + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonFormatTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonFormatTest.scala new file mode 100644 index 0000000..33dac4f --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/CommonFormatTest.scala @@ -0,0 +1,46 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} +import org.scalatest.prop.TableDrivenPropertyChecks + +class CommonFormatTest extends MornyTests with TableDrivenPropertyChecks { + + "while using #formatDate :" - { + + val examples = Table( + ("time_text" , "timestamp", "zone_offset"), + ("2022-10-02 01:54:30:402", 1664646870402L, 8), + ("1970-01-01 08:00:00:001", 1L, 8), + ("1969-12-31 23:00:00:000", 0L, -1), + ) + + forAll(examples) { (time_text, timestamp, zone_offset) => + s"time $time_text in TimeZone($zone_offset) should be UTC timestamp $timestamp" in: + formatDate(timestamp, zone_offset) shouldEqual time_text + } + + } + + "while using #formatDuration :" - { + + val examples = Table( + ("time_millis", "duration_text"), + (100L , "100ms"), + (3000L , "3s 0ms"), + (326117522L , "3d 18h 35min 17s 522ms"), + (53373805L , "14h 49min 33s 805ms"), + (3600001L , "1h 0min 0s 1ms") + ) + + forAll(examples) { (time_millis, duration_text) => + + s"duration ($time_millis) millis should be formatted to '$duration_text'" in: + formatDuration(time_millis) shouldEqual duration_text + 0 should equal (0) + + } + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/ConvertByteHexTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/ConvertByteHexTest.scala new file mode 100644 index 0000000..25aea93 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/ConvertByteHexTest.scala @@ -0,0 +1,45 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.prop.TableDrivenPropertyChecks + +class ConvertByteHexTest extends MornyTests with TableDrivenPropertyChecks { + + private val examples_hex = Table( + ("byte" , "hex"), + ( 0x00 toByte, "00"), + ( 0x01 toByte, "01"), + ( 0x20 toByte, "20"), + ( 0x77 toByte, "77"), + (-0x60 toByte, "a0"), + ( 0x0a toByte, "0a"), + (-0x01 toByte, "ff"), + ( 0xfb toByte, "fb"), + ) + + "while using Byte#toHex :" - forAll (examples_hex) ((byte, hex) => { + s"byte ($byte) should be hex '$hex''" in { + import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + (byte toHex) shouldEqual hex + } + }) + + private val examples_hexs = Table( + ("bytes", "hex"), + (Array[Byte](0x00), "00"), + (Array[Byte](0xff toByte), "ff"), + (Array[Byte](0xc3 toByte), "c3"), + (Array[Byte](), ""), + (Array[Byte](0x30,0x0a,0x00,0x04,0xb0.toByte,0x00), "300a0004b000"), + (Array[Byte](0x00,0x00,0x0a,0xff.toByte,0xfc.toByte,0xab.toByte,0x00.toByte,0x04), "00000afffcab0004"), + (Array[Byte](0x00,0x7c,0x11,0x28,0x88.toByte,0xa6.toByte,0xfc.toByte,0x30), "007c112888a6fc30"), + ) + + "while using Array[Byte]#toHex :" - forAll(examples_hexs) ((bytes, hex) => { + s"byte array(${bytes mkString ","}) should be hex string $hex" in { + import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex + (bytes toHex) shouldEqual hex + } + }) + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/FileUtilsTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/FileUtilsTest.scala new file mode 100644 index 0000000..1873d12 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/FileUtilsTest.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests + +class FileUtilsTest extends MornyTests { + + "while getting the MD5 hash of a file :" in pending + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala new file mode 100644 index 0000000..28a11c5 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala @@ -0,0 +1,75 @@ +package cc.sukazyo.cono.morny.test.utils + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.matchers.should.Matchers +import org.scalatest.prop.TableDrivenPropertyChecks + +class UniversalCommandTest extends MornyTests with Matchers with TableDrivenPropertyChecks { + + "while formatting command from String :" - { + + import cc.sukazyo.cono.morny.util.UniversalCommand as Cmd + + raw"args should be separated by (\u0020) ascii-space" in: + Cmd("a b c delta e") shouldEqual Array("a", "b", "c", "delta", "e"); + "args should not be separated by non-ascii spaces" in: + Cmd("tests ダタ セト") shouldEqual Array("tests", "ダタ セト"); + "multiple ascii-spaces should not generate empty arg in middle" in: + Cmd("tests some of data") shouldEqual Array("tests", "some", "of", "data"); + + """texts and ascii-spaces in '' should grouped in one arg""" in: + Cmd("""tests 'data set'""") shouldEqual Array("tests", "data set"); + """texts and ascii-spaces in "" should grouped in one arg""" in : + Cmd("""tests "data set"""") shouldEqual Array("tests", "data set"); + """mixed ' and " should throws IllegalArgumentsException""" in: + an [IllegalArgumentException] should be thrownBy Cmd("""tests "data set' "of it'"""); + "with ' not closed should throws IllegalArgumentException" in: + an [IllegalArgumentException] should be thrownBy Cmd("""use 'it """); + + raw"\ should escape itself" in: + Cmd(raw"input \\data") shouldEqual Array("input", "\\data"); + raw"\ should escape ascii-space, makes it processed as a normal character" in: + Cmd(raw"input data\ set") shouldEqual Array("input", "data set"); + raw"\ should escape ascii-space, makes it can be an arg body" in: + Cmd(raw"input \ some-thing") shouldEqual Array("input", " ", "some-thing"); + raw"""\ should escape "", makes it processed as a normal character""" in : + Cmd(raw"""use \"inputted""") shouldEqual Array("use", "\"inputted"); + raw"\ should escape '', makes it processed as a normal character" in: + Cmd(raw"use \'inputted") shouldEqual Array("use", "'inputted"); + raw"\ should escape itself which inside a quoted scope" in: + Cmd(raw"use 'quoted \\ body'") shouldEqual Array("use", "quoted \\ body"); + raw"""\ should escape " which inside a "" scope""" in: + Cmd(raw"""in "quoted \" body" body""") shouldEqual Array("in", "quoted \" body", "body"); + raw"""\ should escape ' which inside a "" scope""" in : + Cmd(raw"""in "not-quoted \' body" body""") shouldEqual Array("in", "not-quoted ' body", "body"); + raw"""\ should escape ' which inside a '' scope""" in : + Cmd(raw"""in 'quoted \' body' body""") shouldEqual Array("in", "quoted ' body", "body"); + raw"""\ should escape " which inside a ' scope""" in : + Cmd(raw"""in 'not-quoted \" body' body""") shouldEqual Array("in", "not-quoted \" body", "body"); + raw"\ should not escape ascii-space which inside a quoted scope" in: + Cmd(raw"""'quoted \ do not escape' did""") shouldEqual Array(raw"quoted \ do not escape", "did"); + raw"with \ in the end should throws IllegalArgumentException" in: + an [IllegalArgumentException] should be thrownBy Cmd("something error!\\"); + + "with multi-line input should throws IllegalArgumentException" in: + an [IllegalArgumentException] should be thrownBy Cmd("something will\nhave a new line"); + + val example_special_character = Table( + "char", + " ", + "\t", + "\\t", + "\\a", + "/", + "&&", + "\\u1234", + ) + forAll(example_special_character) { char => + s"input with special character ($char) should keep origin like" in { + Cmd(s"$char dataset data[$char]contains parsed") shouldEqual + Array(char, "dataset", s"data[$char]contains", "parsed") + } + } + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/InputCommandTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/InputCommandTest.scala new file mode 100644 index 0000000..3baa038 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/InputCommandTest.scala @@ -0,0 +1,22 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi + +import cc.sukazyo.cono.morny.test.MornyTests + +class InputCommandTest extends MornyTests { + + "while create new InputCommand :" - { + + s"while input is $pending_val:" - { + + s"command should be $pending_val" in pending + s"target should be $pending_val" in pending + + "args array should always exists" in pending + + s"args should parsed to array $pending_val" in pending + + } + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/NamingUtilsTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/NamingUtilsTest.scala new file mode 100644 index 0000000..883d8aa --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/NamingUtilsTest.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi.formatting + +import cc.sukazyo.cono.morny.test.MornyTests + +class NamingUtilsTest extends MornyTests { + + "while generating inline query result id :" - { + + "while not use no data :" - { + + "(different tag) should return different id" in pending + "(same tag) should return the same id" in pending + + } + + "while use data :" - { + + "(same tag) with (same data) should return the same id" in pending + "(same tag) with (different data) should return different id" in pending + "(different tag) with (same data) should return different id" in pending + "change tag and data position should return different id" in pending + + } + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramFormatterTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramFormatterTest.scala new file mode 100644 index 0000000..1fbd912 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramFormatterTest.scala @@ -0,0 +1,9 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi.formatting + +import cc.sukazyo.cono.morny.test.MornyTests + +class TelegramFormatterTest extends MornyTests { + + "some test" in pending + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramParseEscapeTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramParseEscapeTest.scala new file mode 100644 index 0000000..eb3f7da --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramParseEscapeTest.scala @@ -0,0 +1,25 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi.formatting + +import cc.sukazyo.cono.morny.test.MornyTests + +class TelegramParseEscapeTest extends MornyTests { + + "while escape HTML document :" - { + + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + val any_other = "0ir0Q*%_\"ir[0\"#*I%T\"I{EtjpJGI{\")#W*IT}P%*IH#){#NIJB9-/q{$(Jg'9m]q|MH4j0hq}|+($NR{')}}" + + "& must be escaped" in: + h("a & b") shouldEqual "a & b" + "< and > must be escaped" in: + h("") shouldEqual "<data-error>" + "& and < and > must all be escaped" in: + h(" && ") shouldEqual "<some-a> && <some-b>" + "space and count should be kept" in: + h("\t<<<< \n") shouldEqual "\t<<<< \n" + "any others should kept origin like" in: + h(any_other) shouldEqual any_other + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramUserInformationTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramUserInformationTest.scala new file mode 100644 index 0000000..c839a5b --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/tgapi/formatting/TelegramUserInformationTest.scala @@ -0,0 +1,26 @@ +package cc.sukazyo.cono.morny.test.utils.tgapi.formatting + +import cc.sukazyo.cono.morny.test.MornyTests +import org.scalatest.prop.TableDrivenPropertyChecks +import org.scalatest.tagobjects.{Network, Slow} + +class TelegramUserInformationTest extends MornyTests with TableDrivenPropertyChecks { + + private val examples_telegram_cdn = Table( + ("username", "cdn"), + ("Eyre_S", "cdn5"), + ) + + forAll(examples_telegram_cdn) ((username, cdn) => s"while user is @$username :" - { + + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation.* + + s"datacenter should be $cdn" taggedAs (Slow, Network) in: + getDataCenterFromUser(username) shouldEqual cdn + + "formatted data should as expected" in: + pending + + }) + +} diff --git a/src/test/scala/live/LiveMain.scala b/src/test/scala/live/LiveMain.scala new file mode 100644 index 0000000..2b7de09 --- /dev/null +++ b/src/test/scala/live/LiveMain.scala @@ -0,0 +1,9 @@ +package live + +import cc.sukazyo.cono.morny.test.utils.BiliToolTest + +@main def LiveMain (args: String*): Unit = { + + org.scalatest.run(BiliToolTest()) + +} From 6e822274477d36501f3cd78ad45d82c871cdec6b Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sun, 17 Sep 2023 15:18:57 +0800 Subject: [PATCH 33/40] bug fix for scala port --- .gitignore | 1 + gradle.properties | 2 +- .../scala/cc/sukazyo/cono/morny/MornySystem.scala | 1 + .../cono/morny/bot/command/DirectMsgClear.scala | 4 ++++ .../cono/morny/bot/command/MornyInformation.scala | 9 ++++++--- .../cono/morny/bot/command/MornyOldJrrp.scala | 2 +- .../cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala | 6 +++--- .../cc/sukazyo/cono/morny/bot/command/喵呜.scala | 6 +++--- .../cc/sukazyo/cono/morny/bot/command/私わね.scala | 1 + .../cc/sukazyo/cono/morny/bot/event/OnCallMe.scala | 2 ++ .../cono/morny/bot/event/OnEventHackHandle.scala | 4 ++-- .../cono/morny/bot/event/OnQuestionMarkReply.scala | 12 +++++++++--- .../cono/morny/bot/event/OnTelegramCommand.scala | 2 +- .../sukazyo/cono/morny/bot/event/OnUserRandom.scala | 8 +++++--- .../cono/morny/bot/event/OnUserSlashAction.scala | 12 +++++++++++- .../sukazyo/cono/morny/bot/query/MyInformation.scala | 2 +- .../cono/morny/bot/query/ShareToolTwitter.scala | 12 ++++++------ .../cc/sukazyo/cono/morny/daemon/MornyReport.scala | 1 + .../scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala | 2 +- .../cono/morny/data/ip186/IP186QueryHandler.scala | 1 + .../sukazyo/cono/morny/internal/ScalaJavaConv.scala | 12 ------------ .../sukazyo/cono/morny/util/UniversalCommand.scala | 3 +-- .../sukazyo/cono/morny/util/tgapi/ExtraAction.java | 2 +- .../util/tgapi/formatting/TelegramFormatter.scala | 4 ++++ .../tgapi/formatting/TelegramUserInformation.scala | 4 ++-- 25 files changed, 69 insertions(+), 46 deletions(-) delete mode 100644 src/main/scala/cc/sukazyo/cono/morny/internal/ScalaJavaConv.scala diff --git a/.gitignore b/.gitignore index f9ff432..f02e4e8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ #build /build/ /bin/ +/out/ .metals/ .bloop/ .project diff --git a/gradle.properties b/gradle.properties index 1a9ee69..f7385a5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC4 +VERSION = 1.0.0-RC5 USE_DELTA = true VERSION_DELTA = scalaport4 diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala index 7e052eb..b355595 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala @@ -17,6 +17,7 @@ object MornySystem { @BuildConfigField val VERSION_DELTA: String = BuildConfig.VERSION_DELTA @BuildConfigField val CODENAME: String = BuildConfig.CODENAME @BuildConfigField val CODE_STORE: String = BuildConfig.CODE_STORE + //noinspection ScalaWeakerAccess @BuildConfigField val COMMIT_PATH: String = BuildConfig.COMMIT_PATH @BuildConfigField diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala index 7971d95..fb01c86 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala @@ -25,6 +25,10 @@ object DirectMsgClear extends ISimpleCommand { logger trace "message is not outdated(48 hrs ago)" val isTrusted = MornyCoeur.trusted isTrusted event.message.from.id + // todo: + // it does not work. due to the Telegram Bot API doesn't provide + // nested replyToMessage, so currently the trusted check by + // replyToMessage.replyToMessage will not work! def _isReplyTrusted: Boolean = if (event.message.replyToMessage.replyToMessage == null) false else if (event.message.replyToMessage.replyToMessage.from.id == event.message.from.id) true diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala index 516ddd7..8a55bc7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -14,6 +14,7 @@ import java.net.InetAddress import java.rmi.UnknownHostException import scala.language.postfixOps +// todo: maybe move some utils method outside object MornyInformation extends ITelegramCommand { private case object Subs { @@ -46,6 +47,7 @@ object MornyInformation extends ITelegramCommand { } + //noinspection ScalaWeakerAccess def getVersionGitTagHTML: String = { if (!MornySystem.isGitBuild) return "" val g = StringBuilder() @@ -61,11 +63,12 @@ object MornyInformation extends ITelegramCommand { val v = StringBuilder() v ++= s"${MornySystem VERSION_BASE}" if (MornySystem isUseDelta) v++=s"-δ${MornySystem VERSION_DELTA}" - if (MornySystem isGitBuild) v++="+"++=getVersionGitTagHTML + if (MornySystem isGitBuild) v++="+git."++=getVersionGitTagHTML v ++= s"*${MornySystem.CODENAME toUpperCase}" v toString } + //noinspection ScalaWeakerAccess def getRuntimeHostname: String|Null = { try InetAddress.getLocalHost.getHostName catch case _:UnknownHostException => null @@ -154,13 +157,13 @@ object MornyInformation extends ITelegramCommand { event.message.chat.id, /* language=html */ s"""system: - |- Morny ${h(if (getRuntimeHostname == null) "" else getRuntimeHostname)} + |- ${h(if (getRuntimeHostname == null) "" else getRuntimeHostname)} |- ${h(sysprop("os.name"))} ${h(sysprop("os.arch"))} ${h(sysprop("os.version"))} |java runtime: |- ${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))} |- ${h(sysprop("java.vm.version"))} |vm memory: - |- ${Runtime.getRuntime.totalMemory/1024/1024} / ${Runtime.getRuntime.maxMemory/1024/1024} + |- ${Runtime.getRuntime.totalMemory/1024/1024} / ${Runtime.getRuntime.maxMemory/1024/1024} MB |- ${Runtime.getRuntime.availableProcessors} cores |coeur version: |- $getVersionAllFullTagHTML diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala index 327d7df..c859f14 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala @@ -28,7 +28,7 @@ object MornyOldJrrp extends ITelegramCommand { MornyCoeur.extra exec SendMessage( event.message.chat.id, // language=html - f"${user.fullnameRefHTML} 在(utc的)今天的运气指数是———— $jrrp%.2f%%${h(ending)}" + f"${user.fullnameRefHTML} 在(utc的)今天的运气指数是———— $jrrp%.2f%% ${h(ending)}" ).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala index 4d7e343..32e6819 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala @@ -25,10 +25,10 @@ object Nbnhhsh extends ITelegramCommand { override def execute (using command: InputCommand, event: Update): Unit = { val queryTarget: String|Null = - if (event.message.replyToMessage != null && event.message.replyToMessage.text != null) - event.message.replyToMessage.text - else if command.args nonEmpty then + if command.args nonEmpty then command.args mkString " " + else if (event.message.replyToMessage != null && event.message.replyToMessage.text != null) + event.message.replyToMessage.text else null if (queryTarget == null) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala index bf36bab..109077e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala @@ -3,15 +3,15 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.InputCommand -import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.model.{Message, Update} +import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{SendMessage, SendSticker} import javax.swing.text.html.HTML import scala.annotation.unused import scala.language.postfixOps -@SuppressWarnings(Array("NonAsciiCharacters")) +//noinspection NonAsciiCharacters object 喵呜 { object 抱抱 extends ISimpleCommand { @@ -56,7 +56,7 @@ object 喵呜 { } private def replyingSet (whileRec: String, whileNew: String)(using event: Update): Unit = { - val isNew = event.message.replyToMessage == null; + val isNew = event.message.replyToMessage == null val target = if (isNew) event.message else event.message.replyToMessage MornyCoeur.extra exec new SendMessage( event.message.chat.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala index 8696bb8..5321657 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala @@ -7,6 +7,7 @@ import cc.sukazyo.cono.morny.util.UseRandom.* import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage +//noinspection NonAsciiCharacters object 私わね extends ISimpleCommand { override val name: String = "me" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala index a8d16b3..07f4d96 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -52,6 +52,8 @@ object OnCallMe extends EventListener { var isAllowed = false var lastDinnerData: Message|Null = null if (MornyCoeur.trusted isTrusted_dinnerReader req.from.id) { + // todo: have issues + // i dont want to test it anymore... it might be deprecated soon lastDinnerData = (MornyCoeur.extra exec GetChat(MornyCoeur.config.dinnerChatId)).chat.pinnedMessage val sendResp = MornyCoeur.extra exec ForwardMessage( req.from.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala index 121b3ef..f42d9e9 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala @@ -52,9 +52,9 @@ object OnEventHackHandle extends EventListener { override def onEditedMessage (using update: Update): Boolean = onEventHacked(update.editedMessage.chat.id, update.editedMessage.from.id) override def onChannelPost (using update: Update): Boolean = - onEventHacked(update.channelPost.chat.id, update.channelPost.from.id) + onEventHacked(update.channelPost.chat.id, 0) override def onEditedChannelPost (using update: Update): Boolean = - onEventHacked(update.editedChannelPost.chat.id, update.editedChannelPost.from.id) + onEventHacked(update.editedChannelPost.chat.id, 0) override def onInlineQuery (using update: Update): Boolean = onEventHacked(0, update.inlineQuery.from.id) override def onChosenInlineResult (using update: Update): Boolean = diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala index 601cafc..3d73a09 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala @@ -9,7 +9,14 @@ import scala.language.postfixOps object OnQuestionMarkReply extends EventListener { - private def QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓') + private val QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓') + + def isAllMessageMark (using text: String): Boolean = { + var isAll = true + for (c <- text) + if !(QUESTION_MARKS contains c) then isAll = false + isAll + } override def onMessage (using event: Update): Boolean = { @@ -18,8 +25,7 @@ object OnQuestionMarkReply extends EventListener { import cc.sukazyo.cono.morny.util.UseMath.over import cc.sukazyo.cono.morny.util.UseRandom.chance_is if (1 over 8) chance_is false then return false - for (c <- event.message.text toCharArray) - if !(QUESTION_MARKS contains c) then return false + if !isAllMessageMark(using event.message.text) then return false MornyCoeur.extra exec SendMessage( event.message.chat.id, event.message.text diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala index a2ef6ea..a850f21 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala @@ -22,7 +22,7 @@ object OnTelegramCommand extends EventListener { if (!(inputCommand.command matches "^\\w+$")) logger debug "not command" false - else if ((inputCommand.target ne null) && (inputCommand.target ne MornyCoeur.username)) + else if ((inputCommand.target ne null) && (inputCommand.target != MornyCoeur.username)) logger debug "not morny command" false else diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala index df18cb7..e47f3ce 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala @@ -9,13 +9,13 @@ import scala.language.postfixOps object OnUserRandom extends EventListener { - private val USER_OR_QUERY = "(.+)(?:还是|or)(.+)"r - private val USER_IF_QUERY = "(.+)[吗?|?]+$"r + private val USER_OR_QUERY = "^(.+)(?:还是|or)(.+)$"r + private val USER_IF_QUERY = "^(.+)(?:吗\\?|?|\\?|吗?)$"r override def onMessage(using update: Update): Boolean = { if update.message.text == null then return false - if update.message.text startsWith "/" then return false + if !(update.message.text startsWith "/") then return false import cc.sukazyo.cono.morny.util.UseRandom.rand_half val query = update.message.text substring 1 @@ -23,6 +23,8 @@ object OnUserRandom extends EventListener { case USER_OR_QUERY(_con1, _con2) => if rand_half then _con1 else _con2 case USER_IF_QUERY(_con) => + // for capability with [[OnQuestionMarkReply]] + if OnQuestionMarkReply.isAllMessageMark(using _con) then return false (if rand_half then "不" else "") + _con case _ => null diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala index 1778dfc..db80040 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala @@ -22,14 +22,24 @@ object OnUserSlashAction extends EventListener { if (text startsWith "/") { + // there has to be some special conditions for DP7 + // due to I have left DP7, I closed those special + // conditions. + // that is 2022, May 28th + // when one year goes, These code have rewrite with + // scala, those commented code is removed permanently. + // these message, here to remember the old DP7. + val actions = UniversalCommand(text) actions(0) = actions(0) substring 1 actions(0) actions(0) match + // ignore Telegram command like case TG_FORMAT(_) => return false + // ignore Path link case x if x contains "/" => return false case _ => @@ -55,7 +65,7 @@ object OnUserSlashAction extends EventListener { h(v_verb), if hasObject then "" else "了", if (origin == target) s"自己" - else origin.sender_firstnameRefHTML, + else target.sender_firstnameRefHTML, if hasObject then h(v_object+" ") else "" ) ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala index b6066e0..2c8aa58 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala @@ -14,7 +14,7 @@ object MyInformation extends ITelegramQuery { override def query (event: Update): List[InlineQueryUnit[_]] | Null = { - if ((event.inlineQuery.query ne null) || (event.inlineQuery.query nonEmpty)) return null + if !((event.inlineQuery.query eq null) || (event.inlineQuery.query isEmpty)) then return null List( InlineQueryUnit(InlineQueryResultArticle( diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala index 572c32e..baff446 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala @@ -9,11 +9,11 @@ import scala.util.matching.Regex object ShareToolTwitter extends ITelegramQuery { - val TITLE_VX = "[tweet] Share as VxTwitter" - val TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)" - val ID_PREFIX_VX = "[morny/share/twitter/vxtwi]" - val ID_PREFIX_VX_COMBINED = "[morny/share/twitter/vxtwi_combine]" - val REGEX_TWEET_LINK: Regex = "^(?:https?://)?((?:(?:c\\.)?vx|fx|www\\.)?twitter\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(\\?[\\w&=-]+)?$"r + private val TITLE_VX = "[tweet] Share as VxTwitter" + private val TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)" + private val ID_PREFIX_VX = "[morny/share/twitter/vxtwi]" + private val ID_PREFIX_VX_COMBINED = "[morny/share/twitter/vxtwi_combine]" + private val REGEX_TWEET_LINK: Regex = "^(?:https?://)?((?:(?:c\\.)?vx|fx|www\\.)?twitter\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(\\?[\\w&=-]+)?$"r override def query (event: Update): List[InlineQueryUnit[_]] | Null = { @@ -21,7 +21,7 @@ object ShareToolTwitter extends ITelegramQuery { event.inlineQuery.query match - case REGEX_TWEET_LINK(_1, _2, _) => + case REGEX_TWEET_LINK(_, _2, _, _, _, _) => List( InlineQueryUnit(InlineQueryResultArticle( inlineQueryId(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX, diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala index 9591fb1..7d39431 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala @@ -74,6 +74,7 @@ object MornyReport { ).parseMode(ParseMode HTML)) } + //noinspection ScalaWeakerAccess def sectionConfigFields (config: MornyConfig): String = { val echo = StringBuilder() for (field <- config.getClass.getFields) { diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala index d6ec709..b707576 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyJrrp.scala @@ -12,6 +12,6 @@ object MornyJrrp { private def jrrp_v_xmomi (identifier: Long, dayStamp: Long): Double = import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5 import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex - (java.lang.Long parseLong MD5(s"$identifier@$dayStamp").toHex.substring(0, 4)) / (0xffff toDouble) + java.lang.Long.parseLong(MD5(s"$identifier@$dayStamp").toHex.substring(0, 4), 16) / (0xffff toDouble) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala index f9dc533..d045cc5 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/ip186/IP186QueryHandler.scala @@ -19,6 +19,7 @@ object IP186QueryHandler { commonQuery(SITE_URL + ip, QUERY_PARAM_IP) @throws[IOException] + //noinspection ScalaWeakerAccess def query_whois (domain: String): IP186Response = commonQuery(SITE_URL+"whois/"+domain, QUERY_PARAM_WHOIS) diff --git a/src/main/scala/cc/sukazyo/cono/morny/internal/ScalaJavaConv.scala b/src/main/scala/cc/sukazyo/cono/morny/internal/ScalaJavaConv.scala deleted file mode 100644 index 3547a2b..0000000 --- a/src/main/scala/cc/sukazyo/cono/morny/internal/ScalaJavaConv.scala +++ /dev/null @@ -1,12 +0,0 @@ -package cc.sukazyo.cono.morny.internal - -import scala.jdk.CollectionConverters._ -import scala.collection.immutable as simm -import java.util as j - -object ScalaJavaConv { - - def jSetInteger2simm (data: j.Set[Integer]): simm.Set[Int] = - data.asScala.toSet.map(_.intValue) - -} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala index 65a1e87..9aa6e5c 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala @@ -36,8 +36,7 @@ object UniversalCommand { arg = arg.empty } else if (input(i) isQuote) { val _inside_tag = input(i) - var _inside = true - boundary { while (_inside) { + boundary { while (true) { i=i+1 if (i >= input.length) throw IllegalArgumentException("UniversalCommand: unclosed quoted text") if (input(i) == _inside_tag) diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java index 784e6c6..b7f19ee 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java @@ -32,7 +32,7 @@ public class ExtraAction { public , R extends BaseResponse> R exec (T req, String errorMessage) { final R resp = bot.execute(req); if (!resp.isOk()) throw new EventRuntimeException.ActionFailed( - (errorMessage.equals("") ? String.valueOf(resp.errorCode()) : errorMessage), + (errorMessage.isEmpty() ? String.valueOf(resp.errorCode()) : errorMessage), resp ); return resp; diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala index acc0d49..48fd6ef 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramFormatter.scala @@ -22,6 +22,7 @@ object TelegramFormatter { case _ => s"@[c/${chat.id}]" else s"@${h(chat.username)}" + //noinspection ScalaWeakerAccess def safe_firstnameRefHTML: String = chat.`type` match // language=html @@ -29,6 +30,7 @@ object TelegramFormatter { // language=html case _ => s"${h(chat.title)}" + //noinspection ScalaWeakerAccess def id_tdLib: Long = if chat.id < 0 then (chat.id - MASK_BOTAPI_ID)abs else chat.id @@ -42,12 +44,14 @@ object TelegramFormatter { extension (user: User) { + //noinspection ScalaWeakerAccess def fullname: String = _connectName(user.firstName, user.lastName) def fullnameRefHTML: String = // language=html s"${h(user.fullname)}" + //noinspection ScalaWeakerAccess def firstnameRefHTML: String = // language=html s"${h(user.firstName)}" diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala index 81d87ee..88a9501 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/formatting/TelegramUserInformation.scala @@ -9,8 +9,8 @@ import scala.util.Using object TelegramUserInformation { - val DC_QUERY_SOURCE_SITE = "https://t.me/" - val DC_QUERY_PROCESSOR_REGEX: Regex = "(cdn[1-9]).tele(sco.pe|gram-cdn.org)"r + private val DC_QUERY_SOURCE_SITE = "https://t.me/" + private val DC_QUERY_PROCESSOR_REGEX: Regex = "(cdn[1-9]).tele(sco.pe|gram-cdn.org)"r private val httpClient = OkHttpClient() From 511036e2cedf842f8ded675b7663f8ef8241c090 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 23 Sep 2023 16:38:07 +0800 Subject: [PATCH 34/40] refactor runtime context from class instance to using/given --- build.gradle | 7 +- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/MornyAssets.scala | 2 + .../cc/sukazyo/cono/morny/MornyCoeur.scala | 101 +++++++++--------- .../cc/sukazyo/cono/morny/MornyConfig.java | 2 +- .../cc/sukazyo/cono/morny/MornySystem.scala | 3 +- .../cc/sukazyo/cono/morny/ServerMain.scala | 2 +- .../cono/morny/bot/api/EventListener.scala | 3 +- .../morny/bot/api/EventListenerManager.scala | 7 +- .../bot/api/TelegramUpdatesListener.scala | 7 +- .../morny/bot/command/DirectMsgClear.scala | 20 ++-- .../cono/morny/bot/command/Encryptor.scala | 28 ++--- .../cono/morny/bot/command/EventHack.scala | 12 +-- .../morny/bot/command/GetUsernameAndId.scala | 14 +-- .../cono/morny/bot/command/IP186Query.scala | 10 +- .../morny/bot/command/MornyCommands.scala | 62 ++++++----- .../cono/morny/bot/command/MornyHellos.scala | 6 +- .../morny/bot/command/MornyInfoOnStart.scala | 9 +- .../morny/bot/command/MornyInformation.scala | 64 +++-------- .../bot/command/MornyInformationOlds.scala | 6 +- .../morny/bot/command/MornyManagers.scala | 22 ++-- .../cono/morny/bot/command/MornyOldJrrp.scala | 5 +- .../cono/morny/bot/command/Nbnhhsh.scala | 8 +- .../cono/morny/bot/command/Testing.scala | 7 +- .../sukazyo/cono/morny/bot/command/喵呜.scala | 6 +- .../cono/morny/bot/command/私わね.scala | 4 +- .../morny/bot/event/MornyEventListeners.scala | 34 +++--- ...neQuery.scala => MornyOnInlineQuery.scala} | 6 +- ...and.scala => MornyOnTelegramCommand.scala} | 6 +- ...=> MornyOnUpdateTimestampOffsetLock.scala} | 4 +- .../cono/morny/bot/event/OnCallMe.scala | 22 ++-- .../cono/morny/bot/event/OnCallMsgSend.scala | 24 ++--- .../morny/bot/event/OnEventHackHandle.scala | 64 +++-------- .../bot/event/OnMedicationNotifyApply.scala | 6 +- .../morny/bot/event/OnQuestionMarkReply.scala | 27 +++-- .../morny/bot/event/OnUniMeowTrigger.scala | 5 +- .../cono/morny/bot/event/OnUserRandom.scala | 4 +- .../morny/bot/event/OnUserSlashAction.scala | 4 +- .../cono/morny/bot/query/ITelegramQuery.scala | 6 +- .../cono/morny/bot/query/MornyQueries.scala | 11 +- .../cono/morny/bot/query/MyInformation.scala | 2 +- .../cono/morny/bot/query/RawText.scala | 2 +- .../morny/bot/query/ShareToolBilibili.scala | 2 +- .../morny/bot/query/ShareToolTwitter.scala | 2 +- .../cono/morny/daemon/EventHacker.scala | 49 +++++++++ .../cono/morny/daemon/MedicationTimer.scala | 35 +++--- .../cono/morny/daemon/MornyDaemons.scala | 16 +-- .../cono/morny/daemon/MornyReport.scala | 31 +++--- .../cono/morny/data/MornyInformation.scala | 45 ++++++++ .../cono/morny/data/TelegramImages.scala | 15 ++- 50 files changed, 443 insertions(+), 398 deletions(-) rename src/main/scala/cc/sukazyo/cono/morny/bot/event/{OnInlineQuery.scala => MornyOnInlineQuery.scala} (82%) rename src/main/scala/cc/sukazyo/cono/morny/bot/event/{OnTelegramCommand.scala => MornyOnTelegramCommand.scala} (84%) rename src/main/scala/cc/sukazyo/cono/morny/bot/event/{OnUpdateTimestampOffsetLock.scala => MornyOnUpdateTimestampOffsetLock.scala} (81%) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala diff --git a/build.gradle b/build.gradle index deb0e57..39c8d01 100644 --- a/build.gradle +++ b/build.gradle @@ -110,8 +110,6 @@ tasks.withType(ScalaCompile).configureEach { scalaCompileOptions.encoding = proj_file_encoding.name() scalaCompileOptions.additionalParameters.add "-language:postfixOps" -// scalaCompileOptions.additionalParameters.add("-Yexplicit-nulls") -// scalaCompileOptions.additionalParameters.add "-language:experimental.saferExceptions" } @@ -196,10 +194,15 @@ publishing { } } publications { + //noinspection GroovyAssignabilityCheck main (MavenPublication) { + //noinspection GroovyAssignabilityCheck from components.java + //noinspection GroovyAssignabilityCheck groupId = proj_group + //noinspection GroovyAssignabilityCheck artifactId = proj_archive_name + //noinspection GroovyAssignabilityCheck version = proj_version } } diff --git a/gradle.properties b/gradle.properties index 6f20a80..46a5b30 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s VERSION = 1.0.0-RC5 USE_DELTA = true -VERSION_DELTA = scala1 +VERSION_DELTA = scala2 CODENAME = beiping diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala index 489ac36..c331ea9 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala @@ -4,6 +4,8 @@ import cc.sukazyo.restools.ResourcesPackage object MornyAssets { + class AssetsException (caused: Throwable) extends Exception("Cannot read assets file.", caused) + val pack: ResourcesPackage = ResourcesPackage(MornyAssets.getClass, "assets_morny") } diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala index 1860236..4463678 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -5,7 +5,8 @@ import cc.sukazyo.cono.morny.daemon.MornyDaemons import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.MornyCoeur.THREAD_MORNY_EXIT import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener -import cc.sukazyo.cono.morny.bot.event.MornyEventListeners +import cc.sukazyo.cono.morny.bot.event.{MornyEventListeners, MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock} +import cc.sukazyo.cono.morny.bot.query.MornyQueries import cc.sukazyo.cono.morny.util.tgapi.ExtraAction import com.pengrad.telegrambot.TelegramBot import com.pengrad.telegrambot.request.GetMe @@ -17,53 +18,15 @@ object MornyCoeur { val THREAD_MORNY_EXIT = "morny-exiting" - private var INSTANCE: MornyCoeur|Null = _ - - val coeurStartTimestamp: Long = ServerMain.systemStartupTime - - def account: TelegramBot = INSTANCE.account - def username: String = INSTANCE.username - def userid: Long = INSTANCE.userid - def config: MornyConfig = INSTANCE.my_config - def trusted: MornyTrusted = INSTANCE.trusted - def extra: ExtraAction = INSTANCE.extra - - def available: Boolean = INSTANCE != null - def callSaveData(): Unit = INSTANCE.saveDataAll() - - def exitReason: AnyRef|Null = INSTANCE.whileExit_reason - - def exit (status: Int, reason: AnyRef): Unit = - INSTANCE.whileExit_reason = reason - System exit status - - def init (using config: MornyConfig): Unit = { - - if (INSTANCE ne null) - logger error "Coeur already started!!!" - return; - - logger info "Coeur starting..." - INSTANCE = MornyCoeur() - - MornyDaemons.start() - - logger info "start telegram event listening" - MornyEventListeners.registerAllEvents() - INSTANCE.account.setUpdatesListener(TelegramUpdatesListener) - - if config.commandLoginRefresh then - logger info "resetting telegram command list" - MornyCommands.automaticTGListUpdate() - - logger info "Coeur start complete." - - } - } -class MornyCoeur (using config: MornyConfig) { - def my_config: MornyConfig = config +class MornyCoeur (using val config: MornyConfig) { + + given MornyCoeur = this + + ///>>> BLOCK START instance configure & startup stage 1 + + logger info "Coeur starting..." logger info s"args key:\n ${config.telegramBotKey}" if config.telegramBotUsername ne null then @@ -76,13 +39,47 @@ class MornyCoeur (using config: MornyConfig) { configure_exitCleanup() + ///<<< BLOCK END instance configure & startup stage 1 + + /** [[TelegramBot]] account of this Morny */ val account: TelegramBot = __loginResult.account + /** [[account]]'s telegram username */ val username: String = __loginResult.username + /** [[account]]'s telegram user id */ val userid: Long = __loginResult.userid - val trusted: MornyTrusted = MornyTrusted(using this) + /** current Morny's [[MornyTrusted]] instance */ + val trusted: MornyTrusted = MornyTrusted() + /** current Morny's [[ExtraAction]] toolset */ val extra: ExtraAction = ExtraAction as __loginResult.account - var whileExit_reason: AnyRef|Null = _ + private val updatesListener: TelegramUpdatesListener = TelegramUpdatesListener() + + val daemons: MornyDaemons = MornyDaemons() + updatesListener.manager register MornyOnUpdateTimestampOffsetLock() + val commands: MornyCommands = MornyCommands() + //noinspection ScalaWeakerAccess + val queries: MornyQueries = MornyQueries() + updatesListener.manager register MornyOnTelegramCommand(using commands) + updatesListener.manager register MornyOnInlineQuery(using queries) + val events: MornyEventListeners = MornyEventListeners(using updatesListener.manager) + + /** inner value: about why morny exit, used in [[daemon.MornyReport]]. */ + private var whileExit_reason: AnyRef|Null = _ + def exitReason: AnyRef|Null = whileExit_reason + val coeurStartTimestamp: Long = ServerMain.systemStartupTime + + ///>>> BLOCK START instance configure & startup stage 2 + + daemons.start() + logger info "start telegram event listening" + account setUpdatesListener updatesListener + if config.commandLoginRefresh then + logger info "resetting telegram command list" + commands.automaticTGListUpdate() + + logger info "Coeur start complete." + + ///<<< BLOCK END instance configure & startup stage 2 def saveDataAll(): Unit = { // nothing to do @@ -90,15 +87,19 @@ class MornyCoeur (using config: MornyConfig) { } private def exitCleanup (): Unit = { - MornyDaemons.stop() + daemons.stop() if config.commandLogoutClear then - MornyCommands.automaticTGListRemove() + commands.automaticTGListRemove() } private def configure_exitCleanup (): Unit = { Runtime.getRuntime.addShutdownHook(new Thread(() => exitCleanup(), THREAD_MORNY_EXIT)) } + def exit (status: Int, reason: AnyRef): Unit = + whileExit_reason = reason + System exit status + private case class LoginResult(account: TelegramBot, username: String, userid: Long) private def login (): LoginResult|Null = { diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java index 2702a61..692706d 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java @@ -86,7 +86,7 @@ public class MornyConfig { /** * morny 的事件忽略前缀时间
      *
      - * {@link cc.sukazyo.cono.morny.bot.event.OnUpdateTimestampOffsetLock} + * {@link cc.sukazyo.cono.morny.bot.event.MornyOnUpdateTimestampOffsetLock} * 会根据这里定义的时间戳取消掉比此时间更早的事件链 */ public final long eventOutdatedTimestamp; diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala index b355595..97866db 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala @@ -2,7 +2,6 @@ package cc.sukazyo.cono.morny import cc.sukazyo.cono.morny.internal.BuildConfigField import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} -import cc.sukazyo.cono.morny.daemon.MornyReport import cc.sukazyo.cono.morny.util.FileUtils import java.io.IOException @@ -42,7 +41,7 @@ object MornySystem { "" case n: NoSuchAlgorithmException => logger error exceptionLog(n) - MornyReport.exception(n, "") +// MornyReport.exception(n, "") // todo: will not implemented "" } diff --git a/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala index b481eb0..5947797 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala @@ -143,7 +143,7 @@ object ServerMain { Thread.currentThread setName THREAD_MORNY_INIT try - MornyCoeur.init(using config build) + MornyCoeur(using config build) catch { case _: CheckFailure.NullTelegramBotKey => logger.info("Parameter required has no value:\n --token.") diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala index 67b7047..e4ab6de 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala @@ -1,8 +1,9 @@ package cc.sukazyo.cono.morny.bot.api +import cc.sukazyo.cono.morny.MornyCoeur import com.pengrad.telegrambot.model.Update -trait EventListener { +trait EventListener (using MornyCoeur) { def onMessage (using Update): Boolean = false def onEditedMessage (using Update): Boolean = false diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala index 98b7852..dc61951 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala @@ -1,8 +1,7 @@ package cc.sukazyo.cono.morny.bot.api -import cc.sukazyo.cono.morny.Log +import cc.sukazyo.cono.morny.{Log, MornyCoeur} import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} -import cc.sukazyo.cono.morny.daemon.MornyReport import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException import com.google.gson.GsonBuilder import com.pengrad.telegrambot.model.Update @@ -10,7 +9,7 @@ import com.pengrad.telegrambot.model.Update import scala.collection.mutable import scala.language.postfixOps -object EventListenerManager { +class EventListenerManager (using coeur: MornyCoeur) { private val listeners = mutable.Queue.empty[EventListener] @@ -69,7 +68,7 @@ object EventListenerManager { ) indent 4) ++= "\n" case _ => logger error errorMessage.toString - MornyReport.exception(e, "on event running") + coeur.daemons.reporter.exception(e, "on event running") } if (status isOk) return } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala index 83c2101..dd9f71e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala @@ -1,16 +1,19 @@ package cc.sukazyo.cono.morny.bot.api +import cc.sukazyo.cono.morny.MornyCoeur import com.pengrad.telegrambot.UpdatesListener import com.pengrad.telegrambot.model.Update import java.util import scala.jdk.CollectionConverters.* -object TelegramUpdatesListener extends UpdatesListener { +class TelegramUpdatesListener (using MornyCoeur) extends UpdatesListener { + + val manager = EventListenerManager() override def process (updates: util.List[Update]): Int = { for (update <- updates.asScala) - EventListenerManager.publishUpdate(using update) + manager.publishUpdate(using update) UpdatesListener.CONFIRMED_UPDATES_ALL } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala index fb01c86..56bec4f 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala @@ -9,7 +9,7 @@ import com.pengrad.telegrambot.request.{DeleteMessage, GetChatMember, SendSticke import scala.language.postfixOps -object DirectMsgClear extends ISimpleCommand { +class DirectMsgClear (using coeur: MornyCoeur) extends ISimpleCommand { override val name: String = "r" override val aliases: Array[ICommandAlias] | Null = null @@ -19,16 +19,16 @@ object DirectMsgClear extends ISimpleCommand { logger debug "executing command /r" if (event.message.replyToMessage == null) return; logger trace "message is a reply" - if (event.message.replyToMessage.from.id != MornyCoeur.userid) return; + if (event.message.replyToMessage.from.id != coeur.userid) return; logger trace "message replied is from me" if (System.currentTimeMillis/1000 - event.message.replyToMessage.date > 48*60*60) return; logger trace "message is not outdated(48 hrs ago)" - val isTrusted = MornyCoeur.trusted isTrusted event.message.from.id + val isTrusted = coeur.trusted isTrusted event.message.from.id // todo: - // it does not work. due to the Telegram Bot API doesn't provide - // nested replyToMessage, so currently the trusted check by - // replyToMessage.replyToMessage will not work! + // it does not work. due to the Telegram Bot API doesn't provide + // nested replyToMessage, so currently the trusted check by + // replyToMessage.replyToMessage will not work! def _isReplyTrusted: Boolean = if (event.message.replyToMessage.replyToMessage == null) false else if (event.message.replyToMessage.replyToMessage.from.id == event.message.from.id) true @@ -36,19 +36,19 @@ object DirectMsgClear extends ISimpleCommand { if (isTrusted || _isReplyTrusted) { - MornyCoeur.extra exec DeleteMessage( + coeur.extra exec DeleteMessage( event.message.chat.id, event.message.replyToMessage.messageId ) def _isPrivate: Boolean = event.message.chat.`type` == Chat.Type.Private def _isPermission: Boolean = - (MornyCoeur.extra exec GetChatMember(event.message.chat.id, event.message.from.id)) + (coeur.extra exec GetChatMember(event.message.chat.id, event.message.from.id)) .chatMember.canDeleteMessages if (_isPrivate || _isPermission) { - MornyCoeur.extra exec DeleteMessage(event.message.chat.id, event.message.messageId) + coeur.extra exec DeleteMessage(event.message.chat.id, event.message.messageId) } - } else MornyCoeur.extra exec SendSticker( + } else coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala index f1faef6..8bc5bba 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala @@ -2,7 +2,6 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.MornyCoeur -import cc.sukazyo.cono.morny.daemon.MornyReport import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.CommonEncrypt @@ -17,7 +16,7 @@ import java.util.Base64 import scala.language.postfixOps /** Provides Telegram Command __`/encrypt`__. */ -object Encryptor extends ITelegramCommand { +class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { override val name: String = "encrypt" override val aliases: Array[ICommandAlias] | Null = null @@ -48,7 +47,7 @@ object Encryptor extends ITelegramCommand { val mod_uppercase = if (args.length > 1) { if (args.length < 3 && _is_mod_u(args(1))) true else - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) @@ -74,11 +73,11 @@ object Encryptor extends ITelegramCommand { val _r = event.message.replyToMessage if ((_r ne null) && (_r.document ne null)) { try {XFile( - MornyCoeur.account getFileContent (MornyCoeur.extra exec GetFile(_r.document.fileId)).file, + coeur.account getFileContent (coeur.extra exec GetFile(_r.document.fileId)).file, _r.document.fileName )} catch case e: IOException => logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" - MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI") + coeur.daemons.reporter.exception(e, "NetworkRequest error: TelegramFileAPI") return } else if ((_r ne null) && (_r.photo ne null)) { try { @@ -92,22 +91,23 @@ object Encryptor extends ITelegramCommand { if (_photo_origin eq null) throw IllegalArgumentException("no photo from api.") import cc.sukazyo.cono.morny.util.UseRandom.rand_id XFile( - MornyCoeur.account getFileContent (MornyCoeur.extra exec GetFile(_photo_origin.fileId)).file, + coeur.account getFileContent (coeur.extra exec GetFile(_photo_origin.fileId)).file, s"photo$rand_id.png" ) } catch case e: IOException => + //noinspection DuplicatedCode logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" - MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI") + coeur.daemons.reporter.exception(e, "NetworkRequest error: TelegramFileAPI") return case e: IllegalArgumentException => logger warn s"FileProcess error: PhotoSize:\n\t${e.getMessage}" - MornyReport.exception(e, "FileProcess error: PhotoSize") + coeur.daemons.reporter.exception(e, "FileProcess error: PhotoSize") return } else if ((_r ne null) && (_r.text ne null)) { XText(_r.text) } else { - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, "null" ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) @@ -153,7 +153,7 @@ object Encryptor extends ITelegramCommand { _tool_b64d.decode, CommonEncrypt.lint_base64FileName ) } catch case _: IllegalArgumentException => - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_404 // todo: is here better erro notify? ).replyToMessageId(event.message.messageId) @@ -163,7 +163,7 @@ object Encryptor extends ITelegramCommand { case "sha256" => genResult_hash(input, SHA256) case "sha512" => genResult_hash(input, SHA512) case _ => - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) @@ -173,13 +173,13 @@ object Encryptor extends ITelegramCommand { // output result match case _file: EXFile => - MornyCoeur.extra exec SendDocument( + coeur.extra exec SendDocument( event.message.chat.id, _file.result ).fileName(_file.resultName).replyToMessageId(event.message.messageId) case _text: EXTextLike => import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, s"
      ${h(_text.text)}
      " ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) @@ -211,7 +211,7 @@ object Encryptor extends ITelegramCommand { * */ private def echoHelp(chat: Long, replyTo: Int): Unit = - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( chat, s"""base64, b64 |base64url, base64u, b64u diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala index 963c80a..e6a4fdb 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala @@ -1,7 +1,5 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur -import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle -import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle.{registerHack, HackType} import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.Update @@ -9,7 +7,7 @@ import com.pengrad.telegrambot.request.SendSticker import scala.language.postfixOps -object EventHack extends ITelegramCommand { +class EventHack (using coeur: MornyCoeur) extends ITelegramCommand { override val name: String = "event_hack" override val aliases: Array[ICommandAlias] | Null = null @@ -18,15 +16,17 @@ object EventHack extends ITelegramCommand { override def execute (using command: InputCommand, event: Update): Unit = { + import coeur.daemons.eventHack.{registerHack, HackType} + val x_mode = if (command.args nonEmpty) command.args(0) else "" def done_ok = - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_WAITING ).replyToMessageId(event.message.messageId) def done_forbiddenForAny = - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) @@ -40,7 +40,7 @@ object EventHack extends ITelegramCommand { ) x_mode match case "any" => - if (MornyCoeur.trusted isTrusted event.message.from.id) + if (coeur.trusted isTrusted event.message.from.id) doRegister(HackType ANY) done_ok else done_forbiddenForAny diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala index 78013bd..627d737 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala @@ -9,7 +9,7 @@ import com.pengrad.telegrambot.request.{GetChatMember, SendMessage} import scala.language.postfixOps -object GetUsernameAndId extends ITelegramCommand { +class GetUsernameAndId (using coeur: MornyCoeur) extends ITelegramCommand { override val name: String = "user" override val aliases: Array[ICommandAlias] | Null = null @@ -21,7 +21,7 @@ object GetUsernameAndId extends ITelegramCommand { val args = command.args if (args.length > 1) - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, "[Unavailable] Too much arguments." ).replyToMessageId(event.message.messageId) @@ -31,7 +31,7 @@ object GetUsernameAndId extends ITelegramCommand { if (args nonEmpty) { try args(0) toLong catch case e: NumberFormatException => - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, s"[Unavailable] ${e.getMessage}" ).replyToMessageId(event.message.messageId) @@ -39,10 +39,10 @@ object GetUsernameAndId extends ITelegramCommand { } else if (event.message.replyToMessage eq null) event.message.from.id else event.message.replyToMessage.from.id - val response = MornyCoeur.account execute GetChatMember(event.message.chat.id, userId) + val response = coeur.account execute GetChatMember(event.message.chat.id, userId) if (response.chatMember eq null) - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, "[Unavailable] user not found." ).replyToMessageId(event.message.messageId) @@ -51,13 +51,13 @@ object GetUsernameAndId extends ITelegramCommand { val user = response.chatMember.user if (user.id == Standardize.CHANNEL_SPEAKER_MAGIC_ID) - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, "$__channel_identify" ).replyToMessageId(event.message.messageId) return; - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, TelegramUserInformation getFormattedInformation user ).replyToMessageId(event.message.messageId()).parseMode(ParseMode HTML) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala index a5d17b6..d6c80b0 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala @@ -9,7 +9,7 @@ import com.pengrad.telegrambot.request.SendMessage import scala.language.postfixOps -object IP186Query { +class IP186Query (using coeur: MornyCoeur) { private enum Subs (val cmd: String): case IP extends Subs("ip") @@ -34,7 +34,7 @@ object IP186Query { if (command.args isEmpty) if event.message.replyToMessage eq null then null else event.message.replyToMessage.text else if (command.args.length > 1) - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, "[Unavailable] Too much arguments." ).replyToMessageId(event.message.messageId) @@ -42,7 +42,7 @@ object IP186Query { else command.args(0) if (target eq null) - MornyCoeur.extra exec new SendMessage( + coeur.extra exec new SendMessage( event.message.chat.id, "[Unavailable] No ip defined." ).replyToMessageId(event.message.messageId) @@ -57,7 +57,7 @@ object IP186Query { case Subs.WHOIS.cmd => IP186QueryHandler.query_whoisPretty(target) case _ => throw IllegalArgumentException(s"Unknown 186-IP query method ${command.command}") - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, s"""${h(response.url)} |${h(response.body)}""" @@ -65,7 +65,7 @@ object IP186Query { ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) } catch case e: Exception => - MornyCoeur.extra exec new SendMessage( + coeur.extra exec new SendMessage( event.message().chat().id(), s"""[Exception] in query: |${h(e.getMessage)}""" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala index 4776d68..21fdf46 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala @@ -11,7 +11,7 @@ import scala.collection.{mutable, SeqMap} import scala.collection.mutable.ArrayBuffer import scala.language.postfixOps -object MornyCommands { +class MornyCommands (using coeur: MornyCoeur) { private type CommandMap = SeqMap[String, ISimpleCommand] private def CommandMap (commands: ISimpleCommand*): CommandMap = @@ -22,41 +22,47 @@ object MornyCommands { stash += (alias.name -> i) stash + private val $MornyHellos = MornyHellos() + private val $IP186Query = IP186Query() + private val $MornyInformation = MornyInformation() + private val $MornyInformationOlds = MornyInformationOlds(using $MornyInformation) + private val $MornyManagers = MornyManagers() + //noinspection NonAsciiCharacters + private val $喵呜 = 喵呜() private val commands: CommandMap = CommandMap( - MornyHellos.On, - MornyHellos.On, - MornyHellos.Hello, - MornyInfoOnStart, - GetUsernameAndId, - EventHack, - Nbnhhsh, - IP186Query.IP, - IP186Query.Whois, - Encryptor, - MornyManagers.SaveData, - MornyInformation, - MornyInformationOlds.Version, - MornyInformationOlds.Runtime, - MornyOldJrrp, - MornyManagers.Exit, + $MornyHellos.On, + $MornyHellos.Hello, + MornyInfoOnStart(), + GetUsernameAndId(), + EventHack(), + Nbnhhsh(), + $IP186Query.IP, + $IP186Query.Whois, + Encryptor(), + $MornyManagers.SaveData, + $MornyInformation, + $MornyInformationOlds.Version, + $MornyInformationOlds.Runtime, + MornyOldJrrp(), + $MornyManagers.Exit, - Testing, - DirectMsgClear, + Testing(), + DirectMsgClear(), //noinspection NonAsciiCharacters - 私わね, + 私わね(), //noinspection NonAsciiCharacters - 喵呜.Progynova + $喵呜.Progynova ) //noinspection NonAsciiCharacters val commands_uni: CommandMap = CommandMap( - 喵呜.抱抱, - 喵呜.揉揉, - 喵呜.贴贴, - 喵呜.蹭蹭 + $喵呜.抱抱, + $喵呜.揉揉, + $喵呜.贴贴, + $喵呜.蹭蹭 ) def execute (using command: InputCommand, event: Update): Boolean = { @@ -69,7 +75,7 @@ object MornyCommands { private def nonCommandExecutable (using command: InputCommand, event: Update): Boolean = { if command.target eq null then false else - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) @@ -79,14 +85,14 @@ object MornyCommands { def automaticTGListUpdate (): Unit = { val listing = commands_toTelegramList automaticTGListRemove() - MornyCoeur.extra exec SetMyCommands(listing:_*) + coeur.extra exec SetMyCommands(listing:_*) logger info s"""automatic updated telegram command list : |${commandsTelegramList_toString(listing)}""".stripMargin } def automaticTGListRemove (): Unit = { - MornyCoeur.extra exec DeleteMyCommands() + coeur.extra exec DeleteMyCommands() logger info "cleaned up command list" } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala index 80c4519..5c6abb6 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala @@ -8,7 +8,7 @@ import com.pengrad.telegrambot.request.SendSticker import scala.language.postfixOps -object MornyHellos { +class MornyHellos (using coeur: MornyCoeur) { object On extends ITelegramCommand { @@ -18,7 +18,7 @@ object MornyHellos { override val description: String = "检查是否在线" override def execute (using command: InputCommand, event: Update): Unit = - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_ONLINE_STATUS_RETURN ).replyToMessageId(event.message.messageId) @@ -33,7 +33,7 @@ object MornyHellos { override val description: String = "打招呼" override def execute (using command: InputCommand, event: Update): Unit = - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_HELLO ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala index 8a9b101..038754a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala @@ -1,6 +1,7 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.data.MornyInformation.{getAboutPic, getMornyAboutLinksHTML} import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode @@ -8,22 +9,22 @@ import com.pengrad.telegrambot.request.SendPhoto import scala.language.postfixOps -object MornyInfoOnStart extends ISimpleCommand { +class MornyInfoOnStart (using coeur: MornyCoeur) extends ISimpleCommand { override val name: String = "start" override val aliases: Array[ICommandAlias] | Null = null override def execute (using command: InputCommand, event: Update): Unit = { - MornyCoeur.extra exec new SendPhoto( + coeur.extra exec new SendPhoto( event.message.chat.id, - MornyInformation.getAboutPic + getAboutPic ).caption( s"""欢迎使用 Morny Cono来自安妮的侍从小鼠。 |Morny 具有各种各样的功能。 | |———————————————— - |${MornyInformation.getMornyAboutLinksHTML} + |$getMornyAboutLinksHTML |———————————————— | |(你可以随时通过 /info 重新获得这些信息)""" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala index 8a55bc7..583b885 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -1,7 +1,8 @@ package cc.sukazyo.cono.morny.bot.command -import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornyCoeur, MornySystem} -import cc.sukazyo.cono.morny.data.{TelegramImages, TelegramStickers} +import cc.sukazyo.cono.morny.{BuildConfig, MornyCoeur, MornySystem} +import cc.sukazyo.cono.morny.data.MornyInformation.* +import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.tgapi.InputCommand @@ -10,12 +11,10 @@ import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{SendMessage, SendPhoto, SendSticker} import java.lang.System -import java.net.InetAddress -import java.rmi.UnknownHostException import scala.language.postfixOps // todo: maybe move some utils method outside -object MornyInformation extends ITelegramCommand { +class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { private case object Subs { val STICKERS = "stickers" @@ -47,43 +46,8 @@ object MornyInformation extends ITelegramCommand { } - //noinspection ScalaWeakerAccess - def getVersionGitTagHTML: String = { - if (!MornySystem.isGitBuild) return "" - val g = StringBuilder() - val cm = BuildConfig.COMMIT substring(0, 8) - val cp = MornySystem.currentCodePath - if (cp == null) g++= s"$cm" - else g++= s"$cm" - if (!MornySystem.isCleanBuild) g++= ".δ" - g toString - } - - def getVersionAllFullTagHTML: String = { - val v = StringBuilder() - v ++= s"${MornySystem VERSION_BASE}" - if (MornySystem isUseDelta) v++=s"-δ${MornySystem VERSION_DELTA}" - if (MornySystem isGitBuild) v++="+git."++=getVersionGitTagHTML - v ++= s"*${MornySystem.CODENAME toUpperCase}" - v toString - } - - //noinspection ScalaWeakerAccess - def getRuntimeHostname: String|Null = { - try InetAddress.getLocalHost.getHostName - catch case _:UnknownHostException => null - } - - def getAboutPic: Array[Byte] = TelegramImages.IMG_ABOUT get - - def getMornyAboutLinksHTML: String = - s"""source code | backup - |反馈 / issue tracker - |使用说明书 / user guide & docs""" - .stripMargin - private def echoInfo (chatId: Long, replyTo: Int): Unit = { - MornyCoeur.extra exec new SendPhoto( + coeur.extra exec new SendPhoto( chatId, getAboutPic ).caption( @@ -128,15 +92,15 @@ object MornyInformation extends ITelegramCommand { val send_mid = SendMessage(send_chat, mid) val send_sticker = SendSticker(send_chat, file_id) if (send_replyTo != -1) send_mid.replyToMessageId(send_replyTo) - val result_send_mid = MornyCoeur.extra exec send_mid + val result_send_mid = coeur.extra exec send_mid send_sticker.replyToMessageId(result_send_mid.message.messageId) - MornyCoeur.extra exec send_sticker + coeur.extra exec send_sticker } private[command] def echoVersion (using event: Update): Unit = { val versionDeltaHTML = if (MornySystem.isUseDelta) s"-δ${h(MornySystem.VERSION_DELTA)}" else "" val versionGitHTML = if (MornySystem.isGitBuild) s"git $getVersionGitTagHTML" else "" - MornyCoeur.extra exec new SendMessage( + coeur.extra exec new SendMessage( event.message.chat.id, // language=html s"""version: @@ -153,7 +117,7 @@ object MornyInformation extends ITelegramCommand { private[command] def echoRuntime (using event: Update): Unit = { def sysprop (p: String): String = System.getProperty(p) - MornyCoeur.extra exec new SendMessage( + coeur.extra exec new SendMessage( event.message.chat.id, /* language=html */ s"""system: @@ -171,16 +135,16 @@ object MornyInformation extends ITelegramCommand { |- ${h(formatDate(BuildConfig.CODE_TIMESTAMP, 0))} [UTC] |- [${BuildConfig.CODE_TIMESTAMP}] |continuous: - |- ${h(formatDuration(System.currentTimeMillis - MornyCoeur.coeurStartTimestamp))} - |- [${System.currentTimeMillis - MornyCoeur.coeurStartTimestamp}] - |- ${h(formatDate(MornyCoeur.coeurStartTimestamp, 0))} - |- [${MornyCoeur.coeurStartTimestamp}]""" + |- ${h(formatDuration(System.currentTimeMillis - coeur.coeurStartTimestamp))} + |- [${System.currentTimeMillis - coeur.coeurStartTimestamp}] + |- ${h(formatDate(coeur.coeurStartTimestamp, 0))} + |- [${coeur.coeurStartTimestamp}]""" .stripMargin ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) } private def echo404 (using event: Update): Unit = - MornyCoeur.extra exec new SendSticker( + coeur.extra exec new SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala index cad2acf..60f94ec 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformationOlds.scala @@ -3,16 +3,16 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.Update -object MornyInformationOlds { +class MornyInformationOlds (using base: MornyInformation) { object Version extends ISimpleCommand: override val name: String = "version" override val aliases: Array[ICommandAlias] | Null = null - override def execute (using command: InputCommand, event: Update): Unit = MornyInformation.echoVersion + override def execute (using command: InputCommand, event: Update): Unit = base.echoVersion object Runtime extends ISimpleCommand: override val name: String = "runtime" override val aliases: Array[ICommandAlias] | Null = null - override def execute (using command: InputCommand, event: Update): Unit = MornyInformation.echoRuntime + override def execute (using command: InputCommand, event: Update): Unit = base.echoRuntime } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala index 8800c9f..05e0bed 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala @@ -11,7 +11,7 @@ import com.pengrad.telegrambot.request.SendSticker import scala.language.postfixOps -object MornyManagers { +class MornyManagers (using coeur: MornyCoeur) { object Exit extends ITelegramCommand { @@ -24,23 +24,23 @@ object MornyManagers { val user = event.message.from - if (MornyCoeur.trusted isTrusted user.id) { + if (coeur.trusted isTrusted user.id) { - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_EXIT ).replyToMessageId(event.message.messageId) logger info s"Morny exited by user ${user toLogTag}" - MornyCoeur.exit(0, user) + coeur.exit(0, user) } else { - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) logger info s"403 exit caught from user ${user toLogTag}" - MornyReport.unauthenticatedAction("/exit", user) + coeur.daemons.reporter.unauthenticatedAction("/exit", user) } @@ -59,23 +59,23 @@ object MornyManagers { val user = event.message.from - if (MornyCoeur.trusted isTrusted user.id) { + if (coeur.trusted isTrusted user.id) { logger info s"call save from command by ${user toLogTag}" - MornyCoeur.callSaveData() - MornyCoeur.extra exec SendSticker( + coeur.saveDataAll() + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_SAVED ).replyToMessageId(event.message.messageId) } else { - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) logger info s"403 save caught from user ${user toLogTag}" - MornyReport.unauthenticatedAction("/save", user) + coeur.daemons.reporter.unauthenticatedAction("/save", user) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala index c859f14..c468f32 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala @@ -7,8 +7,7 @@ import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendMessage -import scala.language.postfixOps -object MornyOldJrrp extends ITelegramCommand { +class MornyOldJrrp (using coeur: MornyCoeur) extends ITelegramCommand { override val name: String = "jrrp" override val aliases: Array[ICommandAlias] | Null = null @@ -25,7 +24,7 @@ object MornyOldJrrp extends ITelegramCommand { case _ => "..." import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, // language=html f"${user.fullnameRefHTML} 在(utc的)今天的运气指数是———— $jrrp%.2f%% ${h(ending)}" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala index 32e6819..2e0162e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala @@ -11,7 +11,7 @@ import com.pengrad.telegrambot.request.{SendMessage, SendSticker} import java.io.IOException import scala.language.postfixOps -object Nbnhhsh extends ITelegramCommand { +class Nbnhhsh (using coeur: MornyCoeur) extends ITelegramCommand { private val NBNHHSH_RESULT_HEAD_HTML = // language=html @@ -32,7 +32,7 @@ object Nbnhhsh extends ITelegramCommand { else null if (queryTarget == null) - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) @@ -67,13 +67,13 @@ object Nbnhhsh extends ITelegramCommand { logger debug s"**exec as ${_word.name}" } - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, message toString ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) } catch case e: IOException => { - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, s"""[Exception] in query: |${h(e.getMessage)} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala index 8316b42..808628c 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala @@ -6,18 +6,17 @@ import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendMessage -import javax.annotation.Nonnull -import javax.annotation.Nullable +import javax.annotation.{Nonnull, Nullable} import scala.language.postfixOps -object Testing extends ISimpleCommand { +class Testing (using coeur: MornyCoeur) extends ISimpleCommand { override val name: String = "test" override val aliases: Array[ICommandAlias] | Null = null override def execute (using command: InputCommand, event: Update): Unit = { - MornyCoeur.extra exec new SendMessage( + coeur.extra exec new SendMessage( event.message.chat.id, // language=html "Just a TEST command." diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala index 109077e..c75038b 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala @@ -12,7 +12,7 @@ import scala.annotation.unused import scala.language.postfixOps //noinspection NonAsciiCharacters -object 喵呜 { +class 喵呜 (using coeur: MornyCoeur) { object 抱抱 extends ISimpleCommand { override val name: String = "抱抱" @@ -48,7 +48,7 @@ object 喵呜 { override val paramRule: String = "" override val description: String = "抽取一个神秘盒子" override def execute (using command: InputCommand, event: Update): Unit = { - MornyCoeur.extra exec new SendSticker( + coeur.extra exec new SendSticker( event.message.chat.id, TelegramStickers ID_PROGYNOVA ).replyToMessageId(event.message.messageId) @@ -58,7 +58,7 @@ object 喵呜 { private def replyingSet (whileRec: String, whileNew: String)(using event: Update): Unit = { val isNew = event.message.replyToMessage == null val target = if (isNew) event.message else event.message.replyToMessage - MornyCoeur.extra exec new SendMessage( + coeur.extra exec new SendMessage( event.message.chat.id, if (isNew) whileNew else whileRec ).replyToMessageId(target.messageId).parseMode(ParseMode HTML) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala index 5321657..5ef39f5 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala @@ -8,7 +8,7 @@ import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage //noinspection NonAsciiCharacters -object 私わね extends ISimpleCommand { +class 私わね (using coeur: MornyCoeur) extends ISimpleCommand { override val name: String = "me" override val aliases: Array[ICommandAlias] | Null = null @@ -17,7 +17,7 @@ object 私わね extends ISimpleCommand { if ((1 over 521) chance_is true) { val text = "/打假" - MornyCoeur.extra exec new SendMessage( + coeur.extra exec new SendMessage( event.message.chat.id, text ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala index 4f39ac7..ea3149b 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala @@ -1,27 +1,21 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.bot.api.EventListenerManager +import cc.sukazyo.cono.morny.MornyCoeur -object MornyEventListeners { +class MornyEventListeners (using manager: EventListenerManager) (using coeur: MornyCoeur) { - def registerAllEvents(): Unit = { - - EventListenerManager.register( - // ACTIVITY_RECORDER - OnUpdateTimestampOffsetLock, - // KUOHUANHUAN_NEED_SLEEP - OnTelegramCommand, - OnUniMeowTrigger, - OnUserRandom, - OnQuestionMarkReply, - OnUserSlashAction, - OnInlineQuery, - OnCallMe, - OnCallMsgSend, - OnMedicationNotifyApply, - OnEventHackHandle - ) - - } + manager.register( + // ACTIVITY_RECORDER + // KUOHUANHUAN_NEED_SLEEP + OnUniMeowTrigger(using coeur.commands), + OnUserRandom(), + OnQuestionMarkReply(), + OnUserSlashAction(), + OnCallMe(), + OnCallMsgSend(), + OnMedicationNotifyApply(), + OnEventHackHandle() + ) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala similarity index 82% rename from src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala rename to src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala index d4df473..90858bb 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnInlineQuery.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala @@ -11,11 +11,11 @@ import scala.collection.mutable.ListBuffer import scala.language.postfixOps import scala.reflect.ClassTag -object OnInlineQuery extends EventListener { +class MornyOnInlineQuery (using queryManager: MornyQueries) (using coeur: MornyCoeur) extends EventListener { override def onInlineQuery (using update: Update): Boolean = { - val results: List[InlineQueryUnit[_]] = MornyQueries query update + val results: List[InlineQueryUnit[_]] = queryManager query update var cacheTime = Int.MaxValue var isPersonal = InlineQueryUnit.defaults.IS_PERSONAL @@ -28,7 +28,7 @@ object OnInlineQuery extends EventListener { if (results isEmpty) return false - MornyCoeur.extra exec AnswerInlineQuery( + coeur.extra exec AnswerInlineQuery( update.inlineQuery.id, resultAnswers toArray:_* ).cacheTime(cacheTime).isPersonal(isPersonal) true diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnTelegramCommand.scala similarity index 84% rename from src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala rename to src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnTelegramCommand.scala index a850f21..541070b 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnTelegramCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnTelegramCommand.scala @@ -7,7 +7,7 @@ import cc.sukazyo.cono.morny.bot.command.MornyCommands import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.{Message, Update} -object OnTelegramCommand extends EventListener { +class MornyOnTelegramCommand (using commandManager: MornyCommands) (using coeur: MornyCoeur) extends EventListener { override def onMessage (using update: Update): Boolean = { @@ -22,12 +22,12 @@ object OnTelegramCommand extends EventListener { if (!(inputCommand.command matches "^\\w+$")) logger debug "not command" false - else if ((inputCommand.target ne null) && (inputCommand.target != MornyCoeur.username)) + else if ((inputCommand.target ne null) && (inputCommand.target != coeur.username)) logger debug "not morny command" false else logger debug "is command" - MornyCommands.execute(using inputCommand) + commandManager.execute(using inputCommand) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnUpdateTimestampOffsetLock.scala similarity index 81% rename from src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.scala rename to src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnUpdateTimestampOffsetLock.scala index a1978a8..89f09cd 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUpdateTimestampOffsetLock.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnUpdateTimestampOffsetLock.scala @@ -4,10 +4,10 @@ import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.MornyCoeur import com.pengrad.telegrambot.model.Update -object OnUpdateTimestampOffsetLock extends EventListener { +class MornyOnUpdateTimestampOffsetLock (using coeur: MornyCoeur) extends EventListener { private def isOutdated (timestamp: Int): Boolean = - timestamp < (MornyCoeur.config.eventOutdatedTimestamp/1000) + timestamp < (coeur.config.eventOutdatedTimestamp/1000) override def onMessage (using update: Update): Boolean = isOutdated(update.message.date) override def onEditedMessage (using update: Update): Boolean = isOutdated(update.editedMessage.date) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala index 07f4d96..c42caf9 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -10,9 +10,9 @@ import com.pengrad.telegrambot.request.{ForwardMessage, GetChat, SendMessage, Se import scala.language.postfixOps -object OnCallMe extends EventListener { +class OnCallMe (using coeur: MornyCoeur) extends EventListener { - private val me = MornyCoeur.config.trustedMaster + private val me = coeur.config.trustedMaster override def onMessage (using update: Update): Boolean = { @@ -32,7 +32,7 @@ object OnCallMe extends EventListener { case _ => return false - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( update.message.chat.id, TelegramStickers ID_SENT ).replyToMessageId(update.message.messageId) @@ -41,7 +41,7 @@ object OnCallMe extends EventListener { } private def requestItem (user: User, itemHTML: String, extra: String|Null = null): Unit = - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( me, s"""request $itemHTML |from ${user.fullnameRefHTML}${if extra == null then "" else "\n"+extra}""" @@ -51,11 +51,11 @@ object OnCallMe extends EventListener { private def requestLastDinner (req: Message): Unit = { var isAllowed = false var lastDinnerData: Message|Null = null - if (MornyCoeur.trusted isTrusted_dinnerReader req.from.id) { + if (coeur.trusted isTrusted_dinnerReader req.from.id) { // todo: have issues - // i dont want to test it anymore... it might be deprecated soon - lastDinnerData = (MornyCoeur.extra exec GetChat(MornyCoeur.config.dinnerChatId)).chat.pinnedMessage - val sendResp = MornyCoeur.extra exec ForwardMessage( + // i dont want to test it anymore... it might be deprecated soon + lastDinnerData = (coeur.extra exec GetChat(coeur.config.dinnerChatId)).chat.pinnedMessage + val sendResp = coeur.extra exec ForwardMessage( req.from.id, lastDinnerData.forwardFromChat.id, lastDinnerData.forwardFromMessageId @@ -63,7 +63,7 @@ object OnCallMe extends EventListener { import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h def lastDinner_dateMillis: Long = lastDinnerData.forwardDate longValue; - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( req.from.id, "on %s [UTC+8]\n- %s before".formatted( h(formatDate(lastDinner_dateMillis, 8)), @@ -72,7 +72,7 @@ object OnCallMe extends EventListener { ).parseMode(ParseMode HTML).replyToMessageId(sendResp.message.messageId) isAllowed = true } else { - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( req.from.id, TelegramStickers ID_403 ).replyToMessageId(req.messageId) @@ -87,6 +87,6 @@ object OnCallMe extends EventListener { private def requestCustom (message: Message): Unit = requestItem(message.from, "[???]") - MornyCoeur.extra exec ForwardMessage(me, message.chat.id, message.messageId) + coeur.extra exec ForwardMessage(me, message.chat.id, message.messageId) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala index 1065a29..7544c6b 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala @@ -11,7 +11,7 @@ import scala.collection.mutable.ArrayBuffer import scala.language.postfixOps import scala.util.matching.Regex -object OnCallMsgSend extends EventListener { +class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { private val REGEX_MSG_SENDREQ_DATA_HEAD: Regex = "^\\*msg(-?\\d+)(\\*\\S+)?(?:\\n([\\s\\S]+))?$"r @@ -59,8 +59,8 @@ object OnCallMsgSend extends EventListener { if message.text eq null then return false if !(message.text startsWith "*msg") then return false - if (!(MornyCoeur.trusted isTrusted message.from.id)) - MornyCoeur.extra exec SendSticker( + if (!(coeur.trusted isTrusted message.from.id)) + coeur.extra exec SendSticker( message.chat.id, TelegramStickers ID_403 ).replyToMessageId(message.messageId) @@ -71,15 +71,15 @@ object OnCallMsgSend extends EventListener { if (message.replyToMessage eq null) return answer404 val messageToSend = MessageToSend from message.replyToMessage if ((messageToSend eq null) || (messageToSend.message eq null)) return answer404 - val sendResponse = MornyCoeur.account execute messageToSend.toSendMessage() + val sendResponse = coeur.account execute messageToSend.toSendMessage() if (sendResponse isOk) { - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( update.message.chat.id, TelegramStickers ID_SENT ).replyToMessageId(update.message.messageId) } else { - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( update.message.chat.id, // language=html s"""${sendResponse.errorCode} FAILED @@ -104,7 +104,7 @@ object OnCallMsgSend extends EventListener { if _toSend eq null then return answer404 else _toSend - val targetChatResponse = MornyCoeur.account execute GetChat(messageToSend.targetId) + val targetChatResponse = coeur.account execute GetChat(messageToSend.targetId) if (targetChatResponse isOk) { def getChatDescriptionHTML (chat: Chat): String = import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* @@ -113,12 +113,12 @@ object OnCallMsgSend extends EventListener { s"""${h(chat.id toString)}@${h(chat.`type`.name)}${if (chat.`type` != Chat.Type.Private) ":::" else ""} |${chat.typeTag} ${h(chat.safe_name)} ${chat.safe_linkHTML}""" .stripMargin - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( update.message.chat.id, getChatDescriptionHTML(targetChatResponse.chat) ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) } else { - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( update.message.chat.id, // language=html s"""${targetChatResponse.errorCode} FAILED @@ -128,10 +128,10 @@ object OnCallMsgSend extends EventListener { } if messageToSend.message eq null then return true - val testSendResponse = MornyCoeur.account execute messageToSend.toSendMessage(update.message.chat.id) + val testSendResponse = coeur.account execute messageToSend.toSendMessage(update.message.chat.id) .replyToMessageId(update.message.messageId) if (!(testSendResponse isOk)) - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( update.message.chat.id, // language=html s"""${testSendResponse.errorCode} FAILED @@ -144,7 +144,7 @@ object OnCallMsgSend extends EventListener { } private def answer404 (using update: Update): Boolean = - MornyCoeur.extra exec SendSticker( + coeur.extra exec SendSticker( update.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(update.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala index f42d9e9..d6bc1f6 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnEventHackHandle.scala @@ -11,69 +11,37 @@ import com.pengrad.telegrambot.request.SendMessage import scala.collection.mutable import scala.language.postfixOps -object OnEventHackHandle extends EventListener { +class OnEventHackHandle (using coeur: MornyCoeur) extends EventListener { - private case class Hacker (from_chat: Long, from_message: Long): - override def toString: String = s"$from_chat/$from_message" - enum HackType: - case USER - case GROUP - case ANY - - private val hackers = mutable.HashMap.empty[String, Hacker] - - def registerHack (from_message: Long, from_user: Long, from_chat: Long, t: HackType): Unit = - val record = t match - case HackType.USER => s"(($from_user))" - case HackType.GROUP => s"{{$from_chat}}" - case HackType.ANY => "[[]]" - hackers += (record -> Hacker(from_chat, from_message)) - logger debug s"add hacker track $record" - - private def onEventHacked (chat: Long, fromUser: Long)(using update: Update): Boolean = { - logger debug s"got event signed {{$chat}}(($fromUser))" - val x: Hacker = - if hackers contains s"(($fromUser))" then (hackers remove s"(($fromUser))")get - else if hackers contains s"{{$chat}}" then (hackers remove s"{{$chat}}")get - else if hackers contains "[[]]" then (hackers remove "[[]]")get - else return false - logger debug s"hacked event by $x" - import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h - MornyCoeur.extra exec SendMessage( - x.from_chat, - // language=html - s"${h(GsonBuilder().setPrettyPrinting().create.toJson(update))}" - ).parseMode(ParseMode HTML).replyToMessageId(x.from_message toInt) - true - } + import coeur.daemons.eventHack.trigger override def onMessage (using update: Update): Boolean = - onEventHacked(update.message.chat.id, update.message.from.id) + trigger(update.message.chat.id, update.message.from.id) override def onEditedMessage (using update: Update): Boolean = - onEventHacked(update.editedMessage.chat.id, update.editedMessage.from.id) + trigger(update.editedMessage.chat.id, update.editedMessage.from.id) override def onChannelPost (using update: Update): Boolean = - onEventHacked(update.channelPost.chat.id, 0) + trigger(update.channelPost.chat.id, 0) override def onEditedChannelPost (using update: Update): Boolean = - onEventHacked(update.editedChannelPost.chat.id, 0) + trigger(update.editedChannelPost.chat.id, 0) override def onInlineQuery (using update: Update): Boolean = - onEventHacked(0, update.inlineQuery.from.id) + trigger(0, update.inlineQuery.from.id) override def onChosenInlineResult (using update: Update): Boolean = - onEventHacked(0, update.chosenInlineResult.from.id) + trigger(0, update.chosenInlineResult.from.id) override def onCallbackQuery (using update: Update): Boolean = - onEventHacked(0, update.callbackQuery.from.id) + trigger(0, update.callbackQuery.from.id) override def onShippingQuery (using update: Update): Boolean = - onEventHacked(0, update.shippingQuery.from.id) + trigger(0, update.shippingQuery.from.id) override def onPreCheckoutQuery (using update: Update): Boolean = - onEventHacked(0, update.preCheckoutQuery.from.id) + trigger(0, update.preCheckoutQuery.from.id) override def onPoll (using update: Update): Boolean = - onEventHacked(0, 0) + trigger(0, 0) override def onPollAnswer (using update: Update): Boolean = - onEventHacked(0, update.pollAnswer.user.id) + trigger(0, update.pollAnswer.user.id) override def onMyChatMemberUpdated (using update: Update): Boolean = - onEventHacked(update.myChatMember.chat.id, update.myChatMember.from.id) + trigger(update.myChatMember.chat.id, update.myChatMember.from.id) override def onChatMemberUpdated (using update: Update): Boolean = - onEventHacked(update.chatMember.chat.id, update.chatMember.from.id) + trigger(update.chatMember.chat.id, update.chatMember.from.id) override def onChatJoinRequest (using update: Update): Boolean = - onEventHacked(update.chatJoinRequest.chat.id, update.chatJoinRequest.from.id) + trigger(update.chatJoinRequest.chat.id, update.chatJoinRequest.from.id) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala index 4e90c9b..25b29c1 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnMedicationNotifyApply.scala @@ -5,7 +5,7 @@ import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.daemon.{MedicationTimer, MornyDaemons} import com.pengrad.telegrambot.model.{Message, Update} -object OnMedicationNotifyApply extends EventListener { +class OnMedicationNotifyApply (using coeur: MornyCoeur) extends EventListener { override def onEditedMessage (using event: Update): Boolean = editedMessageProcess(event.editedMessage) @@ -13,8 +13,8 @@ object OnMedicationNotifyApply extends EventListener { editedMessageProcess(event.editedChannelPost) private def editedMessageProcess (edited: Message): Boolean = { - if edited.chat.id != MornyCoeur.config.medicationNotifyToChat then return false - MedicationTimer.refreshNotificationWrite(edited) + if edited.chat.id != coeur.config.medicationNotifyToChat then return false + coeur.daemons.medicationTimer.refreshNotificationWrite(edited) true } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala index 3d73a09..9010871 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala @@ -2,21 +2,13 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.event.OnQuestionMarkReply.isAllMessageMark import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage import scala.language.postfixOps -object OnQuestionMarkReply extends EventListener { - - private val QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓') - - def isAllMessageMark (using text: String): Boolean = { - var isAll = true - for (c <- text) - if !(QUESTION_MARKS contains c) then isAll = false - isAll - } +class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener { override def onMessage (using event: Update): Boolean = { @@ -27,7 +19,7 @@ object OnQuestionMarkReply extends EventListener { if (1 over 8) chance_is false then return false if !isAllMessageMark(using event.message.text) then return false - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( event.message.chat.id, event.message.text ).replyToMessageId(event.message.messageId) true @@ -35,3 +27,16 @@ object OnQuestionMarkReply extends EventListener { } } + +object OnQuestionMarkReply { + + private val QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓') + + def isAllMessageMark (using text: String): Boolean = { + var isAll = true + for (c <- text) + if !(QUESTION_MARKS contains c) then isAll = false + isAll + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala index c482b63..d1c97da 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUniMeowTrigger.scala @@ -3,15 +3,16 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.bot.command.MornyCommands import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.MornyCoeur import com.pengrad.telegrambot.model.Update -object OnUniMeowTrigger extends EventListener { +class OnUniMeowTrigger (using commands: MornyCommands) (using coeur: MornyCoeur) extends EventListener { override def onMessage (using update: Update): Boolean = { if update.message.text eq null then return false var ok = false - for ((name, command) <- MornyCommands.commands_uni) + for ((name, command) <- commands.commands_uni) val _name = "/"+name if (_name == update.message.text) command.execute(using InputCommand(_name)) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala index e47f3ce..2237630 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala @@ -7,7 +7,7 @@ import com.pengrad.telegrambot.request.SendMessage import scala.language.postfixOps -object OnUserRandom extends EventListener { +class OnUserRandom (using coeur: MornyCoeur) extends EventListener { private val USER_OR_QUERY = "^(.+)(?:还是|or)(.+)$"r private val USER_IF_QUERY = "^(.+)(?:吗\\?|?|\\?|吗?)$"r @@ -30,7 +30,7 @@ object OnUserRandom extends EventListener { if result == null then return false - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( update.message.chat.id, result ).replyToMessageId(update.message.messageId) true diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala index db80040..033054d 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala @@ -11,7 +11,7 @@ import com.pengrad.telegrambot.request.SendMessage import scala.language.postfixOps -object OnUserSlashAction extends EventListener { +class OnUserSlashAction (using coeur: MornyCoeur) extends EventListener { private val TG_FORMAT = "^\\w+(@\\w+)?$"r @@ -58,7 +58,7 @@ object OnUserSlashAction extends EventListener { origin else update.message.replyToMessage - MornyCoeur.extra exec SendMessage( + coeur.extra exec SendMessage( update.message.chat.id, "%s %s%s %s %s!".format( origin.sender_firstnameRefHTML, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala index 70c4739..a7a2b99 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ITelegramQuery.scala @@ -1,10 +1,12 @@ package cc.sukazyo.cono.morny.bot.query -import javax.annotation.Nullable +import cc.sukazyo.cono.morny.MornyCoeur import com.pengrad.telegrambot.model.Update +import javax.annotation.Nullable + trait ITelegramQuery { def query (event: Update): List[InlineQueryUnit[_]] | Null -} \ No newline at end of file +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala index 0c06dc4..11c6a12 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala @@ -1,17 +1,18 @@ package cc.sukazyo.cono.morny.bot.query import cc.sukazyo.cono.morny.bot.query +import cc.sukazyo.cono.morny.MornyCoeur import com.pengrad.telegrambot.model.Update import scala.collection.mutable.ListBuffer -object MornyQueries { +class MornyQueries (using MornyCoeur) { private val queryInstances = Set[ITelegramQuery]( - RawText, - MyInformation, - ShareToolTwitter, - ShareToolBilibili + RawText(), + MyInformation(), + ShareToolTwitter(), + ShareToolBilibili() ) def query (event: Update): List[InlineQueryUnit[_]] = { diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala index 2c8aa58..1b27068 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MyInformation.scala @@ -7,7 +7,7 @@ import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTex import scala.language.postfixOps -object MyInformation extends ITelegramQuery { +class MyInformation extends ITelegramQuery { private val ID_PREFIX = "[morny/info/me]" private val TITLE = "My Account Information" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala index 4fe13c8..036fb1d 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/RawText.scala @@ -5,7 +5,7 @@ import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTex import scala.language.postfixOps -object RawText extends ITelegramQuery { +class RawText extends ITelegramQuery { private val ID_PREFIX = "[morny/r/text]" private val TITLE = "Raw Text" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala index 7b2f750..a74aad2 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala @@ -9,7 +9,7 @@ import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTex import scala.language.postfixOps import scala.util.matching.Regex -object ShareToolBilibili extends ITelegramQuery { +class ShareToolBilibili extends ITelegramQuery { private val TITLE_BILI_AV = "[bilibili] Share video / av" private val TITLE_BILI_BV = "[bilibili] Share video / BV" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala index baff446..03ba0de 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala @@ -7,7 +7,7 @@ import com.pengrad.telegrambot.model.request.InlineQueryResultArticle import scala.language.postfixOps import scala.util.matching.Regex -object ShareToolTwitter extends ITelegramQuery { +class ShareToolTwitter extends ITelegramQuery { private val TITLE_VX = "[tweet] Share as VxTwitter" private val TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)" diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala new file mode 100644 index 0000000..b20e72d --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala @@ -0,0 +1,49 @@ +package cc.sukazyo.cono.morny.daemon + +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur +import com.google.gson.GsonBuilder +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.SendMessage + +import scala.collection.mutable + +class EventHacker (using coeur: MornyCoeur) { + + private case class Hacker (from_chat: Long, from_message: Long): + override def toString: String = s"$from_chat/$from_message" + + enum HackType: + case USER + case GROUP + case ANY + + private val hackers = mutable.HashMap.empty[String, Hacker] + + def registerHack (from_message: Long, from_user: Long, from_chat: Long, t: HackType): Unit = + val record = t match + case HackType.USER => s"(($from_user))" + case HackType.GROUP => s"{{$from_chat}}" + case HackType.ANY => "[[]]" + hackers += (record -> Hacker(from_chat, from_message)) + logger debug s"add hacker track $record" + + def trigger (chat: Long, fromUser: Long)(using update: Update): Boolean = { + logger debug s"got event signed {{$chat}}(($fromUser))" + val x: Hacker = + if hackers contains s"(($fromUser))" then (hackers remove s"(($fromUser))") get + else if hackers contains s"{{$chat}}" then (hackers remove s"{{$chat}}") get + else if hackers contains "[[]]" then (hackers remove "[[]]") get + else return false + logger debug s"hacked event by $x" + import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + coeur.extra exec SendMessage( + x.from_chat, + // language=html + s"${h(GsonBuilder().setPrettyPrinting().create.toJson(update))}" + ).parseMode(ParseMode HTML).replyToMessageId(x.from_message toInt) + true + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala index 644abe2..880b557 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala @@ -1,7 +1,8 @@ package cc.sukazyo.cono.morny.daemon -import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.daemon.MedicationTimer.calcNextRoutineTimestamp import com.pengrad.telegrambot.model.{Message, MessageEntity} import com.pengrad.telegrambot.request.{EditMessageText, SendMessage} import com.pengrad.telegrambot.response.SendResponse @@ -10,15 +11,15 @@ import java.time.{LocalDateTime, ZoneOffset} import scala.collection.mutable.ArrayBuffer import scala.language.implicitConversions -object MedicationTimer extends Thread { +class MedicationTimer (using coeur: MornyCoeur) extends Thread { private val NOTIFY_MESSAGE = "🍥⏲" private val DAEMON_THREAD_NAME_DEF = "MedicationTimer" - private val use_timeZone = MornyCoeur.config.medicationTimerUseTimezone + private val use_timeZone = coeur.config.medicationTimerUseTimezone import scala.jdk.CollectionConverters.SetHasAsScala - private val notify_atHour: Set[Int] = MornyCoeur.config.medicationNotifyAt.asScala.toSet.map(_.intValue) - private val notify_toChat = MornyCoeur.config.medicationNotifyToChat + private val notify_atHour: Set[Int] = coeur.config.medicationNotifyAt.asScala.toSet.map(_.intValue) + private val notify_toChat = coeur.config.medicationNotifyToChat this.setName(DAEMON_THREAD_NAME_DEF) @@ -42,17 +43,22 @@ object MedicationTimer extends Thread { s"""unexpected error occurred on NotificationTimer |${exceptionLog(e)}""" .stripMargin - MornyReport.exception(e) + coeur.daemons.reporter.exception(e) } logger info "Medication Timer stopped." } private def sendNotification(): Unit = { - val sendResponse: SendResponse = MornyCoeur.extra exec SendMessage(notify_toChat, NOTIFY_MESSAGE) + val sendResponse: SendResponse = coeur.extra exec SendMessage(notify_toChat, NOTIFY_MESSAGE) if sendResponse isOk then lastNotify_messageId = sendResponse.message.messageId else lastNotify_messageId = null } + @throws[InterruptedException | IllegalArgumentException] + private def waitToNextRoutine (): Unit = { + Thread sleep calcNextRoutineTimestamp(System.currentTimeMillis, use_timeZone, notify_atHour) + } + def refreshNotificationWrite (edited: Message): Unit = { if lastNotify_messageId != (edited.messageId toInt) then return import cc.sukazyo.cono.morny.util.CommonFormat.formatDate @@ -60,7 +66,7 @@ object MedicationTimer extends Thread { val entities = ArrayBuffer.empty[MessageEntity] if edited.entities ne null then entities ++= edited.entities entities += MessageEntity(MessageEntity.Type.italic, edited.text.length + "\n-- ".length, editTime.length) - MornyCoeur.extra exec EditMessageText( + coeur.extra exec EditMessageText( notify_toChat, edited.messageId, edited.text + s"\n-- $editTime --" @@ -68,23 +74,22 @@ object MedicationTimer extends Thread { lastNotify_messageId = null } +} + +object MedicationTimer { + @throws[IllegalArgumentException] private[daemon] def calcNextRoutineTimestamp (baseTimeMillis: Long, zone: ZoneOffset, notifyAt: Set[Int]): Long = { if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set") var time = LocalDateTime.ofEpochSecond( - baseTimeMillis/1000, ((baseTimeMillis%1000)*1000*1000) toInt, + baseTimeMillis / 1000, ((baseTimeMillis % 1000) * 1000 * 1000) toInt, zone ).withMinute(0).withSecond(0).withNano(0) time = time plusHours 1 - while (!(notifyAt contains (time getHour))) { + while (!(notifyAt contains(time getHour))) { time = time plusHours 1 } (time toInstant zone) toEpochMilli } - @throws[InterruptedException | IllegalArgumentException] - private def waitToNextRoutine (): Unit = { - Thread sleep calcNextRoutineTimestamp(System.currentTimeMillis, use_timeZone, notify_atHour) - } - } diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala index 95c9415..3cf175e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala @@ -3,13 +3,17 @@ package cc.sukazyo.cono.morny.daemon import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.MornyCoeur -object MornyDaemons { +class MornyDaemons (using val coeur: MornyCoeur) { + + val medicationTimer: MedicationTimer = MedicationTimer() + val reporter: MornyReport = MornyReport() + val eventHack: EventHacker = EventHacker() def start (): Unit = { logger info "ALL Morny Daemons starting..." // TrackerDataManager.init(); - MedicationTimer.start() - MornyReport.onMornyLogin() + medicationTimer.start() + reporter.onMornyLogin() logger info "Morny Daemons started." } @@ -17,12 +21,12 @@ object MornyDaemons { def stop (): Unit = { logger.info("ALL Morny Daemons stopping...") // TrackerDataManager.DAEMON.interrupt(); - MedicationTimer.interrupt() + medicationTimer.interrupt() // TrackerDataManager.trackingLock.lock(); - try { MedicationTimer.join() } + try { medicationTimer.join() } catch case e: InterruptedException => e.printStackTrace(System.out) - MornyReport.onMornyExit(MornyCoeur.exitReason) + reporter.onMornyExit() logger.info("ALL Morny Daemons STOPPED.") } diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala index 7d39431..803bd2e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.daemon import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig} import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.bot.command.MornyInformation +import cc.sukazyo.cono.morny.data.MornyInformation.getVersionAllFullTagHTML import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h @@ -12,14 +13,11 @@ import com.pengrad.telegrambot.model.User import com.pengrad.telegrambot.request.{BaseRequest, SendMessage} import com.pengrad.telegrambot.response.BaseResponse -object MornyReport { - - private def unsupported: Boolean = (!MornyCoeur.available) || (MornyCoeur.config.reportToChat == -1) +class MornyReport (using coeur: MornyCoeur) { private def executeReport[T <: BaseRequest[T, R], R<: BaseResponse] (report: T): Unit = { - if unsupported then return try { - MornyCoeur.extra exec report + coeur.extra exec report } catch case e: EventRuntimeException.ActionFailed => { logger warn s"""cannot execute report to telegram: @@ -31,7 +29,6 @@ object MornyReport { } def exception (e: Throwable, description: String|Null = null): Unit = { - if unsupported then return def _tgErrFormat: String = e match case api: EventRuntimeException.ActionFailed => // language=html @@ -39,7 +36,7 @@ object MornyReport { .formatted(GsonBuilder().setPrettyPrinting().create.toJson(api.response)) case _ => "" executeReport(SendMessage( - MornyCoeur.config.reportToChat, + coeur.config.reportToChat, // language=html s"""▌Coeur Unexpected Exception |${if description ne null then h(description)+"\n" else ""} @@ -49,9 +46,8 @@ object MornyReport { } def unauthenticatedAction (action: String, user: User): Unit = { - if unsupported then return executeReport(SendMessage( - MornyCoeur.config.reportToChat, + coeur.config.reportToChat, // language=html s"""▌User unauthenticated action |action: ${h(action)} @@ -62,14 +58,14 @@ object MornyReport { def onMornyLogin(): Unit = { executeReport(SendMessage( - MornyCoeur.config.reportToChat, + coeur.config.reportToChat, // language=html s"""▌Morny Logged in - |-v ${MornyInformation.getVersionAllFullTagHTML} - |as user ${MornyCoeur.username} + |-v $getVersionAllFullTagHTML + |as user ${coeur.username} | |as config fields: - |${sectionConfigFields(MornyCoeur.config)}""" + |${sectionConfigFields(coeur.config)}""" .stripMargin ).parseMode(ParseMode HTML)) } @@ -103,17 +99,16 @@ object MornyReport { echo dropRight 1 toString } - def onMornyExit (causedBy: AnyRef|Null): Unit = { - if unsupported then return - val causedTag = causedBy match + def onMornyExit (): Unit = { + val causedTag = coeur.exitReason match case u: User => u.fullnameRefHTML case n if n == null => "UNKNOWN reason" case a: AnyRef => /*language=html*/ s"${h(a.toString)}" executeReport(SendMessage( - MornyCoeur.config.reportToChat, + coeur.config.reportToChat, // language=html s"""▌Morny Exited - |from user @${MornyCoeur.username} + |from user @${coeur.username} | |by: $causedTag""" .stripMargin diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala new file mode 100644 index 0000000..12e7671 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala @@ -0,0 +1,45 @@ +package cc.sukazyo.cono.morny.data + +import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornySystem} + +import java.net.InetAddress +import java.rmi.UnknownHostException + +object MornyInformation { + + //noinspection ScalaWeakerAccess + def getVersionGitTagHTML: String = { + if (!MornySystem.isGitBuild) return "" + val g = StringBuilder() + val cm = BuildConfig.COMMIT substring(0, 8) + val cp = MornySystem.currentCodePath + if (cp == null) g ++= s"$cm" + else g ++= s"$cm" + if (!MornySystem.isCleanBuild) g ++= ".δ" + g toString + } + + def getVersionAllFullTagHTML: String = { + val v = StringBuilder() + v ++= s"${MornySystem VERSION_BASE}" + if (MornySystem isUseDelta) v ++= s"-δ${MornySystem VERSION_DELTA}" + if (MornySystem isGitBuild) v ++= "+git." ++= getVersionGitTagHTML + v ++= s"*${MornySystem.CODENAME toUpperCase}" + v toString + } + + //noinspection ScalaWeakerAccess + def getRuntimeHostname: String | Null = { + try InetAddress.getLocalHost.getHostName + catch case _: UnknownHostException => null + } + + def getAboutPic: Array[Byte] = TelegramImages.IMG_ABOUT get + + def getMornyAboutLinksHTML: String = + s"""source code | backup + |反馈 / issue tracker + |使用说明书 / user guide & docs""" + .stripMargin + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala index 667d5d3..6fb7757 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala @@ -1,12 +1,13 @@ package cc.sukazyo.cono.morny.data +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.MornyAssets +import cc.sukazyo.cono.morny.daemon.MornyReport +import cc.sukazyo.cono.morny.MornyAssets.AssetsException +import java.io.IOException import scala.language.postfixOps import scala.util.Using -import java.io.IOException -import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} -import cc.sukazyo.cono.morny.daemon.MornyReport object TelegramImages { @@ -14,19 +15,17 @@ object TelegramImages { private var cache: Array[Byte]|Null = _ + @throws[AssetsException] def get:Array[Byte] = if cache eq null then read() - if cache eq null then throw IllegalStateException("Failed to get assets file image.") cache + @throws[AssetsException] private def read (): Unit = { Using ((MornyAssets.pack getResource assetsPath)read) { stream => try { this.cache = stream.readAllBytes() } catch case e: IOException => { - logger error - s"""Cannot read resource file: - |${exceptionLog(e)}""".stripMargin - MornyReport.exception(e, "Cannot read resource file.") + throw AssetsException(e) } } } From bfacb0d039f46df5bd22e4e276740e40111dfbb6 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 23 Sep 2023 21:44:06 +0800 Subject: [PATCH 35/40] refactor ExtraAction to TelegramExtensions, some coeur startup changes - refactor ExtraAction to TelegramExtensions - make exec a TelegramBot extension - refactor isUserInGroup to Chat extension hasMember and memberHasPermission - added LimBoUser and LimboChat for capability - deleted TelegramUpdatesListener, and make EventListenerManager implemented UpdatesListener - change some value definition, change startup and exit process in MornyCoeur - removed updatesListener, extract its manager to eventManager in Coeur - added account.shutdown before other's - moved Morny's startup/exit report to Coeur - change exitCleanup thread name from "morny-exiting" to "system-exit" --- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/MornyCoeur.scala | 30 ++++--- .../cc/sukazyo/cono/morny/MornyTrusted.scala | 6 +- .../cono/morny/bot/api/EventListener.scala | 3 +- .../morny/bot/api/EventListenerManager.scala | 12 ++- .../bot/api/TelegramUpdatesListener.scala | 20 ----- .../morny/bot/command/DirectMsgClear.scala | 9 +- .../cono/morny/bot/command/Encryptor.scala | 19 ++-- .../cono/morny/bot/command/EventHack.scala | 5 +- .../morny/bot/command/GetUsernameAndId.scala | 11 +-- .../cono/morny/bot/command/IP186Query.scala | 9 +- .../morny/bot/command/MornyCommands.scala | 7 +- .../cono/morny/bot/command/MornyHellos.scala | 5 +- .../morny/bot/command/MornyInfoOnStart.scala | 3 +- .../morny/bot/command/MornyInformation.scala | 13 +-- .../morny/bot/command/MornyManagers.scala | 9 +- .../cono/morny/bot/command/MornyOldJrrp.scala | 3 +- .../cono/morny/bot/command/Nbnhhsh.scala | 7 +- .../cono/morny/bot/command/Testing.scala | 3 +- .../sukazyo/cono/morny/bot/command/喵呜.scala | 5 +- .../cono/morny/bot/command/私わね.scala | 3 +- .../morny/bot/event/MornyOnInlineQuery.scala | 3 +- .../cono/morny/bot/event/OnCallMe.scala | 15 ++-- .../cono/morny/bot/event/OnCallMsgSend.scala | 15 ++-- .../morny/bot/event/OnQuestionMarkReply.scala | 3 +- .../cono/morny/bot/event/OnUserRandom.scala | 3 +- .../morny/bot/event/OnUserSlashAction.scala | 3 +- .../cono/morny/daemon/EventHacker.scala | 3 +- .../cono/morny/daemon/MedicationTimer.scala | 5 +- .../cono/morny/daemon/MornyDaemons.scala | 6 +- .../cono/morny/daemon/MornyReport.scala | 8 +- .../cono/morny/util/tgapi/ExtraAction.java | 88 ------------------- .../morny/util/tgapi/TelegramExtensions.scala | 68 ++++++++++++++ 33 files changed, 199 insertions(+), 205 deletions(-) delete mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala delete mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/tgapi/TelegramExtensions.scala diff --git a/gradle.properties b/gradle.properties index 46a5b30..3d87307 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s VERSION = 1.0.0-RC5 USE_DELTA = true -VERSION_DELTA = scala2 +VERSION_DELTA = scala3 CODENAME = beiping diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala index 4463678..7df72aa 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -3,11 +3,10 @@ package cc.sukazyo.cono.morny import cc.sukazyo.cono.morny.bot.command.MornyCommands import cc.sukazyo.cono.morny.daemon.MornyDaemons import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} -import cc.sukazyo.cono.morny.MornyCoeur.THREAD_MORNY_EXIT -import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener +import cc.sukazyo.cono.morny.MornyCoeur.THREAD_SERVER_EXIT +import cc.sukazyo.cono.morny.bot.api.EventListenerManager import cc.sukazyo.cono.morny.bot.event.{MornyEventListeners, MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock} import cc.sukazyo.cono.morny.bot.query.MornyQueries -import cc.sukazyo.cono.morny.util.tgapi.ExtraAction import com.pengrad.telegrambot.TelegramBot import com.pengrad.telegrambot.request.GetMe @@ -16,7 +15,7 @@ import scala.util.boundary.break object MornyCoeur { - val THREAD_MORNY_EXIT = "morny-exiting" + val THREAD_SERVER_EXIT = "system-exit" } @@ -50,18 +49,18 @@ class MornyCoeur (using val config: MornyConfig) { /** current Morny's [[MornyTrusted]] instance */ val trusted: MornyTrusted = MornyTrusted() - /** current Morny's [[ExtraAction]] toolset */ - val extra: ExtraAction = ExtraAction as __loginResult.account - private val updatesListener: TelegramUpdatesListener = TelegramUpdatesListener() val daemons: MornyDaemons = MornyDaemons() - updatesListener.manager register MornyOnUpdateTimestampOffsetLock() + //noinspection ScalaWeakerAccess + val eventManager: EventListenerManager = EventListenerManager() + eventManager register MornyOnUpdateTimestampOffsetLock() val commands: MornyCommands = MornyCommands() //noinspection ScalaWeakerAccess val queries: MornyQueries = MornyQueries() - updatesListener.manager register MornyOnTelegramCommand(using commands) - updatesListener.manager register MornyOnInlineQuery(using queries) - val events: MornyEventListeners = MornyEventListeners(using updatesListener.manager) + eventManager register MornyOnTelegramCommand(using commands) + eventManager register MornyOnInlineQuery(using queries) + //noinspection ScalaUnusedSymbol + val events: MornyEventListeners = MornyEventListeners(using eventManager) /** inner value: about why morny exit, used in [[daemon.MornyReport]]. */ private var whileExit_reason: AnyRef|Null = _ @@ -72,11 +71,12 @@ class MornyCoeur (using val config: MornyConfig) { daemons.start() logger info "start telegram event listening" - account setUpdatesListener updatesListener + account setUpdatesListener eventManager if config.commandLoginRefresh then logger info "resetting telegram command list" commands.automaticTGListUpdate() + daemons.reporter.reportCoeurMornyLogin() logger info "Coeur start complete." ///<<< BLOCK END instance configure & startup stage 2 @@ -87,13 +87,17 @@ class MornyCoeur (using val config: MornyConfig) { } private def exitCleanup (): Unit = { + daemons.reporter.reportCoeurExit() + account.shutdown() + logger info "stopped bot account" daemons.stop() if config.commandLogoutClear then commands.automaticTGListRemove() + logger info "done exit cleanup" } private def configure_exitCleanup (): Unit = { - Runtime.getRuntime.addShutdownHook(new Thread(() => exitCleanup(), THREAD_MORNY_EXIT)) + Runtime.getRuntime.addShutdownHook(new Thread(() => exitCleanup(), THREAD_SERVER_EXIT)) } def exit (status: Int, reason: AnyRef): Unit = diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala index d042e80..c95bde2 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala @@ -1,13 +1,17 @@ package cc.sukazyo.cono.morny +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.{LimboChat, LimboUser} +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Chat.* import com.pengrad.telegrambot.model.ChatMember.Status +import com.pengrad.telegrambot.TelegramBot class MornyTrusted (using coeur: MornyCoeur)(using config: MornyConfig) { def isTrusted (userId: Long): Boolean = + given TelegramBot = coeur.account if userId == config.trustedMaster then true else if config.trustedChat == -1 then false - else coeur.extra isUserInGroup(userId, config.trustedChat, Status.administrator) + else LimboChat(config.trustedChat) memberHasPermission(LimboUser(userId), Status.administrator) def isTrusted_dinnerReader (userId: Long): Boolean = if userId == config.trustedMaster then true diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala index e4ab6de..0a3e1ac 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListener.scala @@ -1,9 +1,8 @@ package cc.sukazyo.cono.morny.bot.api -import cc.sukazyo.cono.morny.MornyCoeur import com.pengrad.telegrambot.model.Update -trait EventListener (using MornyCoeur) { +trait EventListener () { def onMessage (using Update): Boolean = false def onEditedMessage (using Update): Boolean = false diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala index dc61951..935f561 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala @@ -5,11 +5,12 @@ import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException import com.google.gson.GsonBuilder import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.UpdatesListener import scala.collection.mutable import scala.language.postfixOps -class EventListenerManager (using coeur: MornyCoeur) { +class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener { private val listeners = mutable.Queue.empty[EventListener] @@ -76,8 +77,13 @@ class EventListenerManager (using coeur: MornyCoeur) { } - def publishUpdate (using Update): Unit = { - EventRunner().start() + import java.util + import scala.jdk.CollectionConverters.* + + override def process (updates: util.List[Update]): Int = { + for (update <- updates.asScala) + EventRunner(using update).start() + UpdatesListener.CONFIRMED_UPDATES_ALL } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala deleted file mode 100644 index dd9f71e..0000000 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/TelegramUpdatesListener.scala +++ /dev/null @@ -1,20 +0,0 @@ -package cc.sukazyo.cono.morny.bot.api - -import cc.sukazyo.cono.morny.MornyCoeur -import com.pengrad.telegrambot.UpdatesListener -import com.pengrad.telegrambot.model.Update - -import java.util -import scala.jdk.CollectionConverters.* - -class TelegramUpdatesListener (using MornyCoeur) extends UpdatesListener { - - val manager = EventListenerManager() - - override def process (updates: util.List[Update]): Int = { - for (update <- updates.asScala) - manager.publishUpdate(using update) - UpdatesListener.CONFIRMED_UPDATES_ALL - } - -} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala index 56bec4f..032482b 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/DirectMsgClear.scala @@ -4,6 +4,7 @@ import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.{Chat, Update} import com.pengrad.telegrambot.request.{DeleteMessage, GetChatMember, SendSticker} @@ -36,19 +37,19 @@ class DirectMsgClear (using coeur: MornyCoeur) extends ISimpleCommand { if (isTrusted || _isReplyTrusted) { - coeur.extra exec DeleteMessage( + coeur.account exec DeleteMessage( event.message.chat.id, event.message.replyToMessage.messageId ) def _isPrivate: Boolean = event.message.chat.`type` == Chat.Type.Private def _isPermission: Boolean = - (coeur.extra exec GetChatMember(event.message.chat.id, event.message.from.id)) + (coeur.account exec GetChatMember(event.message.chat.id, event.message.from.id)) .chatMember.canDeleteMessages if (_isPrivate || _isPermission) { - coeur.extra exec DeleteMessage(event.message.chat.id, event.message.messageId) + coeur.account exec DeleteMessage(event.message.chat.id, event.message.messageId) } - } else coeur.extra exec SendSticker( + } else coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala index 8bc5bba..07b1c5a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala @@ -7,6 +7,7 @@ import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.CommonEncrypt import cc.sukazyo.cono.morny.util.CommonEncrypt.* import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.{PhotoSize, Update} import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{GetFile, SendDocument, SendMessage, SendSticker} @@ -47,7 +48,7 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { val mod_uppercase = if (args.length > 1) { if (args.length < 3 && _is_mod_u(args(1))) true else - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) @@ -73,7 +74,7 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { val _r = event.message.replyToMessage if ((_r ne null) && (_r.document ne null)) { try {XFile( - coeur.account getFileContent (coeur.extra exec GetFile(_r.document.fileId)).file, + coeur.account getFileContent (coeur.account exec GetFile(_r.document.fileId)).file, _r.document.fileName )} catch case e: IOException => logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" @@ -91,7 +92,7 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { if (_photo_origin eq null) throw IllegalArgumentException("no photo from api.") import cc.sukazyo.cono.morny.util.UseRandom.rand_id XFile( - coeur.account getFileContent (coeur.extra exec GetFile(_photo_origin.fileId)).file, + coeur.account getFileContent (coeur.account exec GetFile(_photo_origin.fileId)).file, s"photo$rand_id.png" ) } catch @@ -107,7 +108,7 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { } else if ((_r ne null) && (_r.text ne null)) { XText(_r.text) } else { - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, "null" ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) @@ -153,7 +154,7 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { _tool_b64d.decode, CommonEncrypt.lint_base64FileName ) } catch case _: IllegalArgumentException => - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_404 // todo: is here better erro notify? ).replyToMessageId(event.message.messageId) @@ -163,7 +164,7 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { case "sha256" => genResult_hash(input, SHA256) case "sha512" => genResult_hash(input, SHA512) case _ => - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) @@ -173,13 +174,13 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { // output result match case _file: EXFile => - coeur.extra exec SendDocument( + coeur.account exec SendDocument( event.message.chat.id, _file.result ).fileName(_file.resultName).replyToMessageId(event.message.messageId) case _text: EXTextLike => import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, s"
      ${h(_text.text)}
      " ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) @@ -211,7 +212,7 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { * */ private def echoHelp(chat: Long, replyTo: Int): Unit = - coeur.extra exec SendMessage( + coeur.account exec SendMessage( chat, s"""base64, b64 |base64url, base64u, b64u diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala index e6a4fdb..03211e8 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/EventHack.scala @@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendSticker @@ -21,12 +22,12 @@ class EventHack (using coeur: MornyCoeur) extends ITelegramCommand { val x_mode = if (command.args nonEmpty) command.args(0) else "" def done_ok = - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_WAITING ).replyToMessageId(event.message.messageId) def done_forbiddenForAny = - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala index 627d737..a960c07 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetUsernameAndId.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.util.tgapi.{InputCommand, Standardize} import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{GetChatMember, SendMessage} @@ -21,7 +22,7 @@ class GetUsernameAndId (using coeur: MornyCoeur) extends ITelegramCommand { val args = command.args if (args.length > 1) - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, "[Unavailable] Too much arguments." ).replyToMessageId(event.message.messageId) @@ -31,7 +32,7 @@ class GetUsernameAndId (using coeur: MornyCoeur) extends ITelegramCommand { if (args nonEmpty) { try args(0) toLong catch case e: NumberFormatException => - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, s"[Unavailable] ${e.getMessage}" ).replyToMessageId(event.message.messageId) @@ -42,7 +43,7 @@ class GetUsernameAndId (using coeur: MornyCoeur) extends ITelegramCommand { val response = coeur.account execute GetChatMember(event.message.chat.id, userId) if (response.chatMember eq null) - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, "[Unavailable] user not found." ).replyToMessageId(event.message.messageId) @@ -51,13 +52,13 @@ class GetUsernameAndId (using coeur: MornyCoeur) extends ITelegramCommand { val user = response.chatMember.user if (user.id == Standardize.CHANNEL_SPEAKER_MAGIC_ID) - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, "$__channel_identify" ).replyToMessageId(event.message.messageId) return; - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, TelegramUserInformation getFormattedInformation user ).replyToMessageId(event.message.messageId()).parseMode(ParseMode HTML) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala index d6c80b0..60d53a7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/IP186Query.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.ip186.IP186QueryHandler import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendMessage @@ -34,7 +35,7 @@ class IP186Query (using coeur: MornyCoeur) { if (command.args isEmpty) if event.message.replyToMessage eq null then null else event.message.replyToMessage.text else if (command.args.length > 1) - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, "[Unavailable] Too much arguments." ).replyToMessageId(event.message.messageId) @@ -42,7 +43,7 @@ class IP186Query (using coeur: MornyCoeur) { else command.args(0) if (target eq null) - coeur.extra exec new SendMessage( + coeur.account exec new SendMessage( event.message.chat.id, "[Unavailable] No ip defined." ).replyToMessageId(event.message.messageId) @@ -57,7 +58,7 @@ class IP186Query (using coeur: MornyCoeur) { case Subs.WHOIS.cmd => IP186QueryHandler.query_whoisPretty(target) case _ => throw IllegalArgumentException(s"Unknown 186-IP query method ${command.command}") - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, s"""${h(response.url)} |${h(response.body)}""" @@ -65,7 +66,7 @@ class IP186Query (using coeur: MornyCoeur) { ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) } catch case e: Exception => - coeur.extra exec new SendMessage( + coeur.account exec new SendMessage( event.message().chat().id(), s"""[Exception] in query: |${h(e.getMessage)}""" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala index 21fdf46..dbc3205 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyCommands.scala @@ -4,6 +4,7 @@ import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.{BotCommand, DeleteMyCommands, Update} import com.pengrad.telegrambot.request.{SendSticker, SetMyCommands} @@ -75,7 +76,7 @@ class MornyCommands (using coeur: MornyCoeur) { private def nonCommandExecutable (using command: InputCommand, event: Update): Boolean = { if command.target eq null then false else - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) @@ -85,14 +86,14 @@ class MornyCommands (using coeur: MornyCoeur) { def automaticTGListUpdate (): Unit = { val listing = commands_toTelegramList automaticTGListRemove() - coeur.extra exec SetMyCommands(listing:_*) + coeur.account exec SetMyCommands(listing:_*) logger info s"""automatic updated telegram command list : |${commandsTelegramList_toString(listing)}""".stripMargin } def automaticTGListRemove (): Unit = { - coeur.extra exec DeleteMyCommands() + coeur.account exec DeleteMyCommands() logger info "cleaned up command list" } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala index 5c6abb6..34d8943 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyHellos.scala @@ -3,6 +3,7 @@ import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.command.ICommandAlias.ListedAlias import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendSticker @@ -18,7 +19,7 @@ class MornyHellos (using coeur: MornyCoeur) { override val description: String = "检查是否在线" override def execute (using command: InputCommand, event: Update): Unit = - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_ONLINE_STATUS_RETURN ).replyToMessageId(event.message.messageId) @@ -33,7 +34,7 @@ class MornyHellos (using coeur: MornyCoeur) { override val description: String = "打招呼" override def execute (using command: InputCommand, event: Update): Unit = - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_HELLO ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala index 038754a..db0bd7e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInfoOnStart.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.MornyInformation.{getAboutPic, getMornyAboutLinksHTML} import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendPhoto @@ -16,7 +17,7 @@ class MornyInfoOnStart (using coeur: MornyCoeur) extends ISimpleCommand { override def execute (using command: InputCommand, event: Update): Unit = { - coeur.extra exec new SendPhoto( + coeur.account exec new SendPhoto( event.message.chat.id, getAboutPic ).caption( diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala index 583b885..be4aafc 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -6,6 +6,7 @@ import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{SendMessage, SendPhoto, SendSticker} @@ -47,7 +48,7 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { } private def echoInfo (chatId: Long, replyTo: Int): Unit = { - coeur.extra exec new SendPhoto( + coeur.account exec new SendPhoto( chatId, getAboutPic ).caption( @@ -92,15 +93,15 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { val send_mid = SendMessage(send_chat, mid) val send_sticker = SendSticker(send_chat, file_id) if (send_replyTo != -1) send_mid.replyToMessageId(send_replyTo) - val result_send_mid = coeur.extra exec send_mid + val result_send_mid = coeur.account exec send_mid send_sticker.replyToMessageId(result_send_mid.message.messageId) - coeur.extra exec send_sticker + coeur.account exec send_sticker } private[command] def echoVersion (using event: Update): Unit = { val versionDeltaHTML = if (MornySystem.isUseDelta) s"-δ${h(MornySystem.VERSION_DELTA)}" else "" val versionGitHTML = if (MornySystem.isGitBuild) s"git $getVersionGitTagHTML" else "" - coeur.extra exec new SendMessage( + coeur.account exec new SendMessage( event.message.chat.id, // language=html s"""version: @@ -117,7 +118,7 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { private[command] def echoRuntime (using event: Update): Unit = { def sysprop (p: String): String = System.getProperty(p) - coeur.extra exec new SendMessage( + coeur.account exec new SendMessage( event.message.chat.id, /* language=html */ s"""system: @@ -144,7 +145,7 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { } private def echo404 (using event: Update): Unit = - coeur.extra exec new SendSticker( + coeur.account exec new SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala index 05e0bed..5a143c0 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala @@ -6,6 +6,7 @@ import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.daemon.MornyReport import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendSticker @@ -26,7 +27,7 @@ class MornyManagers (using coeur: MornyCoeur) { if (coeur.trusted isTrusted user.id) { - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_EXIT ).replyToMessageId(event.message.messageId) @@ -35,7 +36,7 @@ class MornyManagers (using coeur: MornyCoeur) { } else { - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) @@ -63,14 +64,14 @@ class MornyManagers (using coeur: MornyCoeur) { logger info s"call save from command by ${user toLogTag}" coeur.saveDataAll() - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_SAVED ).replyToMessageId(event.message.messageId) } else { - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala index c468f32..2d44bff 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyOldJrrp.scala @@ -3,6 +3,7 @@ import cc.sukazyo.cono.morny.data.MornyJrrp import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendMessage @@ -24,7 +25,7 @@ class MornyOldJrrp (using coeur: MornyCoeur) extends ITelegramCommand { case _ => "..." import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, // language=html f"${user.fullnameRefHTML} 在(utc的)今天的运气指数是———— $jrrp%.2f%% ${h(ending)}" diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala index 2e0162e..351eb58 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala @@ -4,6 +4,7 @@ import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.{NbnhhshQuery, TelegramStickers} import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{SendMessage, SendSticker} @@ -32,7 +33,7 @@ class Nbnhhsh (using coeur: MornyCoeur) extends ITelegramCommand { else null if (queryTarget == null) - coeur.extra exec SendSticker( + coeur.account exec SendSticker( event.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(event.message.messageId) @@ -67,13 +68,13 @@ class Nbnhhsh (using coeur: MornyCoeur) extends ITelegramCommand { logger debug s"**exec as ${_word.name}" } - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, message toString ).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId) } catch case e: IOException => { - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, s"""[Exception] in query: |${h(e.getMessage)} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala index 808628c..b7a1393 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala @@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendMessage @@ -16,7 +17,7 @@ class Testing (using coeur: MornyCoeur) extends ISimpleCommand { override def execute (using command: InputCommand, event: Update): Unit = { - coeur.extra exec new SendMessage( + coeur.account exec new SendMessage( event.message.chat.id, // language=html "Just a TEST command." diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala index c75038b..996ab79 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/喵呜.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.InputCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.{Message, Update} import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{SendMessage, SendSticker} @@ -48,7 +49,7 @@ class 喵呜 (using coeur: MornyCoeur) { override val paramRule: String = "" override val description: String = "抽取一个神秘盒子" override def execute (using command: InputCommand, event: Update): Unit = { - coeur.extra exec new SendSticker( + coeur.account exec new SendSticker( event.message.chat.id, TelegramStickers ID_PROGYNOVA ).replyToMessageId(event.message.messageId) @@ -58,7 +59,7 @@ class 喵呜 (using coeur: MornyCoeur) { private def replyingSet (whileRec: String, whileNew: String)(using event: Update): Unit = { val isNew = event.message.replyToMessage == null val target = if (isNew) event.message else event.message.replyToMessage - coeur.extra exec new SendMessage( + coeur.account exec new SendMessage( event.message.chat.id, if (isNew) whileNew else whileRec ).replyToMessageId(target.messageId).parseMode(ParseMode HTML) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala index 5ef39f5..0980f68 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/私わね.scala @@ -4,6 +4,7 @@ import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.UseMath.over import cc.sukazyo.cono.morny.util.UseRandom.* +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage @@ -17,7 +18,7 @@ class 私わね (using coeur: MornyCoeur) extends ISimpleCommand { if ((1 over 521) chance_is true) { val text = "/打假" - coeur.extra exec new SendMessage( + coeur.account exec new SendMessage( event.message.chat.id, text ).replyToMessageId(event.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala index 90858bb..38b8f95 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyOnInlineQuery.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.bot.query.{InlineQueryUnit, MornyQueries} +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.InlineQueryResult import com.pengrad.telegrambot.request.AnswerInlineQuery @@ -28,7 +29,7 @@ class MornyOnInlineQuery (using queryManager: MornyQueries) (using coeur: MornyC if (results isEmpty) return false - coeur.extra exec AnswerInlineQuery( + coeur.account exec AnswerInlineQuery( update.inlineQuery.id, resultAnswers toArray:_* ).cacheTime(cacheTime).isPersonal(isPersonal) true diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala index c42caf9..ba0bf7d 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -4,6 +4,7 @@ import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.{Chat, Message, Update, User} import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{ForwardMessage, GetChat, SendMessage, SendSticker} @@ -32,7 +33,7 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener { case _ => return false - coeur.extra exec SendSticker( + coeur.account exec SendSticker( update.message.chat.id, TelegramStickers ID_SENT ).replyToMessageId(update.message.messageId) @@ -41,7 +42,7 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener { } private def requestItem (user: User, itemHTML: String, extra: String|Null = null): Unit = - coeur.extra exec SendMessage( + coeur.account exec SendMessage( me, s"""request $itemHTML |from ${user.fullnameRefHTML}${if extra == null then "" else "\n"+extra}""" @@ -54,8 +55,8 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener { if (coeur.trusted isTrusted_dinnerReader req.from.id) { // todo: have issues // i dont want to test it anymore... it might be deprecated soon - lastDinnerData = (coeur.extra exec GetChat(coeur.config.dinnerChatId)).chat.pinnedMessage - val sendResp = coeur.extra exec ForwardMessage( + lastDinnerData = (coeur.account exec GetChat(coeur.config.dinnerChatId)).chat.pinnedMessage + val sendResp = coeur.account exec ForwardMessage( req.from.id, lastDinnerData.forwardFromChat.id, lastDinnerData.forwardFromMessageId @@ -63,7 +64,7 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener { import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h def lastDinner_dateMillis: Long = lastDinnerData.forwardDate longValue; - coeur.extra exec SendMessage( + coeur.account exec SendMessage( req.from.id, "on %s [UTC+8]\n- %s before".formatted( h(formatDate(lastDinner_dateMillis, 8)), @@ -72,7 +73,7 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener { ).parseMode(ParseMode HTML).replyToMessageId(sendResp.message.messageId) isAllowed = true } else { - coeur.extra exec SendSticker( + coeur.account exec SendSticker( req.from.id, TelegramStickers ID_403 ).replyToMessageId(req.messageId) @@ -87,6 +88,6 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener { private def requestCustom (message: Message): Unit = requestItem(message.from, "[???]") - coeur.extra exec ForwardMessage(me, message.chat.id, message.messageId) + coeur.account exec ForwardMessage(me, message.chat.id, message.messageId) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala index 7544c6b..9c2c80c 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.{Chat, Message, MessageEntity, Update} import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.{GetChat, SendMessage, SendSticker} @@ -60,7 +61,7 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { if !(message.text startsWith "*msg") then return false if (!(coeur.trusted isTrusted message.from.id)) - coeur.extra exec SendSticker( + coeur.account exec SendSticker( message.chat.id, TelegramStickers ID_403 ).replyToMessageId(message.messageId) @@ -74,12 +75,12 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { val sendResponse = coeur.account execute messageToSend.toSendMessage() if (sendResponse isOk) { - coeur.extra exec SendSticker( + coeur.account exec SendSticker( update.message.chat.id, TelegramStickers ID_SENT ).replyToMessageId(update.message.messageId) } else { - coeur.extra exec SendMessage( + coeur.account exec SendMessage( update.message.chat.id, // language=html s"""${sendResponse.errorCode} FAILED @@ -113,12 +114,12 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { s"""${h(chat.id toString)}@${h(chat.`type`.name)}${if (chat.`type` != Chat.Type.Private) ":::" else ""} |${chat.typeTag} ${h(chat.safe_name)} ${chat.safe_linkHTML}""" .stripMargin - coeur.extra exec SendMessage( + coeur.account exec SendMessage( update.message.chat.id, getChatDescriptionHTML(targetChatResponse.chat) ).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId) } else { - coeur.extra exec SendMessage( + coeur.account exec SendMessage( update.message.chat.id, // language=html s"""${targetChatResponse.errorCode} FAILED @@ -131,7 +132,7 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { val testSendResponse = coeur.account execute messageToSend.toSendMessage(update.message.chat.id) .replyToMessageId(update.message.messageId) if (!(testSendResponse isOk)) - coeur.extra exec SendMessage( + coeur.account exec SendMessage( update.message.chat.id, // language=html s"""${testSendResponse.errorCode} FAILED @@ -144,7 +145,7 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { } private def answer404 (using update: Update): Boolean = - coeur.extra exec SendSticker( + coeur.account exec SendSticker( update.message.chat.id, TelegramStickers ID_404 ).replyToMessageId(update.message.messageId) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala index 9010871..9f397a7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.event.OnQuestionMarkReply.isAllMessageMark +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage @@ -19,7 +20,7 @@ class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener { if (1 over 8) chance_is false then return false if !isAllMessageMark(using event.message.text) then return false - coeur.extra exec SendMessage( + coeur.account exec SendMessage( event.message.chat.id, event.message.text ).replyToMessageId(event.message.messageId) true diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala index 2237630..0611122 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserRandom.scala @@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.bot.event import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.api.EventListener +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage @@ -30,7 +31,7 @@ class OnUserRandom (using coeur: MornyCoeur) extends EventListener { if result == null then return false - coeur.extra exec SendMessage( + coeur.account exec SendMessage( update.message.chat.id, result ).replyToMessageId(update.message.messageId) true diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala index 033054d..163f893 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala @@ -5,6 +5,7 @@ import cc.sukazyo.cono.morny.bot.api.EventListener import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.UniversalCommand +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.request.SendMessage @@ -58,7 +59,7 @@ class OnUserSlashAction (using coeur: MornyCoeur) extends EventListener { origin else update.message.replyToMessage - coeur.extra exec SendMessage( + coeur.account exec SendMessage( update.message.chat.id, "%s %s%s %s %s!".format( origin.sender_firstnameRefHTML, diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala index b20e72d..ee1ab67 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/EventHacker.scala @@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.daemon import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.google.gson.GsonBuilder import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.ParseMode @@ -38,7 +39,7 @@ class EventHacker (using coeur: MornyCoeur) { else return false logger debug s"hacked event by $x" import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h - coeur.extra exec SendMessage( + coeur.account exec SendMessage( x.from_chat, // language=html s"${h(GsonBuilder().setPrettyPrinting().create.toJson(update))}" diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala index 880b557..df11781 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.daemon import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.daemon.MedicationTimer.calcNextRoutineTimestamp +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.pengrad.telegrambot.model.{Message, MessageEntity} import com.pengrad.telegrambot.request.{EditMessageText, SendMessage} import com.pengrad.telegrambot.response.SendResponse @@ -49,7 +50,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { } private def sendNotification(): Unit = { - val sendResponse: SendResponse = coeur.extra exec SendMessage(notify_toChat, NOTIFY_MESSAGE) + val sendResponse: SendResponse = coeur.account exec SendMessage(notify_toChat, NOTIFY_MESSAGE) if sendResponse isOk then lastNotify_messageId = sendResponse.message.messageId else lastNotify_messageId = null } @@ -66,7 +67,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { val entities = ArrayBuffer.empty[MessageEntity] if edited.entities ne null then entities ++= edited.entities entities += MessageEntity(MessageEntity.Type.italic, edited.text.length + "\n-- ".length, editTime.length) - coeur.extra exec EditMessageText( + coeur.account exec EditMessageText( notify_toChat, edited.messageId, edited.text + s"\n-- $editTime --" diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala index 3cf175e..96ae18c 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala @@ -13,21 +13,19 @@ class MornyDaemons (using val coeur: MornyCoeur) { logger info "ALL Morny Daemons starting..." // TrackerDataManager.init(); medicationTimer.start() - reporter.onMornyLogin() logger info "Morny Daemons started." } def stop (): Unit = { - logger.info("ALL Morny Daemons stopping...") + logger.info("stopping All Morny Daemons...") // TrackerDataManager.DAEMON.interrupt(); medicationTimer.interrupt() // TrackerDataManager.trackingLock.lock(); try { medicationTimer.join() } catch case e: InterruptedException => e.printStackTrace(System.out) - reporter.onMornyExit() - logger.info("ALL Morny Daemons STOPPED.") + logger.info("stopped ALL Morny Daemons.") } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala index 803bd2e..9461880 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala @@ -2,11 +2,11 @@ package cc.sukazyo.cono.morny.daemon import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig} import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} -import cc.sukazyo.cono.morny.bot.command.MornyInformation import cc.sukazyo.cono.morny.data.MornyInformation.getVersionAllFullTagHTML import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import com.google.gson.GsonBuilder import com.pengrad.telegrambot.model.request.ParseMode import com.pengrad.telegrambot.model.User @@ -17,7 +17,7 @@ class MornyReport (using coeur: MornyCoeur) { private def executeReport[T <: BaseRequest[T, R], R<: BaseResponse] (report: T): Unit = { try { - coeur.extra exec report + coeur.account exec report } catch case e: EventRuntimeException.ActionFailed => { logger warn s"""cannot execute report to telegram: @@ -56,7 +56,7 @@ class MornyReport (using coeur: MornyCoeur) { ).parseMode(ParseMode HTML)) } - def onMornyLogin(): Unit = { + def reportCoeurMornyLogin(): Unit = { executeReport(SendMessage( coeur.config.reportToChat, // language=html @@ -99,7 +99,7 @@ class MornyReport (using coeur: MornyCoeur) { echo dropRight 1 toString } - def onMornyExit (): Unit = { + def reportCoeurExit (): Unit = { val causedTag = coeur.exitReason match case u: User => u.fullnameRefHTML case n if n == null => "UNKNOWN reason" diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java deleted file mode 100644 index b7f19ee..0000000 --- a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/ExtraAction.java +++ /dev/null @@ -1,88 +0,0 @@ -package cc.sukazyo.cono.morny.util.tgapi; - -import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException; -import com.pengrad.telegrambot.TelegramBot; -import com.pengrad.telegrambot.model.Chat; -import com.pengrad.telegrambot.model.ChatMember; -import com.pengrad.telegrambot.model.User; -import com.pengrad.telegrambot.request.BaseRequest; -import com.pengrad.telegrambot.request.GetChatMember; -import com.pengrad.telegrambot.response.BaseResponse; - -public class ExtraAction { - - private final TelegramBot bot; - - public ExtraAction (TelegramBot bot) { - this.bot = bot; - } - - public static ExtraAction as (TelegramBot bot) { - return new ExtraAction(bot); - } - - public boolean isUserInGroup (User user, Chat chat) { - return isUserInGroup(user.id(), chat.id()); - } - - public , R extends BaseResponse> R exec (T req) { - return exec(req, ""); - } - - public , R extends BaseResponse> R exec (T req, String errorMessage) { - final R resp = bot.execute(req); - if (!resp.isOk()) throw new EventRuntimeException.ActionFailed( - (errorMessage.isEmpty() ? String.valueOf(resp.errorCode()) : errorMessage), - resp - ); - return resp; - } - - public boolean isUserInGroup (User user, Chat chat, ChatMember.Status permissionLevel) { - return isUserInGroup(user.id(), chat.id(), permissionLevel); - } - - public boolean isUserInGroup (long userId, long chatId) { - return isUserInGroup(userId, chatId, ChatMember.Status.restricted); - } - - public boolean isUserInGroup (long userId, long chatId, ChatMember.Status permissionLevel) { - final ChatMember chatMember = exec(new GetChatMember(chatId, userId)).chatMember(); - return - chatMember != null && - UserPermissionLevel.as(chatMember.status()).hasPermission(UserPermissionLevel.as(permissionLevel)); - } - -} - -enum UserPermissionLevel { - - CREATOR(3), - ADMINISTRATOR(2), - MEMBER(1), - RESTRICTED(0), - LEFT(-1), - KICKED(-2); - - final int permissionLevel; - - UserPermissionLevel (int permissionLevel) { - this.permissionLevel = permissionLevel; - } - - static UserPermissionLevel as (ChatMember.Status status) { - return switch (status) { - case creator -> CREATOR; - case administrator -> ADMINISTRATOR; - case member -> MEMBER; - case restricted -> RESTRICTED; - case left -> LEFT; - case kicked -> KICKED; - }; - } - - boolean hasPermission (UserPermissionLevel required) { - return this.permissionLevel >= required.permissionLevel; - } - -} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/TelegramExtensions.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/TelegramExtensions.scala new file mode 100644 index 0000000..5e38eeb --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/TelegramExtensions.scala @@ -0,0 +1,68 @@ +package cc.sukazyo.cono.morny.util.tgapi + +import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException +import com.pengrad.telegrambot.TelegramBot +import com.pengrad.telegrambot.model.{Chat, ChatMember, User} +import com.pengrad.telegrambot.request.{BaseRequest, GetChatMember} +import com.pengrad.telegrambot.response.BaseResponse + +import scala.annotation.targetName + +object TelegramExtensions { + + object Bot { extension (bot: TelegramBot) { + + def exec [T <: BaseRequest[T, R], R <: BaseResponse] (request: T, onError_message: String = ""): R = { + val response = bot execute request + if response isOk then return response + throw EventRuntimeException.ActionFailed( + if onError_message isEmpty then response.errorCode toString else onError_message, + response + ) + } + + }} + + object Chat { extension (chat: Chat) { + + def hasMember (user: User) (using TelegramBot): Boolean = + memberHasPermission(user, ChatMember.Status.member) + + def memberHasPermission (user: User, permission: ChatMember.Status) (using bot: TelegramBot): Boolean = { + + //noinspection ScalaUnusedSymbol + enum UserPermissionLevel(val level: Int): + private case CREATOR extends UserPermissionLevel(10) + private case ADMINISTRATOR extends UserPermissionLevel(3) + private case MEMBER extends UserPermissionLevel(1) + private case RESTRICTED extends UserPermissionLevel(-1) + private case LEFT extends UserPermissionLevel(-3) + private case KICKED extends UserPermissionLevel(-5) + @targetName("equalOrGreaterThan") + def >= (another: UserPermissionLevel): Boolean = this.level >= another.level + object UserPermissionLevel: + def apply(status: ChatMember.Status): UserPermissionLevel = + status match + case ChatMember.Status.creator => CREATOR + case ChatMember.Status.administrator => ADMINISTRATOR + case ChatMember.Status.member => MEMBER + case ChatMember.Status.restricted => RESTRICTED + case ChatMember.Status.left => LEFT + case ChatMember.Status.kicked => KICKED + def apply (chatMember: ChatMember): UserPermissionLevel = apply(chatMember.status) + + import Bot.* + val chatMember: ChatMember = (bot exec GetChatMember(chat.id, user.id)).chatMember + if chatMember eq null then false + else UserPermissionLevel(chatMember) >= UserPermissionLevel(permission) + + } + + }} + + class LimboUser (id: Long) extends User(id) + class LimboChat (val _id: Long) extends Chat() { + override val id: java.lang.Long = _id + } + +} From 1bd795873c1c0d455f2bed98651385904f819d9b Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Fri, 6 Oct 2023 20:55:26 +0800 Subject: [PATCH 36/40] code optimize - add UseSelect - add scaladoc for some internal trait - code optimize, mostly use Option now --- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/MornyCoeur.scala | 26 +++++++++--------- .../morny/bot/api/EventListenerManager.scala | 18 ++++++++++++- .../morny/bot/command/ICommandAlias.scala | 26 ++++++++++++++++++ .../morny/bot/command/ISimpleCommand.scala | 26 ++++++++++++++++++ .../morny/bot/command/ITelegramCommand.scala | 17 ++++++++++++ .../morny/bot/command/MornyInformation.scala | 2 +- .../cono/morny/bot/command/Nbnhhsh.scala | 16 +++++------ .../cono/morny/bot/event/OnCallMsgSend.scala | 14 +++++----- .../morny/bot/event/OnQuestionMarkReply.scala | 9 ++++--- .../morny/bot/query/ShareToolBilibili.scala | 23 ++++++++-------- .../morny/bot/query/ShareToolTwitter.scala | 6 ++--- .../cono/morny/daemon/MedicationTimer.scala | 10 +++---- .../cono/morny/daemon/MornyReport.scala | 6 ++--- .../cono/morny/data/MornyInformation.scala | 6 ++--- .../cono/morny/data/TelegramImages.scala | 8 +++--- .../sukazyo/cono/morny/util/UseSelect.scala | 27 +++++++++++++++++++ 17 files changed, 178 insertions(+), 64 deletions(-) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala diff --git a/gradle.properties b/gradle.properties index 3d87307..d37e7e7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s VERSION = 1.0.0-RC5 USE_DELTA = true -VERSION_DELTA = scala3 +VERSION_DELTA = scala4 CODENAME = beiping diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala index 7df72aa..9d0f2ba 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -31,10 +31,12 @@ class MornyCoeur (using val config: MornyConfig) { if config.telegramBotUsername ne null then logger info s"login as:\n ${config.telegramBotUsername}" - private val __loginResult = login() - if (__loginResult eq null) - logger error "Login to bot failed." - System exit -1 + private val __loginResult: LoginResult = login() match + case some: Some[LoginResult] => some.get + case None => + logger error "Login to bot failed." + System exit -1 + throw RuntimeException() configure_exitCleanup() @@ -63,8 +65,8 @@ class MornyCoeur (using val config: MornyConfig) { val events: MornyEventListeners = MornyEventListeners(using eventManager) /** inner value: about why morny exit, used in [[daemon.MornyReport]]. */ - private var whileExit_reason: AnyRef|Null = _ - def exitReason: AnyRef|Null = whileExit_reason + private var whileExit_reason: Option[AnyRef] = None + def exitReason: Option[AnyRef] = whileExit_reason val coeurStartTimestamp: Long = ServerMain.systemStartupTime ///>>> BLOCK START instance configure & startup stage 2 @@ -101,12 +103,12 @@ class MornyCoeur (using val config: MornyConfig) { } def exit (status: Int, reason: AnyRef): Unit = - whileExit_reason = reason + whileExit_reason = Some(reason) System exit status private case class LoginResult(account: TelegramBot, username: String, userid: Long) - private def login (): LoginResult|Null = { + private def login (): Option[LoginResult] = { val builder = TelegramBot.Builder(config.telegramBotKey) var api_bot = config.telegramBotApiServer @@ -129,7 +131,7 @@ class MornyCoeur (using val config: MornyConfig) { val account = builder build logger info "Trying to login..." - boundary[LoginResult|Null] { + boundary[Option[LoginResult]] { for (i <- 0 to 3) { if i > 0 then logger info "retrying..." try { @@ -137,16 +139,16 @@ class MornyCoeur (using val config: MornyConfig) { if ((config.telegramBotUsername ne null) && config.telegramBotUsername != remote.username) throw RuntimeException(s"Required the bot @${config.telegramBotUsername} but @${remote.username} logged in") logger info s"Succeed logged in to @${remote.username}" - break(LoginResult(account, remote.username, remote.id)) + break(Some(LoginResult(account, remote.username, remote.id))) } catch - case r: boundary.Break[LoginResult|Null] => throw r + case r: boundary.Break[Option[LoginResult]] => throw r case e => logger error s"""${exceptionLog(e)} |login failed""" .stripMargin } - null + None } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala index 935f561..5448204 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala @@ -10,6 +10,12 @@ import com.pengrad.telegrambot.UpdatesListener import scala.collection.mutable import scala.language.postfixOps +/** Contains a [[mutable.Queue]] of [[EventListener]], and delivery telegram [[Update]]. + * + * Implemented [[process]] in [[UpdatesListener]] so it can directly used in [[com.pengrad.telegrambot.TelegramBot.setupListener]]. + * + * @param coeur the [[MornyCoeur]] context. + */ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener { private val listeners = mutable.Queue.empty[EventListener] @@ -77,9 +83,19 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener { } + import java.util import scala.jdk.CollectionConverters.* - + /** Delivery the telegram [[Update]]s. + * + * The implementation of [[UpdatesListener]]. + * + * For each [[Update]], create an [[EventRunner]] for it, and + * start the it. + * + * @return [[UpdatesListener.CONFIRMED_UPDATES_ALL]], for all Updates + * should be processed in [[EventRunner]] created for it. + */ override def process (updates: util.List[Update]): Int = { for (update <- updates.asScala) EventRunner(using update).start() diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala index 749f550..de9dbe7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala @@ -1,17 +1,43 @@ package cc.sukazyo.cono.morny.bot.command +/** One alias definition, contains the necessary message of how + * to process the alias. + */ trait ICommandAlias { + /** The alias name. + * + * same with the command name, it is the unique identifier of this alias. + */ val name: String + /** If the alias should be listed while list commands to end-user. + * + * The alias can only be listed when the parent command can be listed + * (meanwhile the parent command implemented [[ITelegramCommand]]). If the + * parent command cannot be listed, it will always cannot be listed. + */ val listed: Boolean } +/** Default implementations of [[ICommandAlias]]. */ object ICommandAlias { + /** Alias which can be listed to end-user. + * + * the [[ICommandAlias.listed]] value is always true. + * + * @param name The alias name, see more in [[ICommandAlias.name]] + */ case class ListedAlias (name: String) extends ICommandAlias: override val listed = true + /** Alias which cannot be listed to end-user. + * + * the [[ICommandAlias.listed]] value is always false. + * + * @param name The alias name, see more in [[ICommandAlias.name]] + */ case class HiddenAlias (name: String) extends ICommandAlias: override val listed = false diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala index 25181bd..24c624f 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala @@ -3,11 +3,37 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.Update +/** A simple command. + * + * Contains only [[name]] and [[aliases]]. + * + * Won't be listed to end-user. if you want the command listed, + * see [[ITelegramCommand]]. + * + */ trait ISimpleCommand { + /** the main name of the command. + * + * must have a value as the unique identifier of this command. + */ val name: String + /** aliases of the command. + * + * Alias means it is the same to call [[name main name]] when call this. + * There can be multiple aliases. But notice that, although alias is not + * the unique identifier, it uses the same namespace with [[name]], means + * it also cannot be duplicate with other [[name]] or [[aliases]]. + * + * It can be [[Null]], means no aliases. + */ val aliases: Array[ICommandAlias]|Null + /** The work code of this command. + * + * @param command The parsed input command which called this command. + * @param event The raw event which called this command. + */ def execute (using command: InputCommand, event: Update): Unit } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala index 2c16263..1a720b8 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala @@ -1,8 +1,25 @@ package cc.sukazyo.cono.morny.bot.command +/** A complex telegram command. + * + * the extension of [[ISimpleCommand]], with external defines of the necessary + * introduction message ([[paramRule]] and [[description]]). + * + * It can be listed to end-user. + */ trait ITelegramCommand extends ISimpleCommand { + /** The param rule of this command, used in human-readable command list. + * + * The param rule uses a symbol language to describe how this command + * receives paras. + * + * Set it empty to make this scope not available. + */ val paramRule: String + /** The description/introduction of this command, used in human-readable + * command list. + */ val description: String } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala index be4aafc..36222eb 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -122,7 +122,7 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { event.message.chat.id, /* language=html */ s"""system: - |- ${h(if (getRuntimeHostname == null) "" else getRuntimeHostname)} + |- ${h(if getRuntimeHostname nonEmpty then getRuntimeHostname.get else "")} |- ${h(sysprop("os.name"))} ${h(sysprop("os.arch"))} ${h(sysprop("os.version"))} |java runtime: |- ${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala index 351eb58..543a783 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala @@ -25,19 +25,17 @@ class Nbnhhsh (using coeur: MornyCoeur) extends ITelegramCommand { override def execute (using command: InputCommand, event: Update): Unit = { - val queryTarget: String|Null = + val queryTarget: String = if command.args nonEmpty then command.args mkString " " else if (event.message.replyToMessage != null && event.message.replyToMessage.text != null) event.message.replyToMessage.text - else null - - if (queryTarget == null) - coeur.account exec SendSticker( - event.message.chat.id, - TelegramStickers ID_404 - ).replyToMessageId(event.message.messageId) - return; + else + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + return; try { diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala index 9c2c80c..db920c0 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala @@ -17,11 +17,11 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { private val REGEX_MSG_SENDREQ_DATA_HEAD: Regex = "^\\*msg(-?\\d+)(\\*\\S+)?(?:\\n([\\s\\S]+))?$"r case class MessageToSend ( - message: String|Null, - entities: Array[MessageEntity]|Null, - parseMode: ParseMode|Null, - targetId: Long - ) { + message: String|Null, + entities: Array[MessageEntity]|Null, + parseMode: ParseMode|Null, + targetId: Long + ) { def toSendMessage (target_override: Long|Null = null): SendMessage = val useTarget = if target_override == null then targetId else target_override val sendMessage = SendMessage(useTarget, message) @@ -129,8 +129,8 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { } if messageToSend.message eq null then return true - val testSendResponse = coeur.account execute messageToSend.toSendMessage(update.message.chat.id) - .replyToMessageId(update.message.messageId) + val testSendResponse = coeur.account execute + messageToSend.toSendMessage(update.message.chat.id).replyToMessageId(update.message.messageId) if (!(testSendResponse isOk)) coeur.account exec SendMessage( update.message.chat.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala index 9f397a7..1aff5f2 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala @@ -8,6 +8,7 @@ import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage import scala.language.postfixOps +import scala.util.boundary class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener { @@ -34,10 +35,10 @@ object OnQuestionMarkReply { private val QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓') def isAllMessageMark (using text: String): Boolean = { - var isAll = true - for (c <- text) - if !(QUESTION_MARKS contains c) then isAll = false - isAll + boundary[Boolean] { + for (c <- text) if QUESTION_MARKS contains c then boundary.break(false) + true + } } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala index a74aad2..f73fed6 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.query import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId import cc.sukazyo.cono.morny.util.BiliTool +import cc.sukazyo.cono.morny.util.UseSelect.select import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} @@ -24,23 +25,23 @@ class ShareToolBilibili extends ITelegramQuery { if (event.inlineQuery.query == null) return null event.inlineQuery.query match - case REGEX_BILI_VIDEO(_1, _2, _3, _4, _5, _6, _7) => + case REGEX_BILI_VIDEO(_url_v, _url_av, _url_bv, _url_param, _url_v_part, _raw_av, _raw_bv) => logger debug s"""====== Share Tool Bilibili Catch ok - |1: ${_1} - |2: ${_2} - |3: ${_3} - |4: ${_4} - |5: ${_5} - |6: ${_6} - |7: ${_7}""" + |1: ${_url_v} + |2: ${_url_av} + |3: ${_url_bv} + |4: ${_url_param} + |5: ${_url_v_part} + |6: ${_raw_av} + |7: ${_raw_bv}""" .stripMargin - var av = if (_2 != null) _2 else if (_6 != null) _6 else null - var bv = if (_3!=null) _3 else if (_7!=null) _7 else null + var av = select(_url_av, _raw_av) + var bv = select(_url_bv, _raw_bv) logger trace s"catch id av[$av] bv[$bv]" - val part: Int|Null = if (_5!=null) _5 toInt else null + val part: Int|Null = if (_url_v_part!=null) _url_v_part toInt else null logger trace s"catch video part[$part]" if (av == null) { diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala index 03ba0de..a550501 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala @@ -21,15 +21,15 @@ class ShareToolTwitter extends ITelegramQuery { event.inlineQuery.query match - case REGEX_TWEET_LINK(_, _2, _, _, _, _) => + case REGEX_TWEET_LINK(_, _path_data, _, _, _, _) => List( InlineQueryUnit(InlineQueryResultArticle( inlineQueryId(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX, - s"https://vxtwitter.com/$_2" + s"https://vxtwitter.com/$_path_data" )), InlineQueryUnit(InlineQueryResultArticle( inlineQueryId(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED, - s"https://c.vxtwitter.com/$_2" + s"https://c.vxtwitter.com/$_path_data" )) ) diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala index df11781..f01671a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala @@ -24,7 +24,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { this.setName(DAEMON_THREAD_NAME_DEF) - private var lastNotify_messageId: Int|Null = _ + private var lastNotify_messageId: Option[Int] = None override def run (): Unit = { logger info "Medication Timer started." @@ -51,8 +51,8 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { private def sendNotification(): Unit = { val sendResponse: SendResponse = coeur.account exec SendMessage(notify_toChat, NOTIFY_MESSAGE) - if sendResponse isOk then lastNotify_messageId = sendResponse.message.messageId - else lastNotify_messageId = null + if sendResponse isOk then lastNotify_messageId = Some(sendResponse.message.messageId) + else lastNotify_messageId = None } @throws[InterruptedException | IllegalArgumentException] @@ -61,7 +61,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { } def refreshNotificationWrite (edited: Message): Unit = { - if lastNotify_messageId != (edited.messageId toInt) then return + if (lastNotify_messageId isEmpty) || (lastNotify_messageId.get != (edited.messageId toInt)) then return import cc.sukazyo.cono.morny.util.CommonFormat.formatDate val editTime = formatDate(edited.editDate*1000, use_timeZone.getTotalSeconds/60/60) val entities = ArrayBuffer.empty[MessageEntity] @@ -72,7 +72,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { edited.messageId, edited.text + s"\n-- $editTime --" ).entities(entities toArray:_*) - lastNotify_messageId = null + lastNotify_messageId = None } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala index 9461880..3528ff1 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala @@ -101,9 +101,9 @@ class MornyReport (using coeur: MornyCoeur) { def reportCoeurExit (): Unit = { val causedTag = coeur.exitReason match - case u: User => u.fullnameRefHTML - case n if n == null => "UNKNOWN reason" - case a: AnyRef => /*language=html*/ s"${h(a.toString)}" + case None => "UNKNOWN reason" + case u: Some[User] => u.get.fullnameRefHTML + case a: Some[_] => /*language=html*/ s"${h(a.get.toString)}" executeReport(SendMessage( coeur.config.reportToChat, // language=html diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala index 12e7671..19e4642 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala @@ -29,9 +29,9 @@ object MornyInformation { } //noinspection ScalaWeakerAccess - def getRuntimeHostname: String | Null = { - try InetAddress.getLocalHost.getHostName - catch case _: UnknownHostException => null + def getRuntimeHostname: Option[String] = { + try Some(InetAddress.getLocalHost.getHostName) + catch case _: UnknownHostException => None } def getAboutPic: Array[Byte] = TelegramImages.IMG_ABOUT get diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala index 6fb7757..ac45039 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala @@ -13,17 +13,17 @@ object TelegramImages { class AssetsFileImage (assetsPath: String) { - private var cache: Array[Byte]|Null = _ + private var cache: Option[Array[Byte]] = None @throws[AssetsException] def get:Array[Byte] = - if cache eq null then read() - cache + if cache isEmpty then read() + cache.get @throws[AssetsException] private def read (): Unit = { Using ((MornyAssets.pack getResource assetsPath)read) { stream => - try { this.cache = stream.readAllBytes() } + try { this.cache = Some(stream.readAllBytes()) } catch case e: IOException => { throw AssetsException(e) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala new file mode 100644 index 0000000..feb3e99 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.util + +import scala.util.boundary + +/** Useful utils of select one specific value in the given values. + * + * contains: + * - [[select()]] can select one value which is not [[Null]]. + * + */ +object UseSelect { + + /** Select the non-null value in the given values. + * + * @tparam T The value's type. + * @param values Given values, may be a T value or [[Null]]. + * @return The first non-null value in the given values, or [[Null]] if + * there's no non-null value. + */ + def select [T] (values: T|Null*): T|Null = { + boundary[T|Null] { + for (i <- values) if i != null then boundary.break(i) + null + } + } + +} From 69086b1f365d918cac823a36cf320366b757f0d5 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Fri, 6 Oct 2023 21:42:50 +0800 Subject: [PATCH 37/40] publish 1.0.0-RC5 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index d37e7e7..8a198fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,8 +7,8 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s VERSION = 1.0.0-RC5 -USE_DELTA = true -VERSION_DELTA = scala4 +USE_DELTA = false +VERSION_DELTA = CODENAME = beiping From 981098cf6e411b0314b1f9d71ee50b103b10b978 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Fri, 6 Oct 2023 21:52:31 +0800 Subject: [PATCH 38/40] update README --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 99bf217..22e0fb8 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,22 @@ [todo]: https://github.com/users/Eyre-S/projects/1 [artifact]: https://mvn.sukazyo.cc/#/releases/cc/sukazyo/morny-coeur -[tg4j]: https://github.com/pengrad/java-telegram-bot-api +[scala]: https://www.scala-lang.org/ [spotbugs]: https://spotbugs.github.io/ -[junit5]: https://junit.org/junit5/ +[tg4j]: https://github.com/pengrad/java-telegram-bot-api +[okhttp]: https://square.github.io/okhttp/ +[gson]: https://github.com/google/gson +[scalatest]: https://scalatest.org/
      # ~~给所有喜欢morny的大家的~~ Morny Coeur 源代码 -~~"你们又有意见又不发issue这样子我很为难的啊"~~ +~~"and nobody cares."~~ ![social preview card](morny-github-social-preview-card@0.75x.png) -一个 telegram 上的服侍 A.C.Sukazyo Eyre 和它的花宫成员的 bot 的内核源 +一个 telegram 上的服侍 A.C.Sukazyo Eyre 和它的花宫成员的 bot 内核 [Task Listing][todo] | [~~BBS~~][issues] | [Published][artifact] @@ -32,6 +35,9 @@ [Java Telegram Bot API][tg4j] -[SpotBugs Annotations][spotbugs] | [JUnit 5][junit5] + +[okhttp] | [Gson][gson] + +[Scala][scala] | [SpotBugs Annotations][spotbugs] | [ScalaTest][scalatest]
      From 985fde9aa2eff50e8333ef976fbb39c9f4a2e0b8 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Fri, 6 Oct 2023 22:24:14 +0800 Subject: [PATCH 39/40] fix sources jar gen in scala --- build.gradle | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 39c8d01..e22d4d9 100644 --- a/build.gradle +++ b/build.gradle @@ -92,6 +92,10 @@ dependencies { } +java { + withSourcesJar() +} + tasks.withType(JavaCompile).configureEach { sourceCompatibility proj_java.getMajorVersion() @@ -144,9 +148,12 @@ buildConfig { } +tasks.withType(Jar).configureEach { + archiveBaseName.set proj_archive_name +} + shadowJar { - archiveBaseName.set proj_archive_name archiveClassifier.set "fat" if (project.hasProperty("dockerBuild")) { From 45a85e15f5afb776831de12e98ad182079569463 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Sat, 7 Oct 2023 22:07:00 +0800 Subject: [PATCH 40/40] unset all function defaults config, fix universal command parse unset the following defaults value: - master - trusted chat - dinner chat id - report to chat - medication notify to chat and make the related function can be shutdown by the new -1 defaults: - MedicationTimer daemon will not run when medication-notify-to-chat or notify-at-hour is not set - MornyReport will do not report when report-to is not set - OnCallMe will send ID_501 when there's no master - OnCallMe.requestLastDinner will send ID_501 when there's no dinner-chat fix when using universal command parse may throw exception --- gradle.properties | 2 +- .../cc/sukazyo/cono/morny/MornyConfig.java | 10 +- .../cc/sukazyo/cono/morny/MornyTrusted.scala | 4 + .../cono/morny/bot/event/OnCallMe.scala | 47 ++++++---- .../morny/bot/event/OnUserSlashAction.scala | 2 +- .../cono/morny/daemon/MedicationTimer.scala | 7 ++ .../cono/morny/daemon/MornyReport.scala | 17 ++-- .../cono/morny/data/TelegramStickers.java | 1 + .../cono/morny/util/UniversalCommand.scala | 32 +++++-- .../cono/morny/util/tgapi/InputCommand.scala | 3 +- .../test/utils/UniversalCommandTest.scala | 93 ++++++++++++++----- 11 files changed, 152 insertions(+), 66 deletions(-) diff --git a/gradle.properties b/gradle.properties index 8a198fb..0756bc7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.0.0-RC5 +VERSION = 1.0.0-RC6 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java index 692706d..61e5826 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyConfig.java @@ -167,16 +167,16 @@ public class MornyConfig { @Nullable public String telegramBotApiServer4File = null; @Nullable public String telegramBotKey = null; @Nullable public String telegramBotUsername = null; - public long trustedMaster = 793274677L; - public long trustedChat = -1001541451710L; + public long trustedMaster = -1L; + public long trustedChat = -1L; public boolean eventIgnoreOutdated = false; public long eventOutdatedTimestamp = -1; public boolean commandLoginRefresh = false; public boolean commandLogoutClear = false; @Nonnull public final Set dinnerTrustedReaders = new HashSet<>(); - public long dinnerChatId = -1001707106392L; - public long reportToChat = -1001650050443L; - public long medicationNotifyToChat = -1001729016815L; + public long dinnerChatId = -1L; + public long reportToChat = -1L; + public long medicationNotifyToChat = -1L; @Nonnull public ZoneOffset medicationTimerUseTimezone = ZoneOffset.UTC; @Nonnull public final Set medicationNotifyAt = new HashSet<>(); diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala index c95bde2..1400db6 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala @@ -2,11 +2,15 @@ package cc.sukazyo.cono.morny import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.{LimboChat, LimboUser} import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Chat.* +import cc.sukazyo.cono.morny.Log.logger import com.pengrad.telegrambot.model.ChatMember.Status import com.pengrad.telegrambot.TelegramBot class MornyTrusted (using coeur: MornyCoeur)(using config: MornyConfig) { + if config.trustedMaster == -1 then + logger warn "You have not set your Morny's master.\n it may have some issues on controlling your bot." + def isTrusted (userId: Long): Boolean = given TelegramBot = coeur.account if userId == config.trustedMaster then true diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala index ba0bf7d..244ee66 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMe.scala @@ -21,35 +21,45 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener { if update.message.chat.`type` != (Chat.Type Private) then return false //noinspection ScalaUnnecessaryParentheses - (update.message.text toLowerCase) match - case "steam" | "sbeam" | "sdeam" => - requestItem(update.message.from, "STEAM LIBRARY") - case "hana paresu" | "花宫" | "内群" => - requestItem(update.message.from, "Hana Paresu") - case "dinner" | "lunch" | "breakfast" | "meal" | "eating" | "安妮今天吃什么" => - requestLastDinner(update.message) - case cc if cc startsWith "cc::" => - requestCustom(update.message) - case _ => - return false + val success = if me == -1 then false else + (update.message.text toLowerCase) match + case "steam" | "sbeam" | "sdeam" => + requestItem(update.message.from, "STEAM LIBRARY") + case "hana paresu" | "花宫" | "内群" => + requestItem(update.message.from, "Hana Paresu") + case "dinner" | "lunch" | "breakfast" | "meal" | "eating" | "安妮今天吃什么" => + requestLastDinner(update.message) + case cc if cc startsWith "cc::" => + requestCustom(update.message) + case _ => + return false + + if success then + coeur.account exec SendSticker( + update.message.chat.id, + TelegramStickers ID_SENT + ).replyToMessageId(update.message.messageId) + else + coeur.account exec SendSticker( + update.message.chat.id, + TelegramStickers ID_501 + ).replyToMessageId(update.message.messageId) - coeur.account exec SendSticker( - update.message.chat.id, - TelegramStickers ID_SENT - ).replyToMessageId(update.message.messageId) true } - private def requestItem (user: User, itemHTML: String, extra: String|Null = null): Unit = + private def requestItem (user: User, itemHTML: String, extra: String|Null = null): Boolean = coeur.account exec SendMessage( me, s"""request $itemHTML |from ${user.fullnameRefHTML}${if extra == null then "" else "\n"+extra}""" .stripMargin ).parseMode(ParseMode HTML) + true - private def requestLastDinner (req: Message): Unit = { + private def requestLastDinner (req: Message): Boolean = { + if coeur.config.dinnerChatId == -1 then return false var isAllowed = false var lastDinnerData: Message|Null = null if (coeur.trusted isTrusted_dinnerReader req.from.id) { @@ -86,8 +96,9 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener { ) } - private def requestCustom (message: Message): Unit = + private def requestCustom (message: Message): Boolean = requestItem(message.from, "[???]") coeur.account exec ForwardMessage(me, message.chat.id, message.messageId) + true } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala index 163f893..eb3e119 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnUserSlashAction.scala @@ -31,7 +31,7 @@ class OnUserSlashAction (using coeur: MornyCoeur) extends EventListener { // scala, those commented code is removed permanently. // these message, here to remember the old DP7. - val actions = UniversalCommand(text) + val actions = UniversalCommand.Lossy(text) actions(0) = actions(0) substring 1 actions(0) diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala index f01671a..3c61872 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala @@ -27,6 +27,12 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { private var lastNotify_messageId: Option[Int] = None override def run (): Unit = { + + if ((notify_toChat == -1) || (notify_atHour isEmpty)) { + logger info "Medication Timer disabled : related param is not complete set" + return + } + logger info "Medication Timer started." while (!this.isInterrupted) { try { @@ -47,6 +53,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { coeur.daemons.reporter.exception(e) } logger info "Medication Timer stopped." + } private def sendNotification(): Unit = { diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala index 3528ff1..00ff369 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala @@ -15,7 +15,12 @@ import com.pengrad.telegrambot.response.BaseResponse class MornyReport (using coeur: MornyCoeur) { + private val enabled = coeur.config.reportToChat != -1 + if !enabled then + logger info "Morny Report is disabled : report chat is set to -1" + private def executeReport[T <: BaseRequest[T, R], R<: BaseResponse] (report: T): Unit = { + if !enabled then return; try { coeur.account exec report } catch case e: EventRuntimeException.ActionFailed => { @@ -70,8 +75,7 @@ class MornyReport (using coeur: MornyCoeur) { ).parseMode(ParseMode HTML)) } - //noinspection ScalaWeakerAccess - def sectionConfigFields (config: MornyConfig): String = { + private def sectionConfigFields (config: MornyConfig): String = { val echo = StringBuilder() for (field <- config.getClass.getFields) { // language=html @@ -100,17 +104,18 @@ class MornyReport (using coeur: MornyCoeur) { } def reportCoeurExit (): Unit = { - val causedTag = coeur.exitReason match + def _causedTag = coeur.exitReason match + case Some(_exitReason) => _exitReason match + case u: User => u.fullnameRefHTML + case a => /*language=html*/ s"${h(a.toString)}" case None => "UNKNOWN reason" - case u: Some[User] => u.get.fullnameRefHTML - case a: Some[_] => /*language=html*/ s"${h(a.get.toString)}" executeReport(SendMessage( coeur.config.reportToChat, // language=html s"""▌Morny Exited |from user @${coeur.username} | - |by: $causedTag""" + |by: $_causedTag""" .stripMargin ).parseMode(ParseMode HTML)) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java index 10a017a..51aa33f 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramStickers.java @@ -21,6 +21,7 @@ public class TelegramStickers { public static final String ID_SAVED = "CAACAgEAAx0CSQh32gABBExuYdB_G0srfhQldRWkBYxWzCOv4-IAApooAAJ4_MYFcjuNZszfQcQjBA"; public static final String ID_PROGYNOVA = "CAACAgUAAxkBAAICm2KEuL7UQqNP7vSPCg2DHJIND6UsAAKLAwACH4WSBszIo722aQ3jJAQ"; public static final String ID_NETWORK_ERR = "CAACAgEAAxkBAAID0WNJgNEkD726KW4vZeFlw0FlVVyNAAIXJgACePzGBb50o7O1RbxoKgQ"; + public static final String ID_501 = "CAACAgEAAxkBAAIHbGUhJ8zm2Sb_c0YU-DYQ6xb-ZDtaAAKdJwACePzGBTOftDZL6X7vMAQ"; @Nonnull public static Map map () { diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala index 9aa6e5c..00f4a0f 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UniversalCommand.scala @@ -7,9 +7,15 @@ import scala.util.boundary * @todo docs * @todo maybe there can have some encapsulation */ -object UniversalCommand { +object UniversalCommand { - def apply (input: String): Array[String] = { + opaque type StrictMode = Boolean + //noinspection ScalaWeakerAccess + val strict: StrictMode = true + //noinspection ScalaWeakerAccess + val lossy: StrictMode = false + + def apply (using strict: StrictMode = strict)(input: String): Array[String] = { val builder = ArrayBuffer.empty[String] @@ -38,26 +44,29 @@ object UniversalCommand { val _inside_tag = input(i) boundary { while (true) { i=i+1 - if (i >= input.length) throw IllegalArgumentException("UniversalCommand: unclosed quoted text") + if (i >= input.length) + if strict then throw IllegalArgumentException("UniversalCommand: unclosed quoted text") + else boundary.break() if (input(i) == _inside_tag) boundary.break() - else if (input(i) isUnsupported) + else if (input(i) isUnsupported) && strict then throw IllegalArgumentException("UniversalCommand: unsupported new-line") - else if (input(i) isQuote) + else if (input(i) isQuote) && strict then throw IllegalArgumentException("UniversalCommand: mixed \" and ' used") else if (input(i) isEscapeChar) - if (i+1 >= input.length) throw IllegalArgumentException("UniversalCommand: \\ in the end") - if (input(i+1) escapableInQuote) + if (i+1 >= input.length) && strict then + throw IllegalArgumentException("UniversalCommand: \\ in the end") + if ((i+1 < input.length) && (input(i+1) escapableInQuote)) i=i+1 arg += input(i) else arg += input(i) }} - } else if (input(i) isUnsupported) { + } else if ((input(i) isUnsupported) && strict) { throw IllegalArgumentException("UniversalCommand: unsupported new-line") } else if (input(i) isEscapeChar) { - if (i + 1 >= input.length) throw IllegalArgumentException("UniversalCommand: \\ in the end") - if (input(i+1) escapable) + if (i + 1 >= input.length) && strict then throw IllegalArgumentException("UniversalCommand: \\ in the end") + if ((i+1 < input.length) && (input(i+1) escapable)) i=i+1 arg += input(i) } else { @@ -71,4 +80,7 @@ object UniversalCommand { } + object Lossy: + def apply (input: String): Array[String] = UniversalCommand(using lossy)(input) + } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala index f6c7e26..209415a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/tgapi/InputCommand.scala @@ -25,7 +25,8 @@ object InputCommand { ) } + //noinspection NoTailRecursionAnnotation def apply (input: String): InputCommand = - InputCommand(UniversalCommand(input)) + InputCommand(UniversalCommand.Lossy(input)) } diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala index 28a11c5..abf5010 100644 --- a/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/UniversalCommandTest.scala @@ -9,50 +9,93 @@ class UniversalCommandTest extends MornyTests with Matchers with TableDrivenProp "while formatting command from String :" - { import cc.sukazyo.cono.morny.util.UniversalCommand as Cmd + import cc.sukazyo.cono.morny.util.UniversalCommand.Lossy as Lmd + def whileLossy (info: String): String = "in lossy mode " + info + def whileStrict (info: String): String = "in strict mode" + info raw"args should be separated by (\u0020) ascii-space" in: - Cmd("a b c delta e") shouldEqual Array("a", "b", "c", "delta", "e"); + Cmd("a b c delta e") shouldEqual Array("a", "b", "c", "delta", "e") + Lmd("a b c delta e") shouldEqual Array("a", "b", "c", "delta", "e") "args should not be separated by non-ascii spaces" in: - Cmd("tests ダタ セト") shouldEqual Array("tests", "ダタ セト"); + Cmd("tests ダタ セト") shouldEqual Array("tests", "ダタ セト") + Lmd("tests ダタ セト") shouldEqual Array("tests", "ダタ セト") "multiple ascii-spaces should not generate empty arg in middle" in: - Cmd("tests some of data") shouldEqual Array("tests", "some", "of", "data"); + Cmd("tests some of data") shouldEqual Array("tests", "some", "of", "data") + Lmd("tests some of data") shouldEqual Array("tests", "some", "of", "data") """texts and ascii-spaces in '' should grouped in one arg""" in: - Cmd("""tests 'data set'""") shouldEqual Array("tests", "data set"); + Cmd("""tests 'data set'""") shouldEqual Array("tests", "data set") + Lmd("""tests 'data set'""") shouldEqual Array("tests", "data set") """texts and ascii-spaces in "" should grouped in one arg""" in : - Cmd("""tests "data set"""") shouldEqual Array("tests", "data set"); - """mixed ' and " should throws IllegalArgumentsException""" in: - an [IllegalArgumentException] should be thrownBy Cmd("""tests "data set' "of it'"""); - "with ' not closed should throws IllegalArgumentException" in: - an [IllegalArgumentException] should be thrownBy Cmd("""use 'it """); + Cmd("""tests "data set"""") shouldEqual Array("tests", "data set") + Lmd("""tests "data set"""") shouldEqual Array("tests", "data set") + """texts nested '' should grouped in one arg""" in : + Cmd("""tests some:'data set'.message is""") shouldEqual Array("tests", "some:data set.message", "is") + Lmd("""tests some:'data set'.message is""") shouldEqual Array("tests", "some:data set.message", "is") + """texts and "" nested '' should grouped in one arg""" in : + Cmd("""tests "arg1 x":'arg2 y' param""") shouldEqual Array("tests", "arg1 x:arg2 y", "param") + Lmd("""tests "arg1 x":"arg2 y" param""") shouldEqual Array("tests", "arg1 x:arg2 y", "param") + "with ' not closed" - { + whileStrict("should throws IllegalArgumentException") in: + an[IllegalArgumentException] should be thrownBy Cmd("""use 'it """) + whileLossy("should be cut at end") in: + Lmd("use 'it ") shouldEqual Array("use", "it ") + } + """mixed ' and """" - { + whileStrict("should throws IllegalArgumentsException") in: + an[IllegalArgumentException] should be thrownBy Cmd("""tests "data set' "of it'""") + whileLossy("should be seen as a normal character") in: + Lmd("""tests "data set' "of it'""") shouldEqual Array("tests", "data set' of", "it") + } raw"\ should escape itself" in: - Cmd(raw"input \\data") shouldEqual Array("input", "\\data"); + Cmd(raw"input \\data") shouldEqual Array("input", "\\data") + Lmd(raw"input \\data") shouldEqual Array("input", "\\data") raw"\ should escape ascii-space, makes it processed as a normal character" in: - Cmd(raw"input data\ set") shouldEqual Array("input", "data set"); + Cmd(raw"input data\ set") shouldEqual Array("input", "data set") + Lmd(raw"input data\ set") shouldEqual Array("input", "data set") raw"\ should escape ascii-space, makes it can be an arg body" in: - Cmd(raw"input \ some-thing") shouldEqual Array("input", " ", "some-thing"); + Cmd(raw"input \ some-thing") shouldEqual Array("input", " ", "some-thing") + Lmd(raw"input \ some-thing") shouldEqual Array("input", " ", "some-thing") raw"""\ should escape "", makes it processed as a normal character""" in : - Cmd(raw"""use \"inputted""") shouldEqual Array("use", "\"inputted"); + Cmd(raw"""use \"inputted""") shouldEqual Array("use", "\"inputted") + Lmd(raw"""use \"inputted""") shouldEqual Array("use", "\"inputted") raw"\ should escape '', makes it processed as a normal character" in: - Cmd(raw"use \'inputted") shouldEqual Array("use", "'inputted"); + Cmd(raw"use \'inputted") shouldEqual Array("use", "'inputted") + Lmd(raw"use \'inputted") shouldEqual Array("use", "'inputted") raw"\ should escape itself which inside a quoted scope" in: - Cmd(raw"use 'quoted \\ body'") shouldEqual Array("use", "quoted \\ body"); + Cmd(raw"use 'quoted \\ body'") shouldEqual Array("use", "quoted \\ body") + Lmd(raw"use 'quoted \\ body'") shouldEqual Array("use", "quoted \\ body") raw"""\ should escape " which inside a "" scope""" in: - Cmd(raw"""in "quoted \" body" body""") shouldEqual Array("in", "quoted \" body", "body"); + Cmd(raw"""in "quoted \" body" body""") shouldEqual Array("in", "quoted \" body", "body") + Lmd(raw"""in "quoted \" body" body""") shouldEqual Array("in", "quoted \" body", "body") raw"""\ should escape ' which inside a "" scope""" in : - Cmd(raw"""in "not-quoted \' body" body""") shouldEqual Array("in", "not-quoted ' body", "body"); + Cmd(raw"""in "not-quoted \' body" body""") shouldEqual Array("in", "not-quoted ' body", "body") + Lmd(raw"""in "not-quoted \' body" body""") shouldEqual Array("in", "not-quoted ' body", "body") raw"""\ should escape ' which inside a '' scope""" in : - Cmd(raw"""in 'quoted \' body' body""") shouldEqual Array("in", "quoted ' body", "body"); + Cmd(raw"""in 'quoted \' body' body""") shouldEqual Array("in", "quoted ' body", "body") + Lmd(raw"""in 'quoted \' body' body""") shouldEqual Array("in", "quoted ' body", "body") raw"""\ should escape " which inside a ' scope""" in : - Cmd(raw"""in 'not-quoted \" body' body""") shouldEqual Array("in", "not-quoted \" body", "body"); + Cmd(raw"""in 'not-quoted \" body' body""") shouldEqual Array("in", "not-quoted \" body", "body") + Lmd(raw"""in 'not-quoted \" body' body""") shouldEqual Array("in", "not-quoted \" body", "body") raw"\ should not escape ascii-space which inside a quoted scope" in: - Cmd(raw"""'quoted \ do not escape' did""") shouldEqual Array(raw"quoted \ do not escape", "did"); - raw"with \ in the end should throws IllegalArgumentException" in: - an [IllegalArgumentException] should be thrownBy Cmd("something error!\\"); + Cmd(raw"""'quoted \ do not escape' did""") shouldEqual Array(raw"quoted \ do not escape", "did") + Lmd(raw"""'quoted \ do not escape' did""") shouldEqual Array(raw"quoted \ do not escape", "did") + raw"with \ in the end" - { + whileStrict("should throws IllegalArgumentException") in: + an [IllegalArgumentException] should be thrownBy Cmd("something error!\\") + whileLossy("should seen as normal char") in: + Lmd("something error!\\") shouldEqual Array("something", "error!\\") + } + + + "with multi-line input" - { + whileStrict("should throws IllegalArgumentException") in: + an [IllegalArgumentException] should be thrownBy Cmd("something will\nhave a new line") + whileLossy("should keep new-line char origin like") in: + Lmd("something will\nhave a new line") shouldEqual Array("something", "will\nhave", "a", "new", "line") + } - "with multi-line input should throws IllegalArgumentException" in: - an [IllegalArgumentException] should be thrownBy Cmd("something will\nhave a new line"); val example_special_character = Table( "char", @@ -68,6 +111,8 @@ class UniversalCommandTest extends MornyTests with Matchers with TableDrivenProp s"input with special character ($char) should keep origin like" in { Cmd(s"$char dataset data[$char]contains parsed") shouldEqual Array(char, "dataset", s"data[$char]contains", "parsed") + Lmd(s"$char dataset data[$char]contains parsed") shouldEqual + Array(char, "dataset", s"data[$char]contains", "parsed") } } }