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..1629e2c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,70 @@ +import org.ajoberstar.grgit.Status + +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 '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' } -group 'cc.sukazyo' -version VERSION -project.ext.archiveBaseName = 'Coeur_Morny_Cono' -project.ext.artifactId = 'morny-coeur' -mainClassName = 'cc.sukazyo.cono.morny.ServerMain' +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." +else if (isCleanBuild()) { + print "git: clean build at ${grgit.head().id}" +} else { + final Status status = grgit.status() + print "git: non-clean-build" + if (!status.unstaged.allChanges.empty) { + print "\ngit: unstaged changes" + listChanges(status.unstaged) + } + if (!status.staged.allChanges.empty) { + print "\ngit: staged changes" + listChanges(status.staged) + } +} + +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 +final boolean proj_version_use_delta = Boolean.parseBoolean(USE_DELTA) +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() + +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_full +setArchivesBaseName proj_archive_name repositories { mavenCentral() @@ -31,74 +85,100 @@ 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') - } +application { + mainClass = proj_application_main } -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_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}\"") + 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}\"") + +} + shadowJar { - archiveBaseName.set("${project.ext.archiveBaseName}") - archiveVersion.set("${project.version}") - archiveClassifier.set("fat") + 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) + print "\n add: ${file}" + for (String file in listing.modified) + print "\n mod: ${file}" + for (String file in listing.removed) + 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 338d93f..22b2091 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,23 @@ ## Core -VERSION = 0.8.0.11 +MORNY_ARCHIVE_NAME = morny-coeur -CODENAME = putian +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 + +USE_DELTA = false +VERSION_DELTA = + +CODENAME = beiping # dependencies -libSpotbugsVersion = 4.7.2 +libSpotbugsVersion = 4.7.3 libMessivaVersion = 0.1.0.1 -libJavaTelegramBotApiVersion = 5.6.0 +libJavaTelegramBotApiVersion = 6.2.0 libJunitVersion = 5.9.0 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 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 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 8735b7d..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 = "0.8.0.11"; - public static final String CODENAME = "putian"; - public static final long COMPILE_TIMESTAMP = 1667376095614L; -} 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 9c0cee9..49f8655 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,38 @@ 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 coeurStartTimestamp = ServerMain.systemStartupTime; - public static final long DINNER_CHAT_ID = -1001707106392L; + private Object whileExitReason = null; 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 +95,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 +116,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!!!"); } @@ -177,9 +154,8 @@ public class MornyCoeur { * 用于退出时进行缓存的任务处理等进行安全退出 */ private void exitCleanup () { - logger.info("clean:save tracker data."); MornyDaemons.stop(); - if (isRemoveCommandListWhenExit) { + if (config.commandLogoutClear) { commandManager.automaticRemoveList(); } } @@ -192,16 +168,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 @@ -244,7 +224,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."); } } @@ -260,6 +240,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 对象 * @@ -281,13 +269,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; } /** @@ -317,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 new file mode 100644 index 0000000..be61e15 --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/MornyConfig.java @@ -0,0 +1,162 @@ +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 * + * ======================================= */ + + 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 @Sensitive 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; + + /* ======================================= * + * system: morny report * + * ======================================= */ + + /** + * 控制 Morny Coeur 系统的报告的报告对象. + * @since 1.0.0-alpha5 + */ + public final long reportToChat; + + /* ======================================= * + * function: dinner query tool * + * ======================================= */ + + @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; + 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; + this.reportToChat = prototype.reportToChat; + } + + 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; + 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 e9a486d..a4e2933 100644 --- a/src/main/java/cc/sukazyo/cono/morny/MornySystem.java +++ b/src/main/java/cc/sukazyo/cono/morny/MornySystem.java @@ -1,8 +1,11 @@ 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; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.net.URISyntaxException; import java.security.NoSuchAlgorithmException; @@ -13,20 +16,108 @@ import java.security.NoSuchAlgorithmException; public class MornySystem { /** - * 程序的语义化版本号
- * 会由 gradle 任务 {@code updateVersionCode} 更新 + * 程序的语义化版本号. + *

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

+ * 这个格式的版本号也是在 maven 包仓库中所使用的版本号 + * @since 1.0.0-alpha4 */ - public static final String VERSION = GradleProjectConfigures.VERSION; + @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 当前的版本代号.
+ * Morny Coeur 当前的版本代号. + *

* 一个单个单词,一般作为一个大版本的名称,只在重大更新改变
* 格式保持为仅由小写字母和数字组成
* 有时也可能是复合词或特殊的词句
*
- * 会由 gradle 任务 {@code updateVersionCode} 更新 */ - public static final String CODENAME = GradleProjectConfigures.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 值
@@ -43,10 +134,10 @@ 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); + 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 ba93789..7a43fa0 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,23 @@ 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; + if (instance.config.trustedChat == -1) return false; + 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..e9237ee 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"; @@ -53,11 +49,14 @@ 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} 参数不兼容 —— 会导致程序完全没有任何输出 *
  • *
  • - * {@code --outdated-block} 会使得 {@link MornyCoeur#latestEventTimestamp} + * {@code --outdated-block} 会使得 {@link MornyConfig#eventIgnoreOutdated} * 赋值为程序启动的时间,从而造成阻挡程序启动之前的消息事件处理效果。 *
  • *
  • @@ -85,20 +84,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 +97,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 +114,56 @@ 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; + } + case "--report-to" -> { + i++; + config.reportToChat = Long.parseLong(args[i]); continue; } } @@ -180,7 +176,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; @@ -196,16 +192,26 @@ 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(), - GradleProjectConfigures.COMPILE_TIMESTAMP, - CommonFormat.formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0) + 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; @@ -216,11 +222,12 @@ public class ServerMain { logger.info(String.format(""" ServerMain.java Loaded >>> - - version %s (%s)(%d) - - Morny %s""", - MornySystem.VERSION, - MornySystem.getJarMd5(), GradleProjectConfigures.COMPILE_TIMESTAMP, - MornySystem.CODENAME.toUpperCase() + - version %s + - Morny %s + - <%s> [%d]""", + MornySystem.VERSION_FULL, + MornySystem.CODENAME.toUpperCase(), + MornySystem.getJarMd5(), BuildConfig.CODE_TIMESTAMP )); //# @@ -228,21 +235,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/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 7b37258..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,8 @@ package cc.sukazyo.cono.morny.bot.command; -import cc.sukazyo.cono.morny.GradleProjectConfigures; 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; import cc.sukazyo.cono.morny.util.tgapi.InputCommand; @@ -19,16 +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.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; -import static cc.sukazyo.cono.morny.util.CommonFormat.formatDuration; import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml; public class MornyCommands { @@ -85,12 +78,15 @@ public class MornyCommands { // 统一注册这些奇怪的东西&.& register( + new 私わね(), + new 喵呜.Progynova() + ); + // special: 注册出于兼容使用的特别 event 的数据 + OnUniMeowTrigger.register( new 喵呜.抱抱(), new 喵呜.揉揉(), new 喵呜.蹭蹭(), - new 喵呜.贴贴(), - new 私わね(), - new 喵呜.Progynova() + new 喵呜.贴贴() ); } @@ -215,7 +211,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(), @@ -223,102 +219,24 @@ 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()); } } - 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 版本信息"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandVersionExec(event); } - } - private static void onCommandVersionExec (@Nonnull Update event) { - MornyCoeur.extra().exec(new SendMessage( - event.message().chat().id(), - String.format( - """ - version: - - Morny %s - - %s - core md5_hash: - - %s - compile timestamp: - - %d - - %s [UTC]""", - escapeHtml(MornySystem.CODENAME.toUpperCase()), - escapeHtml(MornySystem.VERSION), - escapeHtml(MornySystem.getJarMd5()), - GradleProjectConfigures.COMPILE_TIMESTAMP, - escapeHtml(formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0)) - ) - ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); + @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); } } - 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 运行时信息(包括版本号)"; } - @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandRuntimeExec(event); } - } - /** - * @since 0.4.1.2 - */ - private 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 [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), - escapeHtml(MornySystem.CODENAME), - escapeHtml(MornySystem.getJarMd5()), - escapeHtml(formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0)), - GradleProjectConfigures.COMPILE_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)); + @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); } } private static class Jrrp implements ITelegramCommand { @@ -367,6 +285,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/MornyInformations.java b/src/main/java/cc/sukazyo/cono/morny/bot/command/MornyInformations.java index 9d815bb..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,46 +1,236 @@ 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 { - 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(event); + return; } 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(event); + } else if (action.equals(SUB_VERSION) || action.equals(SUB_VERSION_2)) { + echoVersion(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 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 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) { + 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/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 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/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/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/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/OnCallMsgSend.java b/src/main/java/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.java index d02beae..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,17 +27,17 @@ 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 ) { } @Override - public boolean onMessage(Update update) { + public boolean onMessage(@Nonnull Update update) { // 执行体检查 if (update.message().chat().type() != Chat.Type.Private) return false; @@ -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/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(); + } + +} 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 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/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 new file mode 100644 index 0000000..b4f7ae5 --- /dev/null +++ b/src/main/java/cc/sukazyo/cono/morny/daemon/MornyReport.java @@ -0,0 +1,155 @@ +package cc.sukazyo.cono.morny.daemon; + +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; +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 , R extends BaseResponse> void executeReport (@Nonnull T report) { + if (!MornyCoeur.available()) 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 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)); + } + + /** + * 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 + """, + MornyInformations.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 (!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)); + } + +} 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); } } 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 {} diff --git a/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java b/src/test/java/cc/sukazyo/cono/morny/MornyCLI.java index 1b6eea7..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-"+GradleProjectConfigures.VERSION+".jar " ); + System.out.print("$ java -jar morny-coeur-"+MornySystem.VERSION_FULL+".jar " ); String x = line.nextLine(); ServerMain.main(UniversalCommand.format(x));