Merge branch 'major/1.0'

This commit is contained in:
A.C.Sukazyo Eyre 2022-11-12 16:13:51 +08:00
commit a863570be2
Signed by: Eyre_S
GPG Key ID: C17CE40291207874
32 changed files with 1057 additions and 379 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@
/build/ /build/
/bin/ /bin/
.project .project
lcoal.properties
# debug dir # debug dir
/run/ /run/

View File

@ -1,16 +1,70 @@
import org.ajoberstar.grgit.Status
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
plugins { plugins {
id 'java' id 'java'
id 'java-library' id 'java-library'
id 'maven-publish'
id 'application' 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' final boolean proj_git = grgit!=null
version VERSION final String proj_store = MORNY_CODE_STORE
project.ext.archiveBaseName = 'Coeur_Morny_Cono' final String proj_commit = proj_git ? grgit.head().id : null
project.ext.artifactId = 'morny-coeur' final String proj_commit_path = MORNY_COMMIT_PATH
mainClassName = 'cc.sukazyo.cono.morny.ServerMain' 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 { repositories {
mavenCentral() mavenCentral()
@ -31,74 +85,100 @@ dependencies {
} }
task updateVersionCode { application {
ant.replaceregexp(match:'VERSION = ["a-zA-Z0-9.\\-_+@]+;', replace:"VERSION = \"$project.version\";", flags:'g', byline:true) { mainClass = proj_application_main
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 { test {
useJUnitPlatform() useJUnitPlatform()
} }
java { java {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility proj_java
targetCompatibility JavaVersion.VERSION_17 targetCompatibility proj_java
withSourcesJar() withSourcesJar()
} }
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
options.encoding = "UTF-8" options.encoding = proj_file_encoding.name()
} }
tasks.withType(Javadoc) { tasks.withType(Javadoc) {
options.encoding = 'UTF-8' options.encoding = proj_file_encoding.name()
options.docEncoding = 'UTF-8' options.docEncoding = proj_file_encoding.name()
options.charSet = 'UTF-8' options.charSet = proj_file_encoding.name()
} }
tasks.test { tasks.test {
useJUnitPlatform() 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 { shadowJar {
archiveBaseName.set("${project.ext.archiveBaseName}") archiveClassifier.set "fat"
archiveVersion.set("${project.version}") }
archiveClassifier.set("fat")
@SuppressWarnings("all")
boolean isCleanBuild () {
if (grgit == null) return false
Set<String> 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 { publishing {
repositories{ repositories{
maven { if (publish_local_url != null) maven {
name 'builds' name 'archives'
url publishLocalArchiveRepoUrl url publish_local_url
} }
maven { if (publish_remote_url != null) maven {
name '-ws-' name '-ws-'
url publishMvnRepoUrl url publish_remote_url
credentials { credentials {
username publishMvnRepoUsername username publish_remote_username
password publishMvnRepoPassword password publish_remote_password
} }
} }
} }
publications { publications {
main (MavenPublication) { main (MavenPublication) {
from components.java from components.java
groupId = project.group groupId = proj_group
artifactId = project.ext.artifactId artifactId = proj_archive_name
version = project.version version = proj_version
} }
} }
} }

View File

@ -1,15 +1,23 @@
## Core ## 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 # dependencies
libSpotbugsVersion = 4.7.2 libSpotbugsVersion = 4.7.3
libMessivaVersion = 0.1.0.1 libMessivaVersion = 0.1.0.1
libJavaTelegramBotApiVersion = 5.6.0 libJavaTelegramBotApiVersion = 6.2.0
libJunitVersion = 5.9.0 libJunitVersion = 5.9.0

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

0
gradlew vendored Normal file → Executable file
View File

0
gradlew.bat vendored Normal file → Executable file
View File

View File

@ -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;
}

View File

@ -3,6 +3,9 @@ package cc.sukazyo.cono.morny;
import cc.sukazyo.messiva.Logger; import cc.sukazyo.messiva.Logger;
import cc.sukazyo.messiva.appender.ConsoleAppender; import cc.sukazyo.messiva.appender.ConsoleAppender;
import java.io.PrintWriter;
import java.io.StringWriter;
/** /**
* Morny log 管理器 * Morny log 管理器
*/ */
@ -15,4 +18,10 @@ public class Log {
*/ */
public static final Logger logger = new Logger(new ConsoleAppender()); 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();
}
} }

View File

@ -14,7 +14,6 @@ import com.pengrad.telegrambot.request.GetMe;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Set;
import static cc.sukazyo.cono.morny.Log.logger; import static cc.sukazyo.cono.morny.Log.logger;
@ -27,6 +26,9 @@ public class MornyCoeur {
/** 当前程序的 Morny Coeur 实例 */ /** 当前程序的 Morny Coeur 实例 */
private static MornyCoeur INSTANCE; private static MornyCoeur INSTANCE;
/** 当前 Morny 的启动配置 */
public final MornyConfig config;
/** 当前 Morny 的{@link MornyTrusted 信任验证机}实例 */ /** 当前 Morny 的{@link MornyTrusted 信任验证机}实例 */
private final MornyTrusted trusted; private final MornyTrusted trusted;
/** 当前 Morny 的 telegram 命令管理器 */ /** 当前 Morny 的 telegram 命令管理器 */
@ -36,7 +38,6 @@ public class MornyCoeur {
/** morny 的 bot 账户 */ /** morny 的 bot 账户 */
private final TelegramBot account; private final TelegramBot account;
private final ExtraAction extraActionInstance; private final ExtraAction extraActionInstance;
private final boolean isRemoveCommandListWhenExit;
/** /**
* morny bot 账户的用户名<br> * morny bot 账户的用户名<br>
* <br> * <br>
@ -53,58 +54,38 @@ public class MornyCoeur {
* 这个字段将会在登陆成功后赋值为登录到的 bot id * 这个字段将会在登陆成功后赋值为登录到的 bot id
*/ */
public final long userid; public final long userid;
/**
* morny 的事件忽略前缀时间<br>
* <br>
* {@link cc.sukazyo.cono.morny.bot.event.OnUpdateTimestampOffsetLock}
* 会根据这里定义的时间戳取消掉比此时间更早的事件链
*/
public final long latestEventTimestamp;
/** /**
* morny 主程序启动时间<br> * morny 主程序启动时间<br>
* 用于统计数据 * 用于统计数据
*/ */
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) { } private record LogInResult(TelegramBot account, String username, long userid) { }
/** /**
* 执行 bot 初始化 * 执行 bot 初始化
* *
* @param botKey bot telegram bot api token * @param config Morny 实例的配置选项数据
* @param botUsername bot username 限定如果为 null 则表示不限定
* 如果指定则登录时会检查所登陆的 bot 的用户名是否与此相等
* @param master morny 实例所信任的主人的 id用于初始化 {@link #trusted}
* @param trustedChat morny 实例所信任的群组的 id用于初始化 {@link #trusted}
* @param latestEventTimestamp 事件处理器会处理事件的最早时间戳
* 只有限定的 message 事件会受此影响
* 单位为毫秒
*/ */
private MornyCoeur ( private MornyCoeur (MornyConfig config) {
@Nullable String botApi, @Nullable String botApi4File,
@Nonnull String botKey, @Nullable String botUsername, this.config = config;
long master, long trustedChat, Set<Long> trustedRDinner,
long latestEventTimestamp,
boolean isRemoveCommandListWhenExit
) {
this.latestEventTimestamp = latestEventTimestamp;
this.isRemoveCommandListWhenExit = isRemoveCommandListWhenExit;
configureSafeExit(); configureSafeExit();
logger.info("args key:\n " + botKey); logger.info("args key:\n " + config.telegramBotKey);
if (botUsername != null) { if (config.telegramBotUsername != null) {
logger.info("login as:\n " + botUsername); logger.info("login as:\n " + config.telegramBotUsername);
} }
try { 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.account = loginResult.account;
this.username = loginResult.username; this.username = loginResult.username;
this.userid = loginResult.userid; this.userid = loginResult.userid;
this.trusted = new MornyTrusted(master, trustedChat, trustedRDinner); this.trusted = new MornyTrusted(this);
StringBuilder trustedReadersDinnerIds = new StringBuilder(); StringBuilder trustedReadersDinnerIds = new StringBuilder();
trusted.getTrustedReadersOfDinnerSet().forEach(id -> trustedReadersDinnerIds.append("\n ").append(id)); trusted.getTrustedReadersOfDinnerSet().forEach(id -> trustedReadersDinnerIds.append("\n ").append(id));
logger.info(String.format(""" logger.info(String.format("""
@ -114,7 +95,7 @@ public class MornyCoeur {
- trusted chat (id) - trusted chat (id)
%d %d
- trusted reader-of-dinner (id)%s""", - trusted reader-of-dinner (id)%s""",
master, trustedChat, trustedReadersDinnerIds config.trustedMaster, config.trustedChat, trustedReadersDinnerIds
)); ));
} }
catch (Exception e) { catch (Exception e) {
@ -135,32 +116,28 @@ public class MornyCoeur {
* 如果 morny 已经初始化则不会进行初始化抛出错误消息并直接退出方法 * 如果 morny 已经初始化则不会进行初始化抛出错误消息并直接退出方法
* *
* @see #MornyCoeur 程序初始化方法 * @see #MornyCoeur 程序初始化方法
* @param config morny 实例的配置选项数据
*/ */
public static void main ( public static void main (MornyConfig config) {
@Nullable String botApi, @Nullable String botApi4File,
@Nonnull String botKey, @Nullable String botUsername,
long master, long trustedChat, Set<Long> trustedRDinner, long latestEventTimestamp,
boolean isAutomaticResetCommandList, boolean isRemoveCommandListWhenExit
) {
if (INSTANCE == null) { if (INSTANCE == null) {
logger.info("Coeur Starting"); logger.info("Coeur Starting");
INSTANCE = new MornyCoeur( INSTANCE = new MornyCoeur(config);
botApi, botApi4File,
botKey, botUsername,
master, trustedChat, trustedRDinner,
latestEventTimestamp,
isRemoveCommandListWhenExit
);
MornyDaemons.start(); MornyDaemons.start();
logger.info("start telegram events listening"); logger.info("start telegram events listening");
EventListeners.registerAllListeners(); EventListeners.registerAllListeners();
INSTANCE.account.setUpdatesListener(OnUpdate::onNormalUpdate); INSTANCE.account.setUpdatesListener(OnUpdate::onNormalUpdate);
if (isAutomaticResetCommandList) {
if (config.commandLoginRefresh) {
logger.info("resetting telegram command list"); logger.info("resetting telegram command list");
commandManager().automaticUpdateList(); commandManager().automaticUpdateList();
} }
logger.info("Coeur start complete"); logger.info("Coeur start complete");
return; return;
} }
logger.error("Coeur already started!!!"); logger.error("Coeur already started!!!");
} }
@ -177,9 +154,8 @@ public class MornyCoeur {
* 用于退出时进行缓存的任务处理等进行安全退出 * 用于退出时进行缓存的任务处理等进行安全退出
*/ */
private void exitCleanup () { private void exitCleanup () {
logger.info("clean:save tracker data.");
MornyDaemons.stop(); MornyDaemons.stop();
if (isRemoveCommandListWhenExit) { if (config.commandLogoutClear) {
commandManager.automaticRemoveList(); commandManager.automaticRemoveList();
} }
} }
@ -192,16 +168,20 @@ public class MornyCoeur {
} }
/** /**
* 登录 bot<br> * 登录 bot.
* <br> * <p>
* 会反复尝试三次进行登录如果登录失败则会直接抛出 RuntimeException 结束处理 * 会反复尝试三次进行登录如果登录失败则会直接抛出 RuntimeException 结束处理
* 会通过 GetMe 动作验证是否连接上了 telegram api 服务器 * 会通过 GetMe 动作验证是否连接上了 telegram api 服务器
* 同时也要求登录获得的 username {@link #username} 声明值相等 * 同时也要求登录获得的 username {@link #username} 声明值相等
* *
* @param api bot client 将会连接到的 telegram bot api 位置 * @param api bot client 将会连接到的 telegram bot api 位置
* @param api4File bot client 将会连接到的 telegram file api 位置如果不指定则会跟随 {@code api} 选项的设定 * 填入 {@code null} 则使用默认的 {@code "https://api.telegram.org/bot"}
* @param key bot api-token * @param api4File bot client 将会连接到的 telegram file api 位置
* @param requireName 要求登录到的需要的 username如果登陆后的 username 与此不同则会报错退出 * 如果传入 {@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} 对象 * @return 成功登录后的 {@link TelegramBot} 对象
*/ */
@Nonnull @Nonnull
@ -244,7 +224,7 @@ public class MornyCoeur {
logger.info("Succeed login to @" + remote.username()); logger.info("Succeed login to @" + remote.username());
return new LogInResult(account, remote.username(), remote.id()); return new LogInResult(account, remote.username(), remote.id());
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(System.out); logger.error(Log.exceptionLog(e));
logger.error("login failed."); logger.error("login failed.");
} }
} }
@ -260,6 +240,14 @@ public class MornyCoeur {
logger.info("done all save action."); logger.info("done all save action.");
} }
/**
* 检查 Coeur 是否已经完成初始化.
* @since 1.0.0-alpha5
*/
public static boolean available() {
return INSTANCE != null;
}
/** /**
* 获取登录成功后的 telegram bot 对象 * 获取登录成功后的 telegram bot 对象
* *
@ -281,13 +269,13 @@ public class MornyCoeur {
} }
/** /**
* 获取当前 morny 的配置数据
* *
* 获取忽略时间点 * @return {@link #config MornyCoeur.config}
*
* @return {@link #latestEventTimestamp MornyCoeur.latestEventTimestamp}
*/ */
public static long getLatestEventTimestamp () { @Nonnull
return INSTANCE.latestEventTimestamp; public static MornyConfig config () {
return INSTANCE.config;
} }
/** /**
@ -317,4 +305,13 @@ public class MornyCoeur {
public static long getUserid () { return INSTANCE.userid; } 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;
}
} }

View File

@ -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 服务器.
* <p>
* 不设定的话默认将会使用 {@code https://api.telegram.org/bot}
*/
@Nullable public final String telegramBotApiServer;
/**
* Morny Telegram 使用的 API 服务器的 file 服务路径.
* <p>
* 不设定的话默认将会使用 {@value com.pengrad.telegrambot.impl.FileApi#FILE_API}
*/
@Nullable public final String telegramBotApiServer4File;
/**
* morny 使用的 telegram bot bot api token.
* <p>
* 这个值必须设定
*/
@Nonnull @Sensitive public final String telegramBotKey;
/**
* morny 所使用的 bot username.
* <p>
* 如果设定了这个值则在 morny 登录 bot 时将会检查所登录的 bot username 是否和这里设定的 username 匹配
* 如果不匹配则会拒绝登录然后报错
* <p>
* 如果没有设定这个值则不会对登录 bot username 进行限制
*/
@Nullable public final String telegramBotUsername;
/* ======================================= *
* morny trusted config *
* ======================================= */
/**
* morny 的主人.
* <p>
* 这项值的对象总是会被{@link MornyTrusted 信任管理器}认为是可信任的
*/
public final long trustedMaster;
/**
* morny 可信群聊的 id.
* <p>
* {@link MornyTrusted 信任管理器}将会认为这个群聊中的所有拥有
* {@link com.pengrad.telegrambot.model.ChatMember.Status#administrator administrator} 权限的成员是可信任的
* <p>
* id 需要符合 bot api 标准
*/
public final long trustedChat;
/* ======================================= *
* system: event ignore *
* ======================================= */
public final boolean eventIgnoreOutdated;
/**
* morny 的事件忽略前缀时间<br>
* <br>
* {@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<Long> 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<Long> dinnerTrustedReaders = new HashSet<>();
public long dinnerChatId = -1001707106392L;
public long reportToChat = -1001650050443L;
}
}

View File

@ -1,8 +1,11 @@
package cc.sukazyo.cono.morny; 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 cc.sukazyo.cono.morny.util.FileUtils;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -13,20 +16,108 @@ import java.security.NoSuchAlgorithmException;
public class MornySystem { public class MornySystem {
/** /**
* 程序的语义化版本号<br> * 程序的语义化版本号.
* 会由 gradle 任务 {@code updateVersionCode} 更新 * <p>
* 这个版本号包含了以下的 {@link #VERSION_BASE}, {@link #VERSION_DELTA} 字段
* 但不包含作为附加属性的构建时的{@link BuildConfig#COMMIT git 状态}属性
* <p>
* 这个格式的版本号也是在 maven 包仓库中所使用的版本号
* @since 1.0.0-alpha4
*/ */
public static final String VERSION = GradleProjectConfigures.VERSION; @BuildConfigField @Nonnull public static final String VERSION = BuildConfig.VERSION;
/**
* 程序的完整语义化版本号.
* <p>
* 包含了全部的 {@link #VERSION_BASE}, {@link #VERSION_DELTA}, 以及{@link BuildConfig#COMMIT git 状态}属性
* <small>虽然仍旧不包含{@link #CODENAME}属性</small>
* <p>
* 这个格式的版本号也是 gradle 构建配置使用的版本号也在普通打包时生成文件时使用
* @since 1.0.0-alpha4.2
*/
@BuildConfigField @Nonnull public static final String VERSION_FULL = BuildConfig.VERSION_FULL;
/**
* 程序的基础版本号.
* <p>
* 它只包含了版本号中的主要信息例如 {@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.
* 设计上用于在一个基版本当中分出不同构建的版本.
* <p>
* {@link null} 作为值表示这个字段没有被使用.
* <p>
* 版本 delta 会以 {@code -δversion-delta} 的形式附着在 {@link #VERSION_BASE} 之后.
* 两者合并后的版本号格式即为 {@link #VERSION}
* <p>
* 在发行版本中一般不应该被使用.
* <p>
* <small>目前并不多被使用.</small>
* @since 1.0.0-alpha4
*/
@BuildConfigField @Nullable public static final String VERSION_DELTA = BuildConfig.VERSION_DELTA;
/** /**
* Morny Coeur 当前的版本代号.<br> * Morny Coeur 当前的版本代号.
* <p>
* 一个单个单词一般作为一个大版本的名称只在重大更新改变<br> * 一个单个单词一般作为一个大版本的名称只在重大更新改变<br>
* 格式保持为仅由小写字母和数字组成<br> * 格式保持为仅由小写字母和数字组成<br>
* 有时也可能是复合词或特殊的词句<br> * 有时也可能是复合词或特殊的词句<br>
* <br> * <br>
* 会由 gradle 任务 {@code updateVersionCode} 更新
*/ */
public static final String CODENAME = GradleProjectConfigures.CODENAME; @BuildConfigField @Nonnull public static final String CODENAME = BuildConfig.CODENAME;
/**
* Coeur 的代码仓库的链接. 它应该链接到当前程序的源码主页.
* <p>
* {@link null} 表示这个属性在构建时未被设置或没有源码主页
* @since 1.0.0-alpha4
*/
@BuildConfigField @Nullable public static final String CODE_STORE = BuildConfig.CODE_STORE;
/**
* Coeur git commit 链接.
* <p>
* 它应该是一个可以通过 {@link String#format(String, Object...)} 要求格式的链接模板带有一个 {@link String} 类型的槽位
* 通过 <code>String.format(COMMIT_PATH, {@link BuildConfig#COMMIT})</code> 即可取得当前当前程序所基于的 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 的链接.
* <p>
* 如果 {@link #COMMIT_PATH}<small>一般表示没有公开储存库</small>
* 或是 {@link BuildConfig#COMMIT}<small>一般表示程序的构建环境没有使用 git</small>
* 任何一个不可用则此方法也不可用
*
* @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 <br> * 获取程序 jar 文件的 md5-hash <br>
@ -43,10 +134,10 @@ public class MornySystem {
try { try {
return FileUtils.getMD5Three(MornyCoeur.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); return FileUtils.getMD5Three(MornyCoeur.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
} catch (IOException | URISyntaxException e) { } catch (IOException | URISyntaxException e) {
e.printStackTrace(System.out);
return "<non-jar-runtime>"; return "<non-jar-runtime>";
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
e.printStackTrace(System.out); Log.logger.error(Log.exceptionLog(e));
MornyReport.exception(e, "<coeur-md5/calculation-error>");
return "<calculation-error>"; return "<calculation-error>";
} }
} }

View File

@ -1,7 +1,7 @@
package cc.sukazyo.cono.morny; package cc.sukazyo.cono.morny;
import com.pengrad.telegrambot.model.ChatMember.Status; import com.pengrad.telegrambot.model.ChatMember.Status;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
/** /**
@ -9,27 +9,10 @@ import java.util.Set;
*/ */
public class MornyTrusted { public class MornyTrusted {
/** private final MornyCoeur instance;
* 群聊id其指向的群聊指示了哪个群的成员是受信任的
* @see #isTrusted(long) 受信检查
*/
public final Long TRUSTED_CHAT_ID;
/** public MornyTrusted (MornyCoeur instance) {
* morny 的主人<br> this.instance = instance;
* 这项值的对象总是会被认为是可信任的
*/
public final long MASTER;
private final Set<Long> TRUSTED_READERS_OF_DINNER;
public MornyTrusted (long master, long trustedChatId, Set<Long> trustedRDinner) {
this.TRUSTED_CHAT_ID = trustedChatId;
this.MASTER = master;
this.TRUSTED_READERS_OF_DINNER = new HashSet<>(){{
this.add(master);
this.addAll(trustedRDinner);
}};
} }
/** /**
@ -37,22 +20,23 @@ public class MornyTrusted {
* <br> * <br>
* 用户需要受信任才能执行一些对程序甚至是宿主环境而言危险的操作例如关闭程序<br> * 用户需要受信任才能执行一些对程序甚至是宿主环境而言危险的操作例如关闭程序<br>
* <br> * <br>
* 它的逻辑(目前)是检查群聊 {@link #TRUSTED_CHAT_ID} 中这个用户是否为群组管理员 * 它的逻辑(目前)是检查群聊 {@link MornyConfig#trustedChat} 中这个用户是否为群组管理员
* *
* @param userId 需要检查的用户的id * @param userId 需要检查的用户的id
* @return 所传递的用户id对应的用户是否受信任 * @return 所传递的用户id对应的用户是否受信任
*/ */
public boolean isTrusted (long userId) { public boolean isTrusted (long userId) {
if (userId == MASTER) return true; if (userId == instance.config.trustedMaster) return true;
return MornyCoeur.extra().isUserInGroup(userId, TRUSTED_CHAT_ID, Status.administrator); if (instance.config.trustedChat == -1) return false;
return MornyCoeur.extra().isUserInGroup(userId, instance.config.trustedChat, Status.administrator);
} }
public boolean isTrustedForDinnerRead (long userId) { public boolean isTrustedForDinnerRead (long userId) {
return TRUSTED_READERS_OF_DINNER.contains(userId); return instance.config.dinnerTrustedReaders.contains(userId);
} }
public Set<Long> getTrustedReadersOfDinnerSet () { public Set<Long> getTrustedReadersOfDinnerSet () {
return Set.copyOf(TRUSTED_READERS_OF_DINNER); return Set.copyOf(instance.config.dinnerTrustedReaders);
} }
} }

View File

@ -4,9 +4,6 @@ import cc.sukazyo.cono.morny.util.CommonFormat;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.util.HashSet;
import java.util.Set;
import static cc.sukazyo.cono.morny.Log.logger; import static cc.sukazyo.cono.morny.Log.logger;
/** /**
@ -18,8 +15,7 @@ import static cc.sukazyo.cono.morny.Log.logger;
*/ */
public class ServerMain { public class ServerMain {
public static final String PROP_TOKEN_KEY = "TELEGRAM_BOT_API_TOKEN"; public static final long systemStartupTime = System.currentTimeMillis();
public static final String PROP_TOKEN_MORNY_KEY = "MORNY_TG_TOKEN";
private static final String THREAD_MORNY_INIT = "morny-init"; 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 --api-files} 单独设定 {@link MornyCoeur#getAccount() bot client} 使用的 telegram bot file api server
* </li> * </li>
* <li> * <li>
* {@code --report-to} 设定 {@link cc.sukazyo.cono.morny.daemon.MornyReport} 的运行报告要发送到的 telegram 频道
* </li>
* <li>
* {@code --no-hello} 不在主程序启动时输出用于欢迎消息的字符画 * {@code --no-hello} 不在主程序启动时输出用于欢迎消息的字符画
* {@code --only-hello} 参数不兼容 会导致程序完全没有任何输出 * {@code --only-hello} 参数不兼容 会导致程序完全没有任何输出
* </li> * </li>
* <li> * <li>
* {@code --outdated-block} 会使得 {@link MornyCoeur#latestEventTimestamp} * {@code --outdated-block} 会使得 {@link MornyConfig#eventIgnoreOutdated}
* 赋值为程序启动的时间从而造成阻挡程序启动之前的消息事件处理效果 * 赋值为程序启动的时间从而造成阻挡程序启动之前的消息事件处理效果
* </li> * </li>
* <li> * <li>
@ -85,20 +84,12 @@ public class ServerMain {
//# //#
//# 启动参数设置区块 //# 启动参数设置区块
//# //#
final MornyConfig.Prototype config = new MornyConfig.Prototype();
boolean versionEchoMode = false; boolean versionEchoMode = false;
boolean welcomeEchoMode = false; boolean welcomeEchoMode = false;
boolean showWelcome = true; boolean showWelcome = true;
String key = null;
String username = null; config.eventOutdatedTimestamp = systemStartupTime;
boolean outdatedBlock = false;
long master = 793274677L;
Set<Long> trustedReadersOfDinner = new HashSet<>();
long trustedChat = -1001541451710L;
boolean autoCmdList = false;
boolean autoCmdRemove = false;
String api = null;
String api4File = null;
for (int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
@ -106,7 +97,7 @@ public class ServerMain {
switch (args[i]) { switch (args[i]) {
case "--outdated-block", "-ob" -> { case "--outdated-block", "-ob" -> {
outdatedBlock = true; config.eventIgnoreOutdated = true;
continue; continue;
} }
case "--no-hello", "-hf", "--quiet", "-q" -> { case "--no-hello", "-hf", "--quiet", "-q" -> {
@ -123,51 +114,56 @@ public class ServerMain {
} }
case "--token", "-t" -> { case "--token", "-t" -> {
i++; i++;
key = args[i]; config.telegramBotKey = args[i];
continue; continue;
} }
case "--username", "-u" -> { case "--username", "-u" -> {
i++; i++;
username = args[i]; config.telegramBotUsername = args[i];
continue; continue;
} }
case "--master", "-mm" -> { case "--master", "-mm" -> {
i++; i++;
master = Long.parseLong(args[i]); config.trustedMaster = Long.parseLong(args[i]);
continue; continue;
} }
case "--trusted-chat", "-trs" -> { case "--trusted-chat", "-trs" -> {
i++; i++;
trustedChat = Long.parseLong(args[i]); config.trustedChat = Long.parseLong(args[i]);
continue; continue;
} }
//noinspection SpellCheckingInspection //noinspection SpellCheckingInspection
case "--trusted-reader-dinner", "-trsd" -> { case "--trusted-reader-dinner", "-trsd" -> {
i++; i++;
trustedReadersOfDinner.add(Long.parseLong(args[i])); config.dinnerTrustedReaders.add(Long.parseLong(args[i]));
continue; continue;
} }
case "--auto-cmd", "-cmd", "-c" -> { case "--auto-cmd", "-cmd", "-c" -> {
autoCmdList = true; config.commandLoginRefresh = true;
autoCmdRemove = true; config.commandLogoutClear = true;
continue; continue;
} }
case "--auto-cmd-list", "-ca" -> { case "--auto-cmd-list", "-ca" -> {
autoCmdList = true; config.commandLoginRefresh = true;
continue; continue;
} }
case "--auto-cmd-remove", "-cr" -> { case "--auto-cmd-remove", "-cr" -> {
autoCmdRemove = true; config.commandLogoutClear = true;
continue; continue;
} }
case "--api", "-a" -> { case "--api", "-a" -> {
i++; i++;
api = args[i]; config.telegramBotApiServer = args[i];
continue; continue;
} }
case "--api-files", "files-api", "-af" -> { case "--api-files", "files-api", "-af" -> {
i++; i++;
api4File = args[i]; config.telegramBotApiServer4File = args[i];
continue;
}
case "--report-to" -> {
i++;
config.reportToChat = Long.parseLong(args[i]);
continue; continue;
} }
} }
@ -180,7 +176,7 @@ public class ServerMain {
String propToken = null; String propToken = null;
String propTokenKey = 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) { if (System.getenv(iKey) != null) {
propToken = System.getenv(iKey); propToken = System.getenv(iKey);
propTokenKey = iKey; propTokenKey = iKey;
@ -196,16 +192,26 @@ public class ServerMain {
logger.info(String.format(""" logger.info(String.format("""
Morny Cono Version Morny Cono Version
- version : - version :
%s %s Morny %s
%s%s
- md5hash : - md5hash :
%s %s
- gitstat :
%s
- co.time : - co.time :
%d %d
%s [UTC]""", %s [UTC]""",
MornySystem.VERSION, MornySystem.CODENAME.toUpperCase(), MornySystem.CODENAME.toUpperCase(),
MornySystem.VERSION_BASE,
MornySystem.isUseDelta() ? ""+MornySystem.VERSION_DELTA : "",
MornySystem.getJarMd5(), MornySystem.getJarMd5(),
GradleProjectConfigures.COMPILE_TIMESTAMP, MornySystem.isGitBuild() ? (String.format(
CommonFormat.formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0) "on commit %s\n %s",
MornySystem.isCleanBuild() ? "- clean-build" : "<δ/non-clean-build>",
BuildConfig.COMMIT
)) : "<non-git-build>",
BuildConfig.CODE_TIMESTAMP,
CommonFormat.formatDate(BuildConfig.CODE_TIMESTAMP, 0)
)); ));
return; return;
@ -216,11 +222,12 @@ public class ServerMain {
logger.info(String.format(""" logger.info(String.format("""
ServerMain.java Loaded >>> ServerMain.java Loaded >>>
- version %s (%s)(%d) - version %s
- Morny %s""", - Morny %s
MornySystem.VERSION, - <%s> [%d]""",
MornySystem.getJarMd5(), GradleProjectConfigures.COMPILE_TIMESTAMP, MornySystem.VERSION_FULL,
MornySystem.CODENAME.toUpperCase() MornySystem.CODENAME.toUpperCase(),
MornySystem.getJarMd5(), BuildConfig.CODE_TIMESTAMP
)); ));
//# //#
@ -228,21 +235,19 @@ public class ServerMain {
//# //#
if (propToken != null) { if (propToken != null) {
key = propToken; config.telegramBotKey = propToken;
logger.info("Parameter <token> set by EnvVar $"+propTokenKey); logger.info("Parameter <token> set by EnvVar $"+propTokenKey);
} }
if (key == null) {
logger.info("Parameter required has no value:\n --token.");
return;
}
Thread.currentThread().setName(THREAD_MORNY_INIT); Thread.currentThread().setName(THREAD_MORNY_INIT);
MornyCoeur.main( try {
api, api4File, MornyCoeur.main(new MornyConfig(config));
key, username, } catch (MornyConfig.CheckFailure.NullTelegramBotKey ignore) {
master, trustedChat, trustedReadersOfDinner, logger.info("Parameter required has no value:\n --token.");
outdatedBlock?System.currentTimeMillis():0, } catch (MornyConfig.CheckFailure e) {
autoCmdList, autoCmdRemove logger.error("Unknown failure occurred while starting ServerMain!:");
); e.printStackTrace(System.out);
}
} }

View File

@ -1,5 +1,7 @@
package cc.sukazyo.cono.morny.bot.api; 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 cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
@ -32,27 +34,20 @@ public class EventListenerManager {
if (exec.apply(x)) return; 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) { } catch (Exception e) {
logger.error("Event Error!"); final StringBuilder errorMessage = new StringBuilder();
e.printStackTrace(System.out); 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");
} }
} }

View File

@ -1,6 +1,7 @@
package cc.sukazyo.cono.morny.bot.command; package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur; 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.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.CommonConvert; import cc.sukazyo.cono.morny.util.CommonConvert;
import cc.sukazyo.cono.morny.util.CommonEncrypt; import cc.sukazyo.cono.morny.util.CommonEncrypt;
@ -88,6 +89,7 @@ public class Encryptor implements ITelegramCommand {
)).file()); )).file());
} catch (IOException e) { } catch (IOException e) {
logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage()); logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage());
MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI");
MornyCoeur.extra().exec(new SendSticker( MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(), event.message().chat().id(),
TelegramStickers.ID_NETWORK_ERR TelegramStickers.ID_NETWORK_ERR
@ -110,6 +112,7 @@ public class Encryptor implements ITelegramCommand {
)).file()); )).file());
} catch (IOException e) { } catch (IOException e) {
logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage()); logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage());
MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI");
MornyCoeur.extra().exec(new SendSticker( MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(), event.message().chat().id(),
TelegramStickers.ID_NETWORK_ERR TelegramStickers.ID_NETWORK_ERR

View File

@ -1,8 +1,8 @@
package cc.sukazyo.cono.morny.bot.command; package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.GradleProjectConfigures;
import cc.sukazyo.cono.morny.MornyCoeur; 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.MornyJrrp;
import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand; import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
@ -19,16 +19,9 @@ import org.jetbrains.annotations.NotNull;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.net.InetAddress; import java.util.*;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static cc.sukazyo.cono.morny.Log.logger; 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; import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
public class MornyCommands { public class MornyCommands {
@ -85,12 +78,15 @@ public class MornyCommands {
// 统一注册这些奇怪的东西&.& // 统一注册这些奇怪的东西&.&
register( register(
new 私わね(),
new 喵呜.Progynova()
);
// special: 注册出于兼容使用的特别 event 的数据
OnUniMeowTrigger.register(
new 喵呜.抱抱(), new 喵呜.抱抱(),
new 喵呜.揉揉(), new 喵呜.揉揉(),
new 喵呜.蹭蹭(), new 喵呜.蹭蹭(),
new 喵呜.贴贴(), new 喵呜.贴贴()
new 私わね(),
new 喵呜.Progynova()
); );
} }
@ -215,7 +211,7 @@ public class MornyCommands {
).replyToMessageId(event.message().messageId()) ).replyToMessageId(event.message().messageId())
); );
logger.info("Morny exited by user " + TGToString.as(event.message().from()).toStringLogTag()); logger.info("Morny exited by user " + TGToString.as(event.message().from()).toStringLogTag());
System.exit(0); MornyCoeur.exit(0, event.message().from());
} else { } else {
MornyCoeur.extra().exec(new SendSticker( MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(), event.message().chat().id(),
@ -223,102 +219,24 @@ public class MornyCommands {
).replyToMessageId(event.message().messageId()) ).replyToMessageId(event.message().messageId())
); );
logger.info("403 exited tag from user " + TGToString.as(event.message().from()).toStringLogTag()); 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"; } @Nonnull @Override public String getName () { return "version"; }
@Nullable @Override public String[] getAliases () { return null; } @Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return ""; } @Nonnull @Deprecated public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "检查 Bot 版本信息"; } @Nonnull @Deprecated public String getDescription () { return "检查 Bot 版本信息"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandVersionExec(event); } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformations.echoVersion(event); }
}
private static void onCommandVersionExec (@Nonnull Update event) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
String.format(
"""
version:
- Morny <code>%s</code>
- <code>%s</code>
core md5_hash:
- <code>%s</code>
compile timestamp:
- <code>%d</code>
- <code>%s [UTC]</code>""",
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));
} }
private static class MornyRuntime implements ITelegramCommand { private static class MornyRuntime implements ISimpleCommand {
@Nonnull @Override public String getName () { return "runtime"; } @Nonnull @Override public String getName () { return "runtime"; }
@Nullable @Override public String[] getAliases () { return null; } @Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return ""; } @Nonnull @Deprecated public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; } @Nonnull @Deprecated public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandRuntimeExec(event); } @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformations.echoRuntime(event); }
}
/**
* @since 0.4.1.2
*/
private static void onCommandRuntimeExec (@Nonnull Update event) {
String hostname;
try {
hostname = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
hostname = "<unknown>";
}
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
String.format("""
system:
- <code>%s</code>
- <code>%s</code>
- <code>%s</code>
java runtime:
- <code>%s</code>
- <code>%s</code>
vm memory:
- <code>%d</code> / <code>%d</code> MB
- <code>%d</code> cores
coeur version:
- <code>%s</code> (<code>%s</code>)
- <code>%s</code>
- <code>%s [UTC]</code>
- [<code>%d</code>]
continuous:
- <code>%s</code>
- [<code>%d</code>]
- <code>%s [UTC]</code>
- [<code>%d</code>]""",
// 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));
} }
private static class Jrrp implements ITelegramCommand { private static class Jrrp implements ITelegramCommand {
@ -367,6 +285,7 @@ public class MornyCommands {
).replyToMessageId(event.message().messageId()) ).replyToMessageId(event.message().messageId())
); );
logger.info("403 call save tag from user " + TGToString.as(event.message().from()).toStringLogTag()); logger.info("403 call save tag from user " + TGToString.as(event.message().from()).toStringLogTag());
MornyReport.unauthenticatedAction("/save", event.message().from());
} }
} }

View File

@ -1,46 +1,236 @@
package cc.sukazyo.cono.morny.bot.command; package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.BuildConfig;
import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.MornySystem;
import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.tgapi.ExtraAction;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand; import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update; 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.SendSticker;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; 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 { 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"; } @Nonnull @Override public String getName () { return "info"; }
@Nullable @Override public String[] getAliases () { return new String[0]; } @Nullable @Override public String[] getAliases () { return new String[0]; }
@Nonnull @Override public String getParamRule () { return "[(stickers)|(stickers.)sticker_id]"; } @Nonnull @Override public String getParamRule () { return "[subcommand]"; }
@Nonnull @Override public String getDescription () { return "输出 Morny 当前版本的一些预定义信息"; } @Nonnull @Override public String getDescription () { return "输出当前 Morny 的各种信息"; }
@Override @Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) { public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
if (!command.hasArgs() || command.getArgs().length > 1) { if (!command.hasArgs()) {
MornyCoeur.extra().exec(new SendSticker(event.message().chat().id(), TelegramStickers.ID_404).replyToMessageId(event.message().messageId())); echoRuntime(event);
return;
} }
final String action = command.getArgs()[0]; final String action = command.getArgs()[0];
if (action.startsWith("stickers")) { if (action.startsWith(SUB_STICKER)) {
if (action.equals("stickers")) echoStickers(command, event);
TelegramStickers.echoAllStickers(MornyCoeur.extra(), event.message().chat().id(), event.message().messageId()); } else if (action.equals(SUB_RUNTIME)) {
else { echoRuntime(event);
TelegramStickers.echoStickerByID( } else if (action.equals(SUB_VERSION) || action.equals(SUB_VERSION_2)) {
action.substring((ACT_STICKER+".").length()), echoVersion(event);
MornyCoeur.extra(), event.message().chat().id(), event.message().messageId() } else {
); echo404(event);
}
return;
} }
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:
- <code>%s</code>
- <code>%s</code> (<code>%s</code>) <code>%s</code>
java runtime:
- <code>%s</code>
- <code>%s</code>
vm memory:
- <code>%d</code> / <code>%d</code> MB
- <code>%d</code> cores
coeur version:
- %s
- <code>%s</code>
- <code>%s [UTC]</code>
- [<code>%d</code>]
continuous:
- <code>%s</code>
- [<code>%d</code>]
- <code>%s [UTC]</code>
- [<code>%d</code>]""",
// system
escapeHtml(getRuntimeHostName()==null ? "<unknown-host>" : 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 <code>%s</code>
- <code>%s</code>%s%s
core md5_hash:
- <code>%s</code>
coding timestamp:
- <code>%d</code>
- <code>%s [UTC]</code>""",
escapeHtml(MornySystem.CODENAME.toUpperCase()),
escapeHtml(MornySystem.VERSION_BASE),
MornySystem.isUseDelta() ? String.format("-δ<code>%s</code>", 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 格式类似于 <u>{@code 28e8c82a.δ}</u> 的以 HTML 方式格式化的版本号组件
* 其中 {@code .δ} 对应着 {@link MornySystem#isCleanBuild}
* commit tag 字段如果支援 {@link MornySystem#currentCodePath} 则会以链接形式解析否则则为 code 格式
* <small>为了对 telegram api html 格式兼容所以不支援嵌套链接与code标签</small>
* 如果 {@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("<code>%s</code>", BuildConfig.COMMIT.substring(0, 8)));
else g.append(String.format("<a href='%s'>%s</a>", MornySystem.currentCodePath(), BuildConfig.COMMIT.substring(0, 8)));
if (!MornySystem.isCleanBuild()) g.append(".<code>δ</code>");
return g.toString();
}
/**
* 取得完整 Morny 版本的 HTML 格式化标签.
* <p>
* 相比于 {@link MornySystem#VERSION_FULL}这个版本号还包含了 {@link MornySystem#CODENAME 版本 codename}
* 各个部分也被以 HTML 的格式进行了格式化以可以更好的在富文本中插入使用.
* @return 基于 HTML 标签进行了格式化了的类似于
* <u><code>{@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}</code></u>
* 的版本号
* @since 1.0.0-beta2
*/
@Nonnull
public static String getVersionAllFullTagHtml () {
final StringBuilder v = new StringBuilder();
v.append("<code>").append(MornySystem.VERSION_BASE).append("</code>");
if (MornySystem.isUseDelta()) v.append("-δ<code>").append(MornySystem.VERSION_DELTA).append("</code>");
if (MornySystem.isGitBuild()) v.append("+git.").append(getVersionGitTagHtml());
v.append("*<code>").append(MornySystem.CODENAME.toUpperCase()).append("</code>");
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()));
} }
} }

View File

@ -28,7 +28,7 @@ public class Testing implements ISimpleCommand {
MornyCoeur.extra().exec(new SendMessage( MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(), event.message().chat().id(),
"<b>Just<b/> a TEST command." "<b>Just</b> a TEST command."
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML)); ).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
} }

View File

@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur; import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand; 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.Update;
import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.request.SendMessage;
@ -11,6 +12,16 @@ import com.pengrad.telegrambot.request.SendSticker;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
/**
* <b>WARNING</b> that {@link cc.sukazyo.cono.morny.bot.event.OnTelegramCommand}
* 并不能够处理非 english word 字符之外的命令.
* <p>
* 出于这个限制以下几个命令目前都无法使用
* @see 抱抱
* @see 揉揉
* @see 蹭蹭
* @see 贴贴
*/
@SuppressWarnings("NonAsciiCharacters") @SuppressWarnings("NonAsciiCharacters")
public class 喵呜 { public class 喵呜 {
@ -18,10 +29,7 @@ public class 喵呜 {
@Nonnull @Override public String getName () { return "抱抱"; } @Nonnull @Override public String getName () { return "抱抱"; }
@Nullable @Override public String[] getAliases () { return new String[0]; } @Nullable @Override public String[] getAliases () { return new String[0]; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
MornyCoeur.extra().exec(new SendMessage( replyingSet(event, "抱抱", "抱抱");
event.message().chat().id(),
"抱抱——"
));
} }
} }
@ -29,10 +37,7 @@ public class 喵呜 {
@Nonnull @Override public String getName () { return "揉揉"; } @Nonnull @Override public String getName () { return "揉揉"; }
@Nullable @Override public String[] getAliases () { return new String[0]; } @Nullable @Override public String[] getAliases () { return new String[0]; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
MornyCoeur.extra().exec(new SendMessage( replyingSet(event, "蹭蹭", "摸摸");
event.message().chat().id(),
"蹭蹭w"
));
} }
} }
@ -40,10 +45,7 @@ public class 喵呜 {
@Nonnull @Override public String getName () { return "蹭蹭"; } @Nonnull @Override public String getName () { return "蹭蹭"; }
@Nullable @Override public String[] getAliases () { return new String[0]; } @Nullable @Override public String[] getAliases () { return new String[0]; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { @Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
MornyCoeur.extra().exec(new SendMessage( replyingSet(event, "揉揉", "蹭蹭");
event.message().chat().id(),
"喵呜~-"
));
} }
} }
@ -51,11 +53,17 @@ public class 喵呜 {
@Nonnull @Override public String getName () { return "贴贴"; } @Nonnull @Override public String getName () { return "贴贴"; }
@Nullable @Override public String[] getAliases () { return new String[0]; } @Nullable @Override public String[] getAliases () { return new String[0]; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { @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( MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(), event.message().chat().id(),
"<tg-spoiler>(贴贴喵呜&amp;.&amp;)</tg-spoiler>" isNew ? whileNew : whileRec
).parseMode(ParseMode.HTML)); ).replyToMessageId(target.messageId()).parseMode(ParseMode.HTML));
}
} }
public static class Progynova implements ITelegramCommand { public static class Progynova implements ITelegramCommand {

View File

@ -22,12 +22,13 @@ public class 私わね implements ISimpleCommand {
public void execute (@Nonnull InputCommand command, @Nonnull Update event) { public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
if (ThreadLocalRandom.current().nextInt(521) == 0) { if (ThreadLocalRandom.current().nextInt(521) == 0) {
// 可以接入未来的心情系统如果有的话 // 可以接入未来的心情系统如果有的话
final String text = switch (ThreadLocalRandom.current().nextInt(11)) { // final String text = switch (ThreadLocalRandom.current().nextInt(11)) {
case 0,7,8,9,10 -> "才不是"; // case 0,7,8,9,10 -> "才不是";
case 1,2,3,6 -> "才不是!"; // case 1,2,3,6 -> "才不是!";
case 4,5 -> "才不是.."; // case 4,5 -> "才不是..";
default -> throw new IllegalStateException("Unexpected random value in 私わね command."); // default -> throw new IllegalStateException("Unexpected random value in 私わね command.");
}; // };
final String text = "/打假";
MornyCoeur.extra().exec(new SendMessage( MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(), event.message().chat().id(),
text text

View File

@ -16,6 +16,7 @@ public class EventListeners {
public static final OnCallMsgSend CALL_MSG_SEND = new OnCallMsgSend(); public static final OnCallMsgSend CALL_MSG_SEND = new OnCallMsgSend();
public static final OnMedicationNotifyApply MEDICATION_NOTIFY_APPLY = new OnMedicationNotifyApply(); public static final OnMedicationNotifyApply MEDICATION_NOTIFY_APPLY = new OnMedicationNotifyApply();
public static final OnRandomlyTriggered RANDOMLY_TRIGGERED = new OnRandomlyTriggered(); public static final OnRandomlyTriggered RANDOMLY_TRIGGERED = new OnRandomlyTriggered();
public static final OnUniMeowTrigger UNI_MEOW_TRIGGER = new OnUniMeowTrigger();
public static void registerAllListeners () { public static void registerAllListeners () {
EventListenerManager.addListener( EventListenerManager.addListener(
@ -24,6 +25,7 @@ public class EventListeners {
/* write functional event behind here */ /* write functional event behind here */
// KUOHUANHUAN_NEED_SLEEP, // KUOHUANHUAN_NEED_SLEEP,
COMMANDS_LISTENER, COMMANDS_LISTENER,
UNI_MEOW_TRIGGER,
RANDOMLY_TRIGGERED, RANDOMLY_TRIGGERED,
USER_RANDOMS, USER_RANDOMS,
USER_SLASH_ACTION, USER_SLASH_ACTION,

View File

@ -1,7 +1,6 @@
package cc.sukazyo.cono.morny.bot.event; package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur; 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.bot.api.EventListener;
import cc.sukazyo.cono.morny.data.TelegramStickers; import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.CommonFormat; import cc.sukazyo.cono.morny.util.CommonFormat;
@ -27,10 +26,10 @@ public class OnCallMe extends EventListener {
/** /**
* 主人的 telegram user id同时被用于 chat id<br> * 主人的 telegram user id同时被用于 chat id<br>
* 跟随 {@link MornyTrusted#MASTER} 的值 * 跟随 {@link cc.sukazyo.cono.morny.MornyConfig#trustedMaster} 的值
* @since 0.4.2.1 * @since 0.4.2.1
*/ */
private static final long ME = MornyCoeur.trustedInstance().MASTER; private static final long ME = MornyCoeur.config().trustedMaster;
/** /**
* 监听私聊 bot 的消息进行呼叫关键字匹配 * 监听私聊 bot 的消息进行呼叫关键字匹配
@ -112,7 +111,7 @@ public class OnCallMe extends EventListener {
boolean isAllowed = false; boolean isAllowed = false;
Message lastDinnerData = null; Message lastDinnerData = null;
if (MornyCoeur.trustedInstance().isTrustedForDinnerRead(event.message().from().id())) { 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( SendResponse sendResp = MornyCoeur.extra().exec(new ForwardMessage(
event.message().from().id(), event.message().from().id(),
lastDinnerData.forwardFromChat().id(), lastDinnerData.forwardFromChat().id(),

View File

@ -27,17 +27,17 @@ import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
public class OnCallMsgSend extends EventListener { 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 ( private record MessageToSend (
String message, @Nullable String message,
MessageEntity[] entities, @Nullable MessageEntity[] entities,
ParseMode parseMode, @Nullable ParseMode parseMode,
long targetId long targetId
) { } ) { }
@Override @Override
public boolean onMessage(Update update) { public boolean onMessage(@Nonnull Update update) {
// 执行体检查 // 执行体检查
if (update.message().chat().type() != Chat.Type.Private) return false; 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); if (update.message().replyToMessage() == null) return answer404(update);
msgsendReqBody = parseRequest(update.message().replyToMessage()); 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)); SendResponse sendResponse = MornyCoeur.getAccount().execute(parseMessageToSend(msgsendReqBody));
if (!sendResponse.isOk()) { // 发送失败 if (!sendResponse.isOk()) { // 发送失败
@ -150,7 +150,8 @@ public class OnCallMsgSend extends EventListener {
).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML)); ).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()) parseMessageToSend(msgsendReqBody, update.message().chat().id()).replyToMessageId(update.message().messageId())
); );
if (!testSendResp.isOk()) { if (!testSendResp.isOk()) {

View File

@ -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<String, ISimpleCommand> 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();
}
}

View File

@ -7,7 +7,7 @@ import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
/** /**
* 阻止 {@link MornyCoeur#latestEventTimestamp 指定时间} 之前的事件处理. * 阻止 {@link cc.sukazyo.cono.morny.MornyConfig#eventOutdatedTimestamp 指定时间} 之前的事件处理.
* <p> * <p>
* 只支持以下事件 * 只支持以下事件
* <ul> * <ul>
@ -27,7 +27,7 @@ public class OnUpdateTimestampOffsetLock extends EventListener {
* @since 0.4.2.7 * @since 0.4.2.7
*/ */
public boolean isOutdated(long timestamp) { public boolean isOutdated(long timestamp) {
return timestamp < MornyCoeur.getLatestEventTimestamp()/1000; return timestamp < MornyCoeur.config().eventOutdatedTimestamp/1000;
} }
@Override @Override

View File

@ -12,6 +12,7 @@ import com.pengrad.telegrambot.response.SendResponse;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static cc.sukazyo.cono.morny.Log.exceptionLog;
import static cc.sukazyo.cono.morny.Log.logger; import static cc.sukazyo.cono.morny.Log.logger;
public class MedicationTimer extends Thread { public class MedicationTimer extends Thread {
@ -40,7 +41,8 @@ public class MedicationTimer extends Thread {
logger.info("MedicationTimer was interrupted, will be exit now"); logger.info("MedicationTimer was interrupted, will be exit now");
} catch (Exception e) { } catch (Exception e) {
logger.error("Unexpected error occurred"); logger.error("Unexpected error occurred");
e.printStackTrace(System.out); logger.error(exceptionLog(e));
MornyReport.exception(e);
} }
} }
logger.info("MedicationTimer stopped"); logger.info("MedicationTimer stopped");

View File

@ -1,5 +1,7 @@
package cc.sukazyo.cono.morny.daemon; package cc.sukazyo.cono.morny.daemon;
import cc.sukazyo.cono.morny.MornyCoeur;
import static cc.sukazyo.cono.morny.Log.logger; import static cc.sukazyo.cono.morny.Log.logger;
public class MornyDaemons { public class MornyDaemons {
@ -10,6 +12,7 @@ public class MornyDaemons {
logger.info("ALL Morny Daemons starting..."); logger.info("ALL Morny Daemons starting...");
// TrackerDataManager.init(); // TrackerDataManager.init();
medicationTimerInstance.start(); medicationTimerInstance.start();
MornyReport.onMornyLogIn();
logger.info("Morny Daemons started."); logger.info("Morny Daemons started.");
} }
@ -23,6 +26,7 @@ public class MornyDaemons {
// TrackerDataManager.trackingLock.lock(); // TrackerDataManager.trackingLock.lock();
try { medicationTimerInstance.join(); } catch (InterruptedException e) { e.printStackTrace(System.out); } try { medicationTimerInstance.join(); } catch (InterruptedException e) { e.printStackTrace(System.out); }
MornyReport.onMornyExit(MornyCoeur.getExitReason());
logger.info("ALL Morny Daemons STOPPED."); logger.info("ALL Morny Daemons STOPPED.");
} }

View File

@ -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 <T extends BaseRequest<T, R>, 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("""
<b>Coeur Unexpected Exception</b>
%s
<pre><code>%s</code></pre>%s
""",
description == null ? "" : escapeHtml(description)+"\n",
escapeHtml(Log.exceptionLog(e)),
e instanceof EventRuntimeException.ActionFailed ? (String.format(
"\n\ntg-api error:\n<pre><code>%s</code></pre>",
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("""
<b>User unauthenticated action</b>
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("""
<b>Morny Logged in</b>
-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("- <i><u>").append(field.getName()).append("</u></i> ");
try {
if (field.isAnnotationPresent(MornyConfig.Sensitive.class)) {
echo.append(": <i>sensitive_field</i>");
} else {
final Object fieldValue = field.get(config);
echo.append("= ");
if (fieldValue == null)
echo.append("null");
else echo.append("<code>").append(escapeHtml(fieldValue.toString())).append("</code>");
}
} catch (IllegalAccessException | IllegalArgumentException | NullPointerException e) {
echo.append(": <i>").append(escapeHtml("<read-error>")).append("</i>");
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 关闭/登出时发送的报告.
* <p>
* 基于 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 = "<code>" + escapeHtml(causedBy.toString()) + "</code>";
}
executeReport(new SendMessage(
MornyCoeur.config().reportToChat,
String.format("""
<b>Morny Exited</b>
from user @%s
%s
""",
MornyCoeur.getUsername(),
causedBy == null ? "with UNKNOWN reason" : "\nby " + causedTag
)
).parseMode(ParseMode.HTML));
}
}

View File

@ -10,6 +10,7 @@ import java.util.HashMap;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import static cc.sukazyo.cono.morny.Log.exceptionLog;
import static cc.sukazyo.cono.morny.Log.logger; import static cc.sukazyo.cono.morny.Log.logger;
public class TrackerDataManager { public class TrackerDataManager {
@ -113,8 +114,10 @@ public class TrackerDataManager {
)); ));
} catch (Exception e) { } catch (Exception e) {
logger.error(String.format("exception in write tracker data: %d/%d/%d", chat, user, timestamp)); final String message = String.format("exception in write tracker data: %d/%d/%d", chat, user, timestamp);
e.printStackTrace(System.out); logger.error(message);
logger.error(exceptionLog(e));
MornyReport.exception(e, message);
} }
} }

View File

@ -5,6 +5,7 @@ import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendSticker; import com.pengrad.telegrambot.request.SendSticker;
import com.pengrad.telegrambot.response.SendResponse; import com.pengrad.telegrambot.response.SendResponse;
import javax.annotation.Nonnull;
import java.lang.reflect.Field; import java.lang.reflect.Field;
/** /**
@ -24,7 +25,14 @@ public class TelegramStickers {
public static final String ID_PROGYNOVA = "CAACAgUAAxkBAAICm2KEuL7UQqNP7vSPCg2DHJIND6UsAAKLAwACH4WSBszIo722aQ3jJAQ"; public static final String ID_PROGYNOVA = "CAACAgUAAxkBAAICm2KEuL7UQqNP7vSPCg2DHJIND6UsAAKLAwACH4WSBszIo722aQ3jJAQ";
public static final String ID_NETWORK_ERR = "CAACAgEAAxkBAAID0WNJgNEkD726KW4vZeFlw0FlVVyNAAIXJgACePzGBb50o7O1RbxoKgQ"; 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()) { for (Field object : TelegramStickers.class.getFields()) {
if (object.getType()==String.class && object.getName().startsWith("ID_")) { 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 { try {
// normally get the sticker and echo // normally get the sticker and echo
Field sticker = TelegramStickers.class.getField(stickerFieldID); Field sticker = TelegramStickers.class.getField(stickerFieldID);

View File

@ -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 {}

View File

@ -9,7 +9,7 @@ public class MornyCLI {
public static void main (String[] args) { public static void main (String[] args) {
Scanner line = new Scanner(System.in); 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(); String x = line.nextLine();
ServerMain.main(UniversalCommand.format(x)); ServerMain.main(UniversalCommand.format(x));