mirror of
https://github.com/Eyre-S/Coeur-Morny-Cono.git
synced 2024-11-22 03:04:54 +08:00
scala port stage3 (not tested)
This commit is contained in:
parent
1a31a22cd9
commit
ddfe77350e
41
build.gradle
41
build.gradle
@ -52,7 +52,7 @@ final JavaVersion proj_java = JavaVersion.VERSION_17
|
||||
final Charset proj_file_encoding = StandardCharsets.UTF_8
|
||||
final proj_scala_api = 3
|
||||
//final proj_scala_lib = proj_scala_api+'.4.0-RC1-bin-20230901-89e8dba-NIGHTLY'
|
||||
final proj_scala_lib = proj_scala_api+'.3.1-RC7'
|
||||
final proj_scala_lib = proj_scala_api+'.3.1'
|
||||
String publish_local_url = null
|
||||
String publish_remote_url = null
|
||||
String publish_remote_username = null
|
||||
@ -89,35 +89,26 @@ dependencies {
|
||||
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
scala { srcDirs = ['src/main/scala', 'src/main/old'] }
|
||||
}
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
|
||||
sourceCompatibility proj_java.getMajorVersion()
|
||||
targetCompatibility proj_java.getMajorVersion()
|
||||
|
||||
options.encoding = proj_file_encoding.name()
|
||||
|
||||
}
|
||||
|
||||
scala {
|
||||
tasks.withType(ScalaCompile).configureEach {
|
||||
|
||||
compileJava {
|
||||
|
||||
sourceCompatibility proj_java.getMajorVersion()
|
||||
targetCompatibility proj_java.getMajorVersion()
|
||||
|
||||
options.encoding = proj_file_encoding.name()
|
||||
|
||||
}
|
||||
sourceCompatibility proj_java.getMajorVersion()
|
||||
targetCompatibility proj_java.getMajorVersion()
|
||||
|
||||
compileScala {
|
||||
|
||||
sourceCompatibility proj_java.getMajorVersion()
|
||||
targetCompatibility proj_java.getMajorVersion()
|
||||
|
||||
options.encoding = proj_file_encoding.name()
|
||||
scalaCompileOptions.encoding = proj_file_encoding.name()
|
||||
|
||||
// scalaCompileOptions.additionalParameters.add("-Yexplicit-nulls")
|
||||
|
||||
}
|
||||
options.encoding = proj_file_encoding.name()
|
||||
scalaCompileOptions.encoding = proj_file_encoding.name()
|
||||
|
||||
scalaCompileOptions.additionalParameters.add "-language:postfixOps"
|
||||
// scalaCompileOptions.additionalParameters.add("-Yexplicit-nulls")
|
||||
|
||||
}
|
||||
|
||||
test {
|
||||
|
@ -8,7 +8,7 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s
|
||||
VERSION = 1.0.0-RC4
|
||||
|
||||
USE_DELTA = true
|
||||
VERSION_DELTA = scalaport2
|
||||
VERSION_DELTA = scalaport3
|
||||
|
||||
CODENAME = beiping
|
||||
|
||||
|
@ -1,58 +0,0 @@
|
||||
package cc.sukazyo.cono.morny;
|
||||
|
||||
import cc.sukazyo.messiva.formatter.SimpleFormatter;
|
||||
import cc.sukazyo.messiva.log.LogLevel;
|
||||
import cc.sukazyo.messiva.logger.Logger;
|
||||
import cc.sukazyo.messiva.appender.ConsoleAppender;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
/**
|
||||
* Morny 的 log 管理器
|
||||
*/
|
||||
public class Log {
|
||||
|
||||
/**
|
||||
* Morny 的 Logger 实例,
|
||||
* messiva 更新
|
||||
* @since 0.4.1.1
|
||||
*/
|
||||
public static final Logger logger = new Logger(new ConsoleAppender(new SimpleFormatter())).minLevel(LogLevel.INFO);
|
||||
|
||||
/**
|
||||
* Is the Debug mode enabled.
|
||||
*
|
||||
* @return if the minimal log level is equal or lower than DEBUG level.
|
||||
*/
|
||||
public static boolean debug () {
|
||||
return logger.levelSetting.minLevel().level <= LogLevel.DEBUG.level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the Debug log output enabled.
|
||||
* <p>
|
||||
* if enable the debug log output, all the Log regardless of LogLevel will be output.
|
||||
* As default, if the debug log output is disabled, Logger will ignore the Logs level lower than INFO.
|
||||
*
|
||||
* @param debug switch enable the debug log output as true, or disable it as false.
|
||||
*/
|
||||
public static void debug (boolean debug) {
|
||||
if (debug) logger.minLevel(LogLevel.ALL);
|
||||
else logger.minLevel(LogLevel.INFO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常的堆栈信息.
|
||||
* @param e 异常体
|
||||
* @return {@link String} 格式的异常的堆栈报告信息.
|
||||
* @see 1.0.0-alpha5
|
||||
*/
|
||||
public static String exceptionLog (@Nonnull Throwable e) {
|
||||
final StringWriter stackTrace = new StringWriter();
|
||||
e.printStackTrace(new PrintWriter(stackTrace));
|
||||
return stackTrace.toString();
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package cc.sukazyo.cono.morny;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Some of the static information of Morny.
|
||||
*/
|
||||
public class MornyAbout {
|
||||
|
||||
/**
|
||||
* ASCII art of Morny Featured Image.
|
||||
* <p>
|
||||
* used for Coeur starting welcome screen.
|
||||
* <p>
|
||||
* stored at <i><u>/assets_morny/texts/server-hello.txt</u></i>
|
||||
*/
|
||||
public static final String MORNY_PREVIEW_IMAGE_ASCII;
|
||||
static {
|
||||
try {
|
||||
MORNY_PREVIEW_IMAGE_ASCII = MornyAssets.pack.getResource("texts/server-hello.txt").readAsString();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Cannot read MORNY_PREVIEW_IMAGE_ASCII from assets pack", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final String MORNY_SOURCECODE_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono";
|
||||
public static final String MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK = "https://storage.sukazyo.cc/Eyre_S/Coeur-Morny-Cono";
|
||||
public static final String MORNY_ISSUE_TRACKER_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono/issues";
|
||||
public static final String MORNY_USER_GUIDE_LINK = "https://book.sukazyo.cc/morny";
|
||||
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package cc.sukazyo.cono.morny;
|
||||
|
||||
import cc.sukazyo.restools.ResourcesPackage;
|
||||
|
||||
/**
|
||||
* Morny assets manager.
|
||||
*
|
||||
* @see #pack
|
||||
* @since 1.0.0-RC4
|
||||
*/
|
||||
public class MornyAssets {
|
||||
|
||||
/**
|
||||
* Instance mirror of the Morny assets, the assets root is <i><u>/src/main/resources/assets_morny/</u></i>.
|
||||
* @since 1.0.0-RC4
|
||||
*/
|
||||
public static final ResourcesPackage pack = new ResourcesPackage(MornyAssets.class, "assets_morny");
|
||||
|
||||
}
|
@ -1,310 +0,0 @@
|
||||
package cc.sukazyo.cono.morny;
|
||||
|
||||
import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener$;
|
||||
import cc.sukazyo.cono.morny.bot.command.MornyCommands;
|
||||
import cc.sukazyo.cono.morny.bot.event.MornyEventListeners;
|
||||
import cc.sukazyo.cono.morny.bot.query.MornyQueries;
|
||||
import cc.sukazyo.cono.morny.daemon.MornyDaemons;
|
||||
import cc.sukazyo.cono.morny.daemon.TrackerDataManager;
|
||||
import cc.sukazyo.cono.morny.util.tgapi.ExtraAction;
|
||||
import com.pengrad.telegrambot.TelegramBot;
|
||||
import com.pengrad.telegrambot.impl.FileApi;
|
||||
import com.pengrad.telegrambot.model.User;
|
||||
import com.pengrad.telegrambot.request.GetMe;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static cc.sukazyo.cono.morny.Log.logger;
|
||||
|
||||
/**
|
||||
* Morny Cono 核心<br>
|
||||
* - 的程序化入口类,保管着 morny 的核心属性<br>
|
||||
*/
|
||||
public class MornyCoeur {
|
||||
|
||||
/** 当前程序的 Morny Coeur 实例 */
|
||||
private static MornyCoeur INSTANCE;
|
||||
|
||||
/** 当前 Morny 的启动配置 */
|
||||
public final MornyConfig config;
|
||||
|
||||
/** 当前 Morny 的{@link MornyTrusted 信任验证机}实例 */
|
||||
private final MornyTrusted trusted;
|
||||
private final MornyQueries queryManager = new MornyQueries();
|
||||
|
||||
/** morny 的 bot 账户 */
|
||||
private final TelegramBot account;
|
||||
private final ExtraAction extraActionInstance;
|
||||
/**
|
||||
* morny 的 bot 账户的用户名<br>
|
||||
* <br>
|
||||
* 这个字段将会在登陆成功后赋值为登录到的 bot 的 username。
|
||||
* 它应该是和 {@link #account} 的 username 同步的<br>
|
||||
* <br>
|
||||
* 如果在登陆之前就定义了此字段,则登陆代码会验证登陆的 bot 的 username
|
||||
* 是否与定义的 username 符合。如果不符合则会报错。
|
||||
*/
|
||||
public final String username;
|
||||
/**
|
||||
* morny 的 bot 账户的 telegram id<br>
|
||||
* <br>
|
||||
* 这个字段将会在登陆成功后赋值为登录到的 bot 的 id。
|
||||
*/
|
||||
public final long userid;
|
||||
/**
|
||||
* morny 主程序启动时间<br>
|
||||
* 用于统计数据
|
||||
*/
|
||||
public static final long coeurStartTimestamp = ServerMain.systemStartupTime();
|
||||
|
||||
private Object whileExitReason = null;
|
||||
|
||||
private record LogInResult(TelegramBot account, String username, long userid) { }
|
||||
|
||||
/**
|
||||
* 执行 bot 初始化
|
||||
*
|
||||
* @param config Morny 实例的配置选项数据
|
||||
*/
|
||||
private MornyCoeur (MornyConfig config) {
|
||||
|
||||
this.config = config;
|
||||
|
||||
configureSafeExit();
|
||||
|
||||
logger.info("args key:\n " + config.telegramBotKey);
|
||||
if (config.telegramBotUsername != null) {
|
||||
logger.info("login as:\n " + config.telegramBotUsername);
|
||||
}
|
||||
|
||||
try {
|
||||
final LogInResult loginResult = login(config.telegramBotApiServer, config.telegramBotApiServer4File, config.telegramBotKey, config.telegramBotUsername);
|
||||
this.account = loginResult.account;
|
||||
this.username = loginResult.username;
|
||||
this.userid = loginResult.userid;
|
||||
this.trusted = new MornyTrusted(this);
|
||||
StringBuilder trustedReadersDinnerIds = new StringBuilder();
|
||||
trusted.getTrustedReadersOfDinnerSet().forEach(id -> trustedReadersDinnerIds.append("\n ").append(id));
|
||||
logger.info(String.format("""
|
||||
trusted param set:
|
||||
- master (id)
|
||||
%d
|
||||
- trusted chat (id)
|
||||
%d
|
||||
- trusted reader-of-dinner (id)%s""",
|
||||
config.trustedMaster, config.trustedChat, trustedReadersDinnerIds
|
||||
));
|
||||
}
|
||||
catch (Exception e) {
|
||||
RuntimeException ex = new RuntimeException("Cannot login to bot/api. :\n " + e.getMessage());
|
||||
logger.error(ex.getMessage());
|
||||
throw ex;
|
||||
}
|
||||
|
||||
this.extraActionInstance = ExtraAction.as(account);
|
||||
|
||||
logger.info("Bot login succeed.");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 向外界暴露的 morny 初始化入口.
|
||||
* <p>
|
||||
* 如果 morny 已经初始化,则不会进行初始化,抛出错误消息并直接退出方法。
|
||||
*
|
||||
* @see #MornyCoeur 程序初始化方法
|
||||
* @param config morny 实例的配置选项数据
|
||||
*/
|
||||
public static void init (MornyConfig config) {
|
||||
if (INSTANCE == null) {
|
||||
|
||||
logger.info("Coeur Starting");
|
||||
INSTANCE = new MornyCoeur(config);
|
||||
|
||||
MornyDaemons.start();
|
||||
|
||||
logger.info("start telegram events listening");
|
||||
MornyEventListeners.registerAllEvents();
|
||||
INSTANCE.account.setUpdatesListener(TelegramUpdatesListener$.MODULE$);
|
||||
|
||||
if (config.commandLoginRefresh) {
|
||||
logger.info("resetting telegram command list");
|
||||
MornyCommands.automaticTGListUpdate();
|
||||
}
|
||||
|
||||
logger.info("Coeur start complete");
|
||||
return;
|
||||
|
||||
}
|
||||
logger.error("Coeur already started!!!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 向所有的数据管理器发起保存数据的指令
|
||||
* @since 0.4.3.0
|
||||
*/
|
||||
public void saveDataAll () {
|
||||
TrackerDataManager.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于退出时进行缓存的任务处理等进行安全退出
|
||||
*/
|
||||
private void exitCleanup () {
|
||||
MornyDaemons.stop();
|
||||
if (config.commandLogoutClear) {
|
||||
MornyCommands.automaticTGListRemove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为程序在虚拟机上添加退出钩子
|
||||
*/
|
||||
private void configureSafeExit () {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::exitCleanup, "exit-cleaning"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录 bot.
|
||||
* <p>
|
||||
* 会反复尝试三次进行登录。如果登录失败,则会直接抛出 RuntimeException 结束处理。
|
||||
* 会通过 GetMe 动作验证是否连接上了 telegram api 服务器,
|
||||
* 同时也要求登录获得的 username 和 {@link #username} 声明值相等
|
||||
*
|
||||
* @param api bot client 将会连接到的 telegram bot api 位置。
|
||||
* 填入 {@code null} 则使用默认的 {@code "https://api.telegram.org/bot"}
|
||||
* @param api4File bot client 将会连接到的 telegram file api 位置。
|
||||
* 如果传入 {@code null} 则会跟随 {@param api} 选项的设定(具体为在 {@param api} 路径的后面添加 {@code /file} 路径)。
|
||||
* 如果两者都为 {@code null},则跟随默认的 {@value FileApi#FILE_API}
|
||||
* @param key bot 的 api-token. 必要值
|
||||
* @param requireName 要求登录到的需要的 username,如果登陆后的 username 与此不同则会报错退出。
|
||||
* 填入 {@code null} 则表示不对 username 作要求
|
||||
* @return 成功登录后的 {@link TelegramBot} 对象
|
||||
*/
|
||||
@Nonnull
|
||||
private static LogInResult login (
|
||||
@Nullable String api, @Nullable String api4File,
|
||||
@Nonnull String key, @Nullable String requireName
|
||||
) {
|
||||
final TelegramBot.Builder accountConfig = new TelegramBot.Builder(key);
|
||||
boolean isCustomApi = false;
|
||||
String apiUrlSet = "https://api.telegram.org/bot";
|
||||
String api4FileUrlSet = FileApi.FILE_API;
|
||||
if (api != null) {
|
||||
api = api.endsWith("/") ? api.substring(0, api.length() - 1) : api;
|
||||
accountConfig.apiUrl(apiUrlSet = api.endsWith("/bot")? api : api + "/bot");
|
||||
isCustomApi = true;
|
||||
}
|
||||
if (api4File != null) {
|
||||
api4File = api4File.endsWith("/") ? api4File : api4File + "/";
|
||||
accountConfig.fileApiUrl(api4FileUrlSet = api4File.endsWith("/file/bot")? api4File : api4File + "/file/bot");
|
||||
isCustomApi = true;
|
||||
} else if (api != null && !api.endsWith("/bot")) {
|
||||
accountConfig.fileApiUrl(api4FileUrlSet = api + "/file/bot");
|
||||
}
|
||||
if (isCustomApi) {
|
||||
logger.info(String.format("""
|
||||
Telegram Bot API set to :
|
||||
- %s
|
||||
- %s""",
|
||||
apiUrlSet, api4FileUrlSet
|
||||
));
|
||||
}
|
||||
final TelegramBot account = accountConfig.build();
|
||||
logger.info("Trying to login...");
|
||||
for (int i = 1; i < 4; i++) {
|
||||
if (i != 1) logger.info("retrying...");
|
||||
try {
|
||||
final User remote = account.execute(new GetMe()).user();
|
||||
if (requireName != null && !requireName.equals(remote.username()))
|
||||
throw new RuntimeException("Required the bot @" + requireName + " but @" + remote.username() + " logged in!");
|
||||
logger.info("Succeed login to @" + remote.username());
|
||||
return new LogInResult(account, remote.username(), remote.id());
|
||||
} catch (Exception e) {
|
||||
logger.error(Log.exceptionLog(e));
|
||||
logger.error("login failed.");
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Login failed..");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #saveDataAll()
|
||||
* @since 0.4.3.0
|
||||
*/
|
||||
public static void callSaveData () {
|
||||
INSTANCE.saveDataAll();
|
||||
logger.info("done all save action.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Coeur 是否已经完成初始化.
|
||||
* @since 1.0.0-alpha5
|
||||
*/
|
||||
public static boolean available() {
|
||||
return INSTANCE != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录成功后的 telegram bot 对象
|
||||
*
|
||||
* @return {@link #account MornyCoeur.account}
|
||||
*/
|
||||
@Nonnull
|
||||
public static TelegramBot getAccount () {
|
||||
return INSTANCE.account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录 bot 的 username
|
||||
*
|
||||
* @return {@link #username MornyCoeur.username}
|
||||
*/
|
||||
@Nonnull
|
||||
public static String getUsername () {
|
||||
return INSTANCE.username;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 morny 的配置数据
|
||||
*
|
||||
* @return {@link #config MornyCoeur.config}
|
||||
*/
|
||||
@Nonnull
|
||||
public static MornyConfig config () {
|
||||
return INSTANCE.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Morny 的{@link MornyTrusted 信任验证机}
|
||||
*
|
||||
* @return {@link #trusted MornyCoeur.trusted}
|
||||
*/
|
||||
@Nonnull
|
||||
public static MornyTrusted trustedInstance () {
|
||||
return INSTANCE.trusted;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static MornyQueries queryManager () {
|
||||
return INSTANCE.queryManager;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static ExtraAction extra () {
|
||||
return INSTANCE.extraActionInstance;
|
||||
}
|
||||
|
||||
public static long getUserid () { return INSTANCE.userid; }
|
||||
|
||||
public static void exit (int status, Object reason) {
|
||||
INSTANCE.whileExitReason = reason;
|
||||
System.exit(status);
|
||||
}
|
||||
|
||||
public static Object getExitReason () {
|
||||
return INSTANCE.whileExitReason;
|
||||
}
|
||||
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
package cc.sukazyo.cono.morny;
|
||||
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport;
|
||||
import cc.sukazyo.cono.morny.internal.BuildConfigField;
|
||||
import cc.sukazyo.cono.morny.util.FileUtils;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Morny Cono 的 Coeur 的程序属性存放类
|
||||
*/
|
||||
public class MornySystem {
|
||||
|
||||
/**
|
||||
* 程序的语义化版本号.
|
||||
* <p>
|
||||
* 这个版本号包含了以下的 {@link #VERSION_BASE}, {@link #VERSION_DELTA} 字段,
|
||||
* 但不包含作为附加属性的构建时的{@link BuildConfig#COMMIT git 状态}属性
|
||||
* <p>
|
||||
* 这个格式的版本号也是在 maven 包仓库中所使用的版本号
|
||||
* @since 1.0.0-alpha4
|
||||
*/
|
||||
@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 当前的版本代号.
|
||||
* <p>
|
||||
* 一个单个单词,一般作为一个大版本的名称,只在重大更新改变<br>
|
||||
* 格式保持为仅由小写字母和数字组成<br>
|
||||
* 有时也可能是复合词或特殊的词句<br>
|
||||
* <br>
|
||||
*/
|
||||
@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>
|
||||
* <br>
|
||||
* 只支持 jar 文件方式启动的程序 ——
|
||||
* 如果是通过 classpath 来启动,程序无法找到本体jar文件,则会返回 {@code <non-jar-runtime>} 文本
|
||||
* <br>
|
||||
* 值格式为 {@link java.lang.String}
|
||||
*
|
||||
* @return 程序jar文件的 md5-hash 值字符串,或 {@code <non-jar-runtime>} 如果出现错误
|
||||
*/
|
||||
@Nonnull
|
||||
public static String getJarMd5() {
|
||||
try {
|
||||
return FileUtils.getMD5Three(MornyCoeur.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
return "<non-jar-runtime>";
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.logger.error(Log.exceptionLog(e));
|
||||
MornyReport.exception(e, "<coeur-md5/calculation-error>");
|
||||
return "<calculation-error>";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package cc.sukazyo.cono.morny;
|
||||
|
||||
import com.pengrad.telegrambot.model.ChatMember.Status;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 对用户进行身份权限验证的管理类
|
||||
*/
|
||||
public class MornyTrusted {
|
||||
|
||||
private final MornyCoeur instance;
|
||||
|
||||
public MornyTrusted (MornyCoeur instance) {
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于检查一个 telegram-user 是否受信任<br>
|
||||
* <br>
|
||||
* 用户需要受信任才能执行一些对程序甚至是宿主环境而言危险的操作,例如关闭程序<br>
|
||||
* <br>
|
||||
* 它的逻辑(目前)是检查群聊 {@link MornyConfig#trustedChat} 中这个用户是否为群组管理员
|
||||
*
|
||||
* @param userId 需要检查的用户的id
|
||||
* @return 所传递的用户id对应的用户是否受信任
|
||||
*/
|
||||
public boolean isTrusted (long userId) {
|
||||
if (userId == instance.config.trustedMaster) return true;
|
||||
if (instance.config.trustedChat == -1) return false;
|
||||
return MornyCoeur.extra().isUserInGroup(userId, instance.config.trustedChat, Status.administrator);
|
||||
}
|
||||
|
||||
public boolean isTrustedForDinnerRead (long userId) {
|
||||
return instance.config.dinnerTrustedReaders.contains(userId);
|
||||
}
|
||||
|
||||
public Set<Long> getTrustedReadersOfDinnerSet () {
|
||||
return Set.copyOf(instance.config.dinnerTrustedReaders);
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.bot.command;
|
||||
|
||||
public class Roll {
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
/**
|
||||
* 一系列的 telegram bot 命令的声明.
|
||||
* <p>
|
||||
* 命令将在 {@link cc.sukazyo.cono.morny.bot.command.MornyCommands} 当中实例化并注册管理,并通过事件
|
||||
* {@link cc.sukazyo.cono.morny.bot.event.OnTelegramCommand} 调用.
|
||||
*/
|
||||
package cc.sukazyo.cono.morny.bot.command;
|
@ -1,28 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.bot.event;
|
||||
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener;
|
||||
import cc.sukazyo.cono.morny.daemon.TrackerDataManager;
|
||||
import com.pengrad.telegrambot.model.Chat;
|
||||
import com.pengrad.telegrambot.model.Update;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
@Deprecated
|
||||
public class OnActivityRecord implements EventListener {
|
||||
|
||||
@Override
|
||||
public boolean onMessage (@Nonnull Update update) {
|
||||
if (
|
||||
update.message().chat().type() == Chat.Type.supergroup ||
|
||||
update.message().chat().type() == Chat.Type.group
|
||||
) {
|
||||
TrackerDataManager.record(
|
||||
update.message().chat().id(),
|
||||
update.message().from().id(),
|
||||
(long)update.message().date() * 1000
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.bot.event;
|
||||
|
||||
import cc.sukazyo.cono.morny.MornyCoeur;
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener;
|
||||
import com.pengrad.telegrambot.model.Update;
|
||||
import com.pengrad.telegrambot.request.DeleteMessage;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
|
||||
@Deprecated
|
||||
public class OnKuohuanhuanNeedSleep implements EventListener {
|
||||
|
||||
@Override
|
||||
public boolean onMessage (@Nonnull Update update) {
|
||||
final GregorianCalendar time = new GregorianCalendar(Locale.TAIWAN);
|
||||
time.setTimeInMillis(System.currentTimeMillis());
|
||||
if (
|
||||
( update.message().from().id() == 786563752L && (
|
||||
time.get(Calendar.HOUR_OF_DAY) >= 23 ||
|
||||
time.get(Calendar.HOUR_OF_DAY) < 5
|
||||
)) || ( update.message().from().id() == 1075871712L && (
|
||||
(time.get(Calendar.HOUR_OF_DAY) >= 22 && time.get(Calendar.MINUTE) >= 30) ||
|
||||
time.get(Calendar.HOUR_OF_DAY) >= 23 ||
|
||||
time.get(Calendar.HOUR_OF_DAY) < 5
|
||||
))
|
||||
) {
|
||||
MornyCoeur.extra().exec(
|
||||
new DeleteMessage(update.message().chat().id(),
|
||||
update.message().messageId())
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.bot.event;
|
||||
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener;
|
||||
|
||||
@Deprecated
|
||||
public class OnRandomlyTriggered implements EventListener {
|
||||
|
||||
// /**
|
||||
// * function CODE_IK0XA1
|
||||
// */
|
||||
// // @Override
|
||||
// public boolean onMessage (@Nonnull Update update) {
|
||||
//
|
||||
// if (update.message().text() == null) return false;
|
||||
//
|
||||
// if (update.message().text().contains("急") && Math.random()<(1d/20)) {
|
||||
// MornyCoeur.extra().exec(new SendMessage(
|
||||
// update.message().chat().id(),
|
||||
// "急也没用"
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// return false;
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.daemon;
|
||||
|
||||
import cc.sukazyo.cono.morny.MornyCoeur;
|
||||
import cc.sukazyo.cono.morny.util.CommonFormat;
|
||||
import com.pengrad.telegrambot.model.Message;
|
||||
import com.pengrad.telegrambot.model.MessageEntity;
|
||||
import com.pengrad.telegrambot.model.request.ParseMode;
|
||||
import com.pengrad.telegrambot.request.EditMessageText;
|
||||
import com.pengrad.telegrambot.request.SendMessage;
|
||||
import com.pengrad.telegrambot.response.SendResponse;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static cc.sukazyo.cono.morny.Log.exceptionLog;
|
||||
import static cc.sukazyo.cono.morny.Log.logger;
|
||||
|
||||
public class MedicationTimer extends Thread {
|
||||
|
||||
private final ZoneOffset USE_TIME_ZONE = MornyCoeur.config().medicationTimerUseTimezone;
|
||||
private final Set<Integer> NOTIFY_AT_HOUR = MornyCoeur.config().medicationNotifyAt;
|
||||
private final long NOTIFY_CHAT = MornyCoeur.config().medicationNotifyToChat;
|
||||
public static final String NOTIFY_MESSAGE = "\uD83C\uDF65⏲";
|
||||
private static final String DAEMON_THREAD_NAME = "TIMER_Medication";
|
||||
|
||||
private static final long LAST_NOTIFY_ID_NULL = -1L;
|
||||
private long lastNotify = LAST_NOTIFY_ID_NULL;
|
||||
|
||||
public static class NoNotifyTimeTag extends Throwable { private NoNotifyTimeTag(){} }
|
||||
|
||||
MedicationTimer () {
|
||||
super(DAEMON_THREAD_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run () {
|
||||
logger.info("MedicationTimer started");
|
||||
while (!interrupted()) {
|
||||
try {
|
||||
waitToNextRoutine();
|
||||
sendNotification();
|
||||
} catch (InterruptedException e) {
|
||||
interrupt();
|
||||
logger.info("MedicationTimer was interrupted, will be exit now");
|
||||
} catch (NoNotifyTimeTag ignored) {
|
||||
logger.warn("Notify Time not Set and the MedicationTimer will not working!\nMedicationTimer will be exit now.");
|
||||
interrupt();
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error occurred");
|
||||
logger.error(exceptionLog(e));
|
||||
MornyReport.exception(e);
|
||||
}
|
||||
}
|
||||
logger.info("MedicationTimer stopped");
|
||||
}
|
||||
|
||||
private void sendNotification () {
|
||||
final SendResponse resp = MornyCoeur.extra().exec(new SendMessage(NOTIFY_CHAT, NOTIFY_MESSAGE));
|
||||
if (resp.isOk()) lastNotify = resp.message().messageId();
|
||||
else lastNotify = LAST_NOTIFY_ID_NULL;
|
||||
}
|
||||
|
||||
public void refreshNotificationWrite (Message edited) {
|
||||
if (edited.messageId() != lastNotify) return;
|
||||
final String editTime = CommonFormat.formatDate(edited.editDate()*1000, 8);
|
||||
ArrayList<MessageEntity> entities = new ArrayList<>();
|
||||
if (edited.entities() != null) entities.addAll(List.of(edited.entities()));
|
||||
entities.add(new MessageEntity(MessageEntity.Type.italic, edited.text().length() + "\n-- ".length(), editTime.length()));
|
||||
EditMessageText sending = new EditMessageText(
|
||||
NOTIFY_CHAT,
|
||||
edited.messageId(),
|
||||
edited.text() + "\n-- " + editTime + " --"
|
||||
).parseMode(ParseMode.HTML).entities(entities.toArray(MessageEntity[]::new));
|
||||
MornyCoeur.extra().exec(sending);
|
||||
lastNotify = LAST_NOTIFY_ID_NULL;
|
||||
}
|
||||
|
||||
public static long calcNextRoutineTimestamp (long baseTimeMillis, ZoneOffset useTimeZone, Set<Integer> atHours)
|
||||
throws NoNotifyTimeTag {
|
||||
if (atHours.isEmpty()) throw new NoNotifyTimeTag();
|
||||
LocalDateTime time = LocalDateTime.ofEpochSecond(
|
||||
baseTimeMillis/1000, (int)(baseTimeMillis%1000)*1000*1000,
|
||||
useTimeZone
|
||||
).withMinute(0).withSecond(0).withNano(0);
|
||||
do {
|
||||
time = time.plusHours(1);
|
||||
} while (!atHours.contains(time.getHour()));
|
||||
return time.withMinute(0).withSecond(0).withNano(0).toInstant(useTimeZone).toEpochMilli();
|
||||
}
|
||||
|
||||
private void waitToNextRoutine () throws InterruptedException, NoNotifyTimeTag {
|
||||
sleep(calcNextRoutineTimestamp(System.currentTimeMillis(), USE_TIME_ZONE, NOTIFY_AT_HOUR) - System.currentTimeMillis());
|
||||
}
|
||||
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.daemon;
|
||||
|
||||
import cc.sukazyo.cono.morny.MornyCoeur;
|
||||
|
||||
import static cc.sukazyo.cono.morny.Log.logger;
|
||||
|
||||
public class MornyDaemons {
|
||||
|
||||
public static final MedicationTimer medicationTimerInstance = new MedicationTimer();
|
||||
|
||||
public static void start () {
|
||||
logger.info("ALL Morny Daemons starting...");
|
||||
// TrackerDataManager.init();
|
||||
medicationTimerInstance.start();
|
||||
MornyReport.onMornyLogIn();
|
||||
logger.info("Morny Daemons started.");
|
||||
}
|
||||
|
||||
public static void stop () {
|
||||
|
||||
logger.info("ALL Morny Daemons stopping...");
|
||||
|
||||
// TrackerDataManager.DAEMON.interrupt();
|
||||
medicationTimerInstance.interrupt();
|
||||
|
||||
// TrackerDataManager.trackingLock.lock();
|
||||
try { medicationTimerInstance.join(); } catch (InterruptedException e) { e.printStackTrace(System.out); }
|
||||
|
||||
MornyReport.onMornyExit(MornyCoeur.getExitReason());
|
||||
logger.info("ALL Morny Daemons STOPPED.");
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.daemon;
|
||||
|
||||
import cc.sukazyo.cono.morny.*;
|
||||
import cc.sukazyo.cono.morny.bot.command.MornyInformation;
|
||||
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException;
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.pengrad.telegrambot.model.User;
|
||||
import com.pengrad.telegrambot.model.request.ParseMode;
|
||||
import com.pengrad.telegrambot.request.BaseRequest;
|
||||
import com.pengrad.telegrambot.request.SendMessage;
|
||||
import com.pengrad.telegrambot.response.BaseResponse;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static cc.sukazyo.cono.morny.Log.logger;
|
||||
import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
|
||||
|
||||
public class MornyReport {
|
||||
|
||||
private static boolean unsupported () {
|
||||
return !MornyCoeur.available() || MornyCoeur.config().reportToChat == -1;
|
||||
}
|
||||
|
||||
private static <T extends BaseRequest<T, R>, R extends BaseResponse> void executeReport (@Nonnull T report) {
|
||||
if (unsupported()) return;
|
||||
try {
|
||||
MornyCoeur.extra().exec(report);
|
||||
} catch (EventRuntimeException.ActionFailed e) {
|
||||
logger.warn("cannot execute report to telegram:");
|
||||
logger.warn(Log.exceptionLog(e).indent(4));
|
||||
logger.warn("tg-api response:");
|
||||
logger.warn(e.getResponse().toString().indent(4));
|
||||
}
|
||||
}
|
||||
|
||||
public static void exception (@Nonnull Throwable e, @Nullable String description) {
|
||||
if (unsupported()) return;
|
||||
executeReport(new SendMessage(
|
||||
MornyCoeur.config().reportToChat,
|
||||
String.format("""
|
||||
<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 (unsupported()) 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
|
||||
""",
|
||||
MornyInformation.getVersionAllFullTagHTML(),
|
||||
MornyCoeur.getUsername(),
|
||||
sectionConfigFields(MornyCoeur.config())
|
||||
)
|
||||
).parseMode(ParseMode.HTML));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个 config 字段与值的列表,可以作为 telegram html 格式输出
|
||||
* @since 1.0.0-alpha6
|
||||
*/
|
||||
private static String sectionConfigFields (@Nonnull MornyConfig config) {
|
||||
final StringBuilder echo = new StringBuilder();
|
||||
for (Field field : config.getClass().getFields()) {
|
||||
echo.append("- <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 (unsupported()) 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));
|
||||
}
|
||||
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.daemon;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static cc.sukazyo.cono.morny.Log.exceptionLog;
|
||||
import static cc.sukazyo.cono.morny.Log.logger;
|
||||
|
||||
public class TrackerDataManager {
|
||||
|
||||
/** {@link TrackerDaemon} 的锁。保证在程序中只有一个 TrackerDaemon 会运行。 */
|
||||
public static final ReentrantLock trackingLock = new ReentrantLock();
|
||||
/** {@link #record Tracker 缓存}的锁 <p> 为保证对 Tracker 缓存的操作不会造成线程冲突,在操作缓存数据前应先取得此锁。 */
|
||||
private static final ReentrantLock recordLock = new ReentrantLock();
|
||||
/** Tracker 数据的内存缓存 <p> 进行数据操作前请先取得对应的{@link #recordLock 锁} */
|
||||
private static HashMap<Long, HashMap<Long, TreeSet<Long>>> record = new HashMap<>();
|
||||
|
||||
public static final TrackerDaemon DAEMON = new TrackerDaemon();
|
||||
public static class TrackerDaemon extends Thread {
|
||||
|
||||
public TrackerDaemon () { this.setName("TRACKER"); }
|
||||
|
||||
@Override
|
||||
public void run () {
|
||||
trackingLock.lock();
|
||||
logger.info("Tracker started.");
|
||||
long lastWaitTimestamp = System.currentTimeMillis();
|
||||
boolean postProcess = false;
|
||||
do {
|
||||
lastWaitTimestamp += 10 * 60 * 1000;
|
||||
long sleeping = lastWaitTimestamp - System.currentTimeMillis();
|
||||
if (sleeping > 0) {
|
||||
try { Thread.sleep(sleeping); } catch (InterruptedException e) { interrupt(); }
|
||||
} else {
|
||||
logger.warn("Tracker may be too busy to process data!!");
|
||||
lastWaitTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
if (interrupted()) {
|
||||
postProcess = true;
|
||||
logger.info("CALLED TO EXIT! writing cache.");
|
||||
}
|
||||
if (record.size() != 0) {
|
||||
save();
|
||||
}
|
||||
else logger.info("nothing to do yet");
|
||||
} while (!postProcess);
|
||||
trackingLock.unlock();
|
||||
logger.info("Tracker exited.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 Tracker 缓存写入一条 tracker 数据.
|
||||
* <p>
|
||||
* <font color=green>这个方法对于 Tracker 缓存是原子化的。</font>
|
||||
*
|
||||
* @param chat tracker 所属的 Telegram Chat ID
|
||||
* @param user tracker 所记录的 Telegram User ID
|
||||
* @param timestamp tracker 被生成时的 UTC 时间戳
|
||||
*/
|
||||
public static void record (long chat, long user, long timestamp) {
|
||||
recordLock.lock();
|
||||
if (!record.containsKey(chat)) record.put(chat, new HashMap<>());
|
||||
HashMap<Long, TreeSet<Long>> chatUsers = record.get(chat);
|
||||
if (!chatUsers.containsKey(user)) chatUsers.put(user, new TreeSet<>());
|
||||
TreeSet<Long> userRecords = chatUsers.get(user);
|
||||
userRecords.add(timestamp);
|
||||
recordLock.unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启 {@link TrackerDaemon}.
|
||||
* <p>
|
||||
* <font color=orange>由于 Tracker 已废弃,这个方法已无作用。</font>
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void init () {
|
||||
DAEMON.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 Tracker 的保存逻辑.
|
||||
* @see #reset() 弹出 Tracker 缓存
|
||||
* @see #save(HashMap) 执行硬盘写入操作
|
||||
*/
|
||||
public static void save () {
|
||||
logger.info("start writing tracker data.");
|
||||
save(reset());
|
||||
logger.info("done writing tracker data.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Tracker 的缓存数据弹出.
|
||||
* <p>
|
||||
* 这个方法将返回现在 Tracker 的所有缓存数据,然后清除缓存。
|
||||
* <p>
|
||||
* <font color=green>这个方法对于 Tracker 缓存是原子化的。</font>
|
||||
*
|
||||
* @return 当前 Tracker 所包含的内容
|
||||
*/
|
||||
private static HashMap<Long, HashMap<Long, TreeSet<Long>>> reset () {
|
||||
recordLock.lock();
|
||||
HashMap<Long, HashMap<Long, TreeSet<Long>>> recordOld = record;
|
||||
record = new HashMap<>();
|
||||
recordLock.unlock();
|
||||
return recordOld;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Tracker 数据写入到硬盘.
|
||||
*
|
||||
* @param record 需要保存的 Tracker 数据集
|
||||
*/
|
||||
private static void save (HashMap<Long, HashMap<Long, TreeSet<Long>>> record) {
|
||||
|
||||
{
|
||||
if (!record.containsKey(0L)) record.put(0L, new HashMap<>());
|
||||
HashMap<Long, TreeSet<Long>> chatUsers = record.get(0L);
|
||||
if (!chatUsers.containsKey(0L)) chatUsers.put(0L, new TreeSet<>());
|
||||
TreeSet<Long> userRecords = chatUsers.get(0L);
|
||||
userRecords.add(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
record.forEach((chat, chatUsers) -> chatUsers.forEach((user, userRecords) -> {
|
||||
|
||||
long dayCurrent = -1;
|
||||
FileChannel channelCurrent = null;
|
||||
|
||||
for (long timestamp : userRecords) {
|
||||
try {
|
||||
|
||||
long day = timestamp / (24 * 60 * 60 * 1000);
|
||||
if (dayCurrent != day) {
|
||||
if (channelCurrent != null) channelCurrent.close();
|
||||
channelCurrent = openFile(chat, user, day);
|
||||
dayCurrent = day;
|
||||
}
|
||||
|
||||
assert channelCurrent != null;
|
||||
final int result = channelCurrent.write(ByteBuffer.wrap(
|
||||
String.format("%d\n", timestamp).getBytes(StandardCharsets.UTF_8)
|
||||
));
|
||||
|
||||
if (result == 0) logger.warn("writing tracker data %d/%d/%d: write only 0 bytes! is anything wrong?");
|
||||
|
||||
} catch (Exception e) {
|
||||
final String message = String.format("exception in write tracker data: %d/%d/%d", chat, user, timestamp);
|
||||
logger.error(message);
|
||||
logger.error(exceptionLog(e));
|
||||
MornyReport.exception(e, message);
|
||||
}
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
private static FileChannel openFile (long chat, long user, long day) throws IOException {
|
||||
File data = new File(String.format("./data/tracker/%d/%d", chat, user));
|
||||
if (!data.isDirectory()) if (!data.mkdirs()) throw new IOException("Cannot create file directory " + data.getPath());
|
||||
File file = new File(data, String.valueOf(day));
|
||||
if (!file.isFile()) if (!file.createNewFile()) throw new IOException("Cannot create file " + file.getPath());
|
||||
return FileChannel.open(file.toPath(), StandardOpenOption.APPEND);
|
||||
}
|
||||
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.data;
|
||||
|
||||
import cc.sukazyo.cono.morny.MornyAssets;
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static cc.sukazyo.cono.morny.Log.exceptionLog;
|
||||
import static cc.sukazyo.cono.morny.Log.logger;
|
||||
|
||||
/**
|
||||
* The images of morny will use.
|
||||
*
|
||||
* @since 1.0.0-RC4
|
||||
*/
|
||||
public class TelegramImages {
|
||||
|
||||
/**
|
||||
* Image that stored in the {@link MornyAssets#pack}.
|
||||
* <p>
|
||||
* It has a final {@link #assetsPath} record that is its store location,
|
||||
* and has a {@link #cache} of the binary data.
|
||||
*
|
||||
* @since 1.0.0-RC4
|
||||
*/
|
||||
public static class AssetsFileImage {
|
||||
|
||||
/** the path where the image is stored in {@link MornyAssets#pack}. */
|
||||
@Nonnull private final String assetsPath;
|
||||
/** the binary data cache of the image.<p>{@link null} means it hasn't been cached. */
|
||||
@Nullable private byte[] cache = null;
|
||||
|
||||
/**
|
||||
* An {@link AssetsFileImage}.
|
||||
*
|
||||
* @param path the image path relative to {@link MornyAssets#pack}'s root.
|
||||
*/
|
||||
AssetsFileImage (@Nonnull String path) {
|
||||
this.assetsPath = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the binary data.
|
||||
* <p>
|
||||
* Will read the {@link #cache} firstly. If read {@link null},
|
||||
* then it will try {@link #read load the file}, and read again.
|
||||
*
|
||||
* @return The binary data of the image.
|
||||
* @throws IllegalStateException While the {@link #read()} failed to read data and
|
||||
* the result {@link #cache} is still null
|
||||
*/
|
||||
@Nonnull public byte[] get() {
|
||||
if (cache == null) read();
|
||||
if (cache == null) throw new IllegalStateException("Failed get assets file image.");
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the file from {@link MornyAssets#pack}, and stored the binary data to {@link #cache}.
|
||||
* <p>
|
||||
* If failed, it will output the exception to the log and the {@link MornyReport},
|
||||
* and remains the cache's current data.
|
||||
*/
|
||||
private void read() {
|
||||
try (InputStream stream = MornyAssets.pack.getResource(assetsPath).read()) {
|
||||
this.cache = stream.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
logger.error("Cannot read resource file");
|
||||
logger.error(exceptionLog(e));
|
||||
MornyReport.exception(e, "Cannot read resource file");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final AssetsFileImage IMG_ABOUT = new AssetsFileImage("images/featured-image@0.5x.jpg");
|
||||
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.data;
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.ExtraAction;
|
||||
import com.pengrad.telegrambot.request.SendMessage;
|
||||
import com.pengrad.telegrambot.request.SendSticker;
|
||||
import com.pengrad.telegrambot.response.SendResponse;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* 存放 bot 使用到的贴纸
|
||||
* @since 0.4.2.0
|
||||
*/
|
||||
public class TelegramStickers {
|
||||
|
||||
public static final String ID_ONLINE_STATUS_RETURN = "CAACAgEAAx0CW-CvvgAC5eBhhhODGRuu0pxKLwoQ3yMsowjviAACcycAAnj8xgVVU666si1utiIE";
|
||||
public static final String ID_HELLO = "CAACAgEAAxkBAAMnYYYWKNXO4ibo9dlsmDctHhhV6fIAAqooAAJ4_MYFJJhrHS74xUAiBA";
|
||||
public static final String ID_EXIT = "CAACAgEAAxkBAAMoYYYWt8UjvP0N405SAyvg2SQZmokAAkMiAAJ4_MYFw6yZLu06b-MiBA";
|
||||
public static final String ID_403 = "CAACAgEAAxkBAAMqYYYa_7hpXH6hMOYMX4Nh8AVYd74AAnQnAAJ4_MYFRdmmsQKLDZgiBA";
|
||||
public static final String ID_404 = "CAACAgEAAx0CSQh32gABA966YbRJpbmi2lCHINBDuo1DknSTsbsAAqUoAAJ4_MYFUa8SIaZriAojBA";
|
||||
public static final String ID_WAITING = "CAACAgEAAx0CSQh32gABA-8DYbh7W2VhJ490ucfZMUMrgMR2FW4AAm4nAAJ4_MYFjx6zpxJPWsQjBA";
|
||||
public static final String ID_SENT = "CAACAgEAAx0CSQh32gABA--zYbiyU_wOijEitp-0tSl_k7W6l3gAAgMmAAJ4_MYF4GrompjXPx4jBA";
|
||||
public static final String ID_SAVED = "CAACAgEAAx0CSQh32gABBExuYdB_G0srfhQldRWkBYxWzCOv4-IAApooAAJ4_MYFcjuNZszfQcQjBA";
|
||||
public static final String ID_PROGYNOVA = "CAACAgUAAxkBAAICm2KEuL7UQqNP7vSPCg2DHJIND6UsAAKLAwACH4WSBszIo722aQ3jJAQ";
|
||||
public static final String ID_NETWORK_ERR = "CAACAgEAAxkBAAID0WNJgNEkD726KW4vZeFlw0FlVVyNAAIXJgACePzGBb50o7O1RbxoKgQ";
|
||||
|
||||
/**
|
||||
* 向 telegram 输出当前的 {@link TelegramStickers} 中的所有 stickers.
|
||||
* @param actionObject 要使用的 telegram account 包装实例
|
||||
* @param sentChat 目标 telegram chat id
|
||||
* @param replyToMessageId 输出时回复指定的消息的 id。使用 {@link -1} 表示不回复消息
|
||||
* @since 0.8.0.6
|
||||
*/
|
||||
public static void echoAllStickers (@Nonnull ExtraAction actionObject, long sentChat, int replyToMessageId) {
|
||||
|
||||
for (Field object : TelegramStickers.class.getFields()) {
|
||||
if (object.getType()==String.class && object.getName().startsWith("ID_")) {
|
||||
try {
|
||||
|
||||
final String stickerId = (String)object.get("");
|
||||
SendSticker echo = new SendSticker(sentChat, stickerId);
|
||||
SendMessage echoName = new SendMessage(sentChat, object.getName());
|
||||
if (replyToMessageId!=-1) echo.replyToMessageId(replyToMessageId);
|
||||
SendResponse echoedName = actionObject.exec(echoName);
|
||||
actionObject.exec(echo.replyToMessageId(echoedName.message().messageId()));
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 telegram 输出当前的 {@link TelegramStickers} 中的某个特定 sticker.
|
||||
* @param stickerFieldID 要输出的 sticker 在 {@link TelegramStickers} 当中的字段名
|
||||
* @param actionObject 要使用的 telegram account 包装实例
|
||||
* @param sentChat 目标 telegram chat id
|
||||
* @param replyToMessageId 输出时回复指定的消息的 id。使用 {@link -1} 表示不回复消息
|
||||
* @since 0.8.0.6
|
||||
*/
|
||||
public static void echoStickerByID (
|
||||
@Nonnull String stickerFieldID,
|
||||
@Nonnull ExtraAction actionObject, long sentChat, int replyToMessageId
|
||||
) {
|
||||
try {
|
||||
// normally get the sticker and echo
|
||||
Field sticker = TelegramStickers.class.getField(stickerFieldID);
|
||||
SendMessage echoName = new SendMessage(sentChat, sticker.getName());
|
||||
SendSticker echo = new SendSticker(sentChat, (String)sticker.get(""));
|
||||
if (replyToMessageId!=-1) echo.replyToMessageId(replyToMessageId);
|
||||
SendResponse echoedName = actionObject.exec(echoName);
|
||||
actionObject.exec(echo.replyToMessageId(echoedName.message().messageId()));
|
||||
} catch (NoSuchFieldException e) {
|
||||
// no such sticker found
|
||||
SendSticker echo404 = new SendSticker(sentChat, TelegramStickers.ID_404);
|
||||
if (replyToMessageId!=-1) echo404.replyToMessageId(replyToMessageId);
|
||||
actionObject.exec(echo404);
|
||||
} catch (IllegalAccessException e) {
|
||||
// java-reflect get sticker FILE_ID failed
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
29
src/main/scala/cc/sukazyo/cono/morny/Log.scala
Normal file
29
src/main/scala/cc/sukazyo/cono/morny/Log.scala
Normal file
@ -0,0 +1,29 @@
|
||||
package cc.sukazyo.cono.morny
|
||||
|
||||
import cc.sukazyo.messiva.logger.Logger
|
||||
import cc.sukazyo.messiva.appender.ConsoleAppender
|
||||
import cc.sukazyo.messiva.formatter.SimpleFormatter
|
||||
import cc.sukazyo.messiva.log.LogLevel
|
||||
|
||||
import java.io.{PrintWriter, StringWriter}
|
||||
|
||||
object Log {
|
||||
|
||||
val logger: Logger = Logger(
|
||||
ConsoleAppender(
|
||||
SimpleFormatter()
|
||||
)
|
||||
).minLevel(LogLevel.INFO)
|
||||
|
||||
def debug: Boolean = logger.levelSetting.minLevel.level <= LogLevel.DEBUG.level
|
||||
|
||||
def debug(is: Boolean): Unit =
|
||||
if is then logger.minLevel(LogLevel.ALL)
|
||||
else logger.minLevel(LogLevel.INFO)
|
||||
|
||||
def exceptionLog (e: Throwable): String =
|
||||
val stackTrace = StringWriter()
|
||||
e printStackTrace PrintWriter(stackTrace)
|
||||
stackTrace toString
|
||||
|
||||
}
|
17
src/main/scala/cc/sukazyo/cono/morny/MornyAbout.scala
Normal file
17
src/main/scala/cc/sukazyo/cono/morny/MornyAbout.scala
Normal file
@ -0,0 +1,17 @@
|
||||
package cc.sukazyo.cono.morny
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
object MornyAbout {
|
||||
|
||||
val MORNY_PREVIEW_IMAGE_ASCII: String =
|
||||
try { MornyAssets.pack getResource "texts/server-hello.txt" readAsString }
|
||||
catch case e: IOException =>
|
||||
throw RuntimeException("Cannot read MORNY_PREVIEW_IMAGE_ASCII from assets pack", e)
|
||||
|
||||
val MORNY_SOURCECODE_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono"
|
||||
val MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK = "https://storage.sukazyo.cc/Eyre_S/Coeur-Morny-Cono"
|
||||
val MORNY_ISSUE_TRACKER_LINK = "https://github.com/Eyre-S/Coeur-Morny-Cono/issues"
|
||||
val MORNY_USER_GUIDE_LINK = "https://book.sukazyo.cc/morny"
|
||||
|
||||
}
|
9
src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala
Normal file
9
src/main/scala/cc/sukazyo/cono/morny/MornyAssets.scala
Normal file
@ -0,0 +1,9 @@
|
||||
package cc.sukazyo.cono.morny
|
||||
|
||||
import cc.sukazyo.restools.ResourcesPackage
|
||||
|
||||
object MornyAssets {
|
||||
|
||||
val pack: ResourcesPackage = ResourcesPackage(MornyAssets.getClass, "assets_morny")
|
||||
|
||||
}
|
149
src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala
Normal file
149
src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala
Normal file
@ -0,0 +1,149 @@
|
||||
package cc.sukazyo.cono.morny
|
||||
|
||||
import cc.sukazyo.cono.morny.bot.command.MornyCommands
|
||||
import cc.sukazyo.cono.morny.daemon.MornyDaemons
|
||||
import cc.sukazyo.cono.morny.util.tgapi.ExtraAction
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.MornyCoeur.THREAD_MORNY_EXIT
|
||||
import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener
|
||||
import cc.sukazyo.cono.morny.bot.event.MornyEventListeners
|
||||
import com.pengrad.telegrambot.TelegramBot
|
||||
import com.pengrad.telegrambot.request.GetMe
|
||||
|
||||
import scala.util.boundary
|
||||
import scala.util.boundary.break
|
||||
|
||||
object MornyCoeur {
|
||||
|
||||
val THREAD_MORNY_EXIT = "morny-exiting"
|
||||
|
||||
private var INSTANCE: MornyCoeur|Null = _
|
||||
|
||||
val coeurStartTimestamp: Long = ServerMain.systemStartupTime
|
||||
|
||||
def account: TelegramBot = INSTANCE.account
|
||||
def username: String = INSTANCE.username
|
||||
def userid: Long = INSTANCE.userid
|
||||
def config: MornyConfig = INSTANCE.my_config
|
||||
def trusted: MornyTrusted = INSTANCE.trusted
|
||||
def extra: ExtraAction = INSTANCE.extra
|
||||
|
||||
def available: Boolean = INSTANCE != null
|
||||
def callSaveData(): Unit = INSTANCE.saveDataAll()
|
||||
|
||||
def exitReason: AnyRef|Null = INSTANCE.whileExit_reason
|
||||
|
||||
def exit (status: Int, reason: AnyRef): Unit =
|
||||
INSTANCE.whileExit_reason = reason
|
||||
System exit status
|
||||
|
||||
def init (using config: MornyConfig): Unit = {
|
||||
|
||||
if (INSTANCE ne null)
|
||||
logger error "Coeur already started!!!"
|
||||
return;
|
||||
|
||||
logger info "Coeur starting..."
|
||||
INSTANCE = MornyCoeur()
|
||||
|
||||
MornyDaemons.start()
|
||||
|
||||
logger info "start telegram event listening"
|
||||
MornyEventListeners.registerAllEvents()
|
||||
INSTANCE.account.setUpdatesListener(TelegramUpdatesListener)
|
||||
|
||||
if config.commandLoginRefresh then
|
||||
logger info "resetting telegram command list"
|
||||
MornyCommands.automaticTGListUpdate()
|
||||
|
||||
logger info "Coeur start complete."
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MornyCoeur (using config: MornyConfig) {
|
||||
def my_config: MornyConfig = config
|
||||
|
||||
logger info s"args key:\n ${config.telegramBotKey}"
|
||||
if config.telegramBotUsername ne null then
|
||||
logger info s"login as:\n ${config.telegramBotUsername}"
|
||||
|
||||
private val __loginResult = login()
|
||||
if (__loginResult eq null)
|
||||
logger error "Login to bot failed."
|
||||
System exit -1
|
||||
|
||||
configure_exitCleanup()
|
||||
|
||||
val account: TelegramBot = __loginResult.account
|
||||
val username: String = __loginResult.username
|
||||
val userid: Long = __loginResult.userid
|
||||
|
||||
val trusted: MornyTrusted = MornyTrusted(using this)
|
||||
val extra: ExtraAction = ExtraAction as __loginResult.account
|
||||
var whileExit_reason: AnyRef|Null = _
|
||||
|
||||
def saveDataAll(): Unit = {
|
||||
// nothing to do
|
||||
logger info "done all save action."
|
||||
}
|
||||
|
||||
private def exitCleanup (): Unit = {
|
||||
MornyDaemons.stop()
|
||||
if config.commandLogoutClear then
|
||||
MornyCommands.automaticTGListRemove()
|
||||
}
|
||||
|
||||
private def configure_exitCleanup (): Unit = {
|
||||
Runtime.getRuntime.addShutdownHook(new Thread(() => exitCleanup(), THREAD_MORNY_EXIT))
|
||||
}
|
||||
|
||||
private case class LoginResult(account: TelegramBot, username: String, userid: Long)
|
||||
|
||||
private def login (): LoginResult|Null = {
|
||||
|
||||
val builder = TelegramBot.Builder(config.telegramBotKey)
|
||||
var api_bot = config.telegramBotApiServer
|
||||
var api_file = config.telegramBotApiServer4File
|
||||
if (api_bot ne null)
|
||||
if api_bot endsWith "/" then api_bot = api_bot dropRight 1
|
||||
if !(api_bot endsWith "/bot") then api_bot += "/bot"
|
||||
builder.apiUrl(api_bot)
|
||||
if (api_file ne null)
|
||||
if api_file endsWith "/file/" then api_file = api_file dropRight 1
|
||||
if !(api_file endsWith "/file/bot") then api_file += "/file/bot"
|
||||
builder.apiUrl(api_bot)
|
||||
if ((api_bot ne null) || (api_file ne null))
|
||||
logger info
|
||||
s"""Telegram bot api set to:
|
||||
|- bot: $api_bot
|
||||
|- file: $api_file"""
|
||||
.stripMargin
|
||||
|
||||
val account = builder build
|
||||
|
||||
logger info "Trying to login..."
|
||||
boundary[LoginResult|Null] {
|
||||
for (i <- 0 to 3) {
|
||||
if i > 0 then logger info "retrying..."
|
||||
try {
|
||||
val remote = (account execute GetMe()).user
|
||||
if ((config.telegramBotUsername ne null) && config.telegramBotUsername != remote.username)
|
||||
throw RuntimeException(s"Required the bot @${config.telegramBotUsername} but @${remote.username} logged in")
|
||||
logger info s"Succeed logged in to @${remote.username}"
|
||||
break(LoginResult(account, remote.username, remote.id))
|
||||
} catch
|
||||
case r: boundary.Break[LoginResult|Null] => throw r
|
||||
case e =>
|
||||
logger error
|
||||
s"""${exceptionLog(e)}
|
||||
|login failed"""
|
||||
.stripMargin
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -129,7 +129,7 @@ public class MornyConfig {
|
||||
* End Configs | ConfigBuilder *
|
||||
* ======================================= */
|
||||
|
||||
public MornyConfig (@Nonnull Prototype prototype) throws CheckFailure {
|
||||
private MornyConfig (@Nonnull Prototype prototype) throws CheckFailure {
|
||||
this.telegramBotApiServer = prototype.telegramBotApiServer;
|
||||
this.telegramBotApiServer4File = prototype.telegramBotApiServer4File;
|
||||
if (prototype.telegramBotKey == null) throw new CheckFailure.NullTelegramBotKey();
|
||||
@ -159,6 +159,10 @@ public class MornyConfig {
|
||||
|
||||
public static class Prototype {
|
||||
|
||||
public MornyConfig build () {
|
||||
return new MornyConfig(this);
|
||||
}
|
||||
|
||||
@Nullable public String telegramBotApiServer = null;
|
||||
@Nullable public String telegramBotApiServer4File = null;
|
||||
@Nullable public String telegramBotKey = null;
|
48
src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala
Normal file
48
src/main/scala/cc/sukazyo/cono/morny/MornySystem.scala
Normal file
@ -0,0 +1,48 @@
|
||||
package cc.sukazyo.cono.morny
|
||||
|
||||
import cc.sukazyo.cono.morny.internal.BuildConfigField
|
||||
import cc.sukazyo.cono.morny.util.FileUtils
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.URISyntaxException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport
|
||||
|
||||
object MornySystem {
|
||||
|
||||
@BuildConfigField val VERSION: String = BuildConfig.VERSION
|
||||
@BuildConfigField val VERSION_FULL: String = BuildConfig.VERSION_FULL
|
||||
@BuildConfigField val VERSION_BASE: String = BuildConfig.VERSION_BASE
|
||||
@BuildConfigField val VERSION_DELTA: String = BuildConfig.VERSION_DELTA
|
||||
@BuildConfigField val CODENAME: String = BuildConfig.CODENAME
|
||||
@BuildConfigField val CODE_STORE: String = BuildConfig.CODE_STORE
|
||||
@BuildConfigField val COMMIT_PATH: String = BuildConfig.COMMIT_PATH
|
||||
|
||||
@BuildConfigField
|
||||
def isUseDelta: Boolean = VERSION_DELTA ne null
|
||||
|
||||
@BuildConfigField
|
||||
def isGitBuild: Boolean = BuildConfig.COMMIT ne null
|
||||
|
||||
@BuildConfigField
|
||||
def isCleanBuild: Boolean = BuildConfig.CLEAN_BUILD
|
||||
|
||||
def currentCodePath: String|Null =
|
||||
if ((COMMIT_PATH eq null) || (!isGitBuild)) null
|
||||
else COMMIT_PATH.formatted(BuildConfig.COMMIT)
|
||||
|
||||
def getJarMD5: String = {
|
||||
try {
|
||||
FileUtils.getMD5Three(MornySystem.getClass.getProtectionDomain.getCodeSource.getLocation.toURI.getPath)
|
||||
} catch
|
||||
//noinspection ScalaUnnecessaryParentheses
|
||||
case _: (IOException|URISyntaxException) =>
|
||||
"<non-jar-runtime>"
|
||||
case n: NoSuchAlgorithmException =>
|
||||
logger error exceptionLog(n)
|
||||
MornyReport.exception(n, "<coeur-md5/calculation-error>")
|
||||
"<calculation-error>"
|
||||
}
|
||||
|
||||
}
|
16
src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala
Normal file
16
src/main/scala/cc/sukazyo/cono/morny/MornyTrusted.scala
Normal file
@ -0,0 +1,16 @@
|
||||
package cc.sukazyo.cono.morny
|
||||
|
||||
import com.pengrad.telegrambot.model.ChatMember.Status
|
||||
|
||||
class MornyTrusted (using coeur: MornyCoeur)(using config: MornyConfig) {
|
||||
|
||||
def isTrusted (userId: Long): Boolean =
|
||||
if userId == config.trustedMaster then true
|
||||
else if config.trustedChat == -1 then false
|
||||
else coeur.extra isUserInGroup(userId, config.trustedChat, Status.administrator)
|
||||
|
||||
def isTrusted_dinnerReader (userId: Long): Boolean =
|
||||
if userId == config.trustedMaster then true
|
||||
else config.dinnerTrustedReaders contains userId
|
||||
|
||||
}
|
@ -109,7 +109,7 @@ object ServerMain {
|
||||
| Morny ${MornySystem.CODENAME toUpperCase}
|
||||
| ${MornySystem.VERSION_BASE}${if (MornySystem.isUseDelta) "-δ"+MornySystem.VERSION_DELTA else ""}
|
||||
|- md5hash :
|
||||
| ${MornySystem.getJarMd5}
|
||||
| ${MornySystem.getJarMD5}
|
||||
|- gitstat :
|
||||
|${ if (MornySystem.isGitBuild) {
|
||||
s""" on commit ${if (MornySystem.isCleanBuild) "- clean-build" else "<δ/non-clean-build>"}
|
||||
@ -128,7 +128,7 @@ object ServerMain {
|
||||
s"""ServerMain.java Loaded >>>
|
||||
|- version ${MornySystem.VERSION_FULL}
|
||||
|- Morny ${MornySystem.CODENAME toUpperCase}
|
||||
|- <${MornySystem.getJarMd5}> [${BuildConfig.CODE_TIMESTAMP}]""".stripMargin
|
||||
|- <${MornySystem.getJarMD5}> [${BuildConfig.CODE_TIMESTAMP}]""".stripMargin
|
||||
|
||||
///
|
||||
/// Check Coeur arguments
|
||||
@ -143,7 +143,7 @@ object ServerMain {
|
||||
Thread.currentThread setName THREAD_MORNY_INIT
|
||||
|
||||
try
|
||||
MornyCoeur.init(new MornyConfig(config))
|
||||
MornyCoeur.init(using config build)
|
||||
catch {
|
||||
case _: CheckFailure.NullTelegramBotKey =>
|
||||
logger.info("Parameter required has no value:\n --token.")
|
||||
|
@ -19,12 +19,12 @@ object DirectMsgClear extends ISimpleCommand {
|
||||
logger debug "executing command /r"
|
||||
if (event.message.replyToMessage == null) return;
|
||||
logger trace "message is a reply"
|
||||
if (event.message.replyToMessage.from.id != MornyCoeur.getUserid) return;
|
||||
if (event.message.replyToMessage.from.id != MornyCoeur.userid) return;
|
||||
logger trace "message replied is from me"
|
||||
if (System.currentTimeMillis/1000 - event.message.replyToMessage.date > 48*60*60) return;
|
||||
logger trace "message is not outdated(48 hrs ago)"
|
||||
|
||||
val isTrusted = MornyCoeur.trustedInstance isTrusted event.message.from.id
|
||||
val isTrusted = MornyCoeur.trusted isTrusted event.message.from.id
|
||||
def _isReplyTrusted: Boolean =
|
||||
if (event.message.replyToMessage.replyToMessage == null) false
|
||||
else if (event.message.replyToMessage.replyToMessage.from.id == event.message.from.id) true
|
||||
|
@ -57,7 +57,7 @@ object Encryptor extends ITelegramCommand {
|
||||
val _r = event.message.replyToMessage
|
||||
if ((_r ne null) && (_r.document ne null)) {
|
||||
try {XFile(
|
||||
MornyCoeur.getAccount getFileContent (MornyCoeur.extra exec GetFile(_r.document.fileId)).file,
|
||||
MornyCoeur.account getFileContent (MornyCoeur.extra exec GetFile(_r.document.fileId)).file,
|
||||
_r.document.fileName
|
||||
)} catch case e: IOException =>
|
||||
logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}"
|
||||
@ -74,7 +74,7 @@ object Encryptor extends ITelegramCommand {
|
||||
_photo_size = _size
|
||||
if (_photo_origin eq null) throw IllegalArgumentException("no photo from api.")
|
||||
XFile(
|
||||
MornyCoeur.getAccount getFileContent (MornyCoeur.extra exec GetFile(_photo_origin.fileId)).file,
|
||||
MornyCoeur.account getFileContent (MornyCoeur.extra exec GetFile(_photo_origin.fileId)).file,
|
||||
s"photo${byteArrayToHex(hashMd5(System.currentTimeMillis toString)) substring 32-12 toUpperCase}.png"
|
||||
)
|
||||
} catch
|
||||
|
@ -40,7 +40,7 @@ object EventHack extends ITelegramCommand {
|
||||
)
|
||||
x_mode match
|
||||
case "any" =>
|
||||
if (MornyCoeur.trustedInstance isTrusted event.message.from.id)
|
||||
if (MornyCoeur.trusted isTrusted event.message.from.id)
|
||||
doRegister(HackType ANY)
|
||||
done_ok
|
||||
else done_forbiddenForAny
|
||||
|
@ -39,7 +39,7 @@ object GetUsernameAndId extends ITelegramCommand {
|
||||
} else if (event.message.replyToMessage eq null) event.message.from.id
|
||||
else event.message.replyToMessage.from.id
|
||||
|
||||
val response = MornyCoeur.getAccount execute GetChatMember(event.message.chat.id, userId)
|
||||
val response = MornyCoeur.account execute GetChatMember(event.message.chat.id, userId)
|
||||
|
||||
if (response.chatMember eq null)
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
|
@ -65,12 +65,12 @@ object IP186Query {
|
||||
|
||||
} catch case e: Exception =>
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
MornyCoeur.extra().exec(new SendMessage(
|
||||
MornyCoeur.extra exec new SendMessage(
|
||||
event.message().chat().id(),
|
||||
s"""[Exception] in query:
|
||||
|<code>${h(e.getMessage)}</code>"""
|
||||
.stripMargin
|
||||
).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId()));
|
||||
).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())
|
||||
|
||||
}
|
||||
|
||||
|
@ -3,20 +3,23 @@ package cc.sukazyo.cono.morny.bot.command
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.data.TelegramStickers
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
import com.pengrad.telegrambot.model.{BotCommand, DeleteMyCommands, Update}
|
||||
import com.pengrad.telegrambot.request.{SendSticker, SetMyCommands}
|
||||
|
||||
import scala.collection.{mutable, SeqMap}
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.language.postfixOps
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
|
||||
object MornyCommands {
|
||||
|
||||
private type CommandMap = SeqMap[String, ISimpleCommand]
|
||||
private def CommandMap (commands: ISimpleCommand*): CommandMap =
|
||||
val stash: mutable.SeqMap[String, ISimpleCommand] = mutable.SeqMap()
|
||||
for (i <- commands) stash += ((i.name, i))
|
||||
val stash = mutable.SeqMap.empty[String, ISimpleCommand]
|
||||
for (i <- commands)
|
||||
stash += (i.name -> i)
|
||||
if (i.aliases ne null) for (alias <- i.aliases)
|
||||
stash += (alias.name -> i)
|
||||
stash
|
||||
|
||||
private val commands: CommandMap = CommandMap(
|
||||
@ -41,12 +44,14 @@ object MornyCommands {
|
||||
Testing,
|
||||
DirectMsgClear,
|
||||
|
||||
//noinspection NonAsciiCharacters
|
||||
私わね,
|
||||
//noinspection NonAsciiCharacters
|
||||
喵呜.Progynova
|
||||
|
||||
)
|
||||
|
||||
@SuppressWarnings(Array("NonAsciiCharacters"))
|
||||
//noinspection NonAsciiCharacters
|
||||
val commands_uni: CommandMap = CommandMap(
|
||||
喵呜.抱抱,
|
||||
喵呜.揉揉,
|
||||
|
@ -1,10 +1,10 @@
|
||||
package cc.sukazyo.cono.morny.bot.command
|
||||
|
||||
import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornyCoeur, MornySystem}
|
||||
import cc.sukazyo.cono.morny.data.{TelegramImages, TelegramStickers}
|
||||
import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration}
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornyCoeur, MornySystem}
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.request.{SendMessage, SendPhoto, SendSticker}
|
||||
@ -38,7 +38,7 @@ object MornyInformation extends ITelegramCommand {
|
||||
val action: String = command.getArgs()(0)
|
||||
|
||||
action match {
|
||||
case Subs.STICKERS => echoStickers
|
||||
case s if s startsWith Subs.STICKERS => echoStickers
|
||||
case Subs.RUNTIME => echoRuntime
|
||||
case Subs.VERSION | Subs.VERSION_2 => echoVersion
|
||||
case _ => echo404
|
||||
@ -93,24 +93,41 @@ object MornyInformation extends ITelegramCommand {
|
||||
}
|
||||
|
||||
private def echoStickers (using command: InputCommand, event: Update): Unit = {
|
||||
val chat = event.message.chat.id
|
||||
val replyTo = event.message.messageId
|
||||
var sid: String|Null = null
|
||||
if (command.getArgs()(0) == Subs.STICKERS) {
|
||||
if (command.getArgs.length == 1) sid = ""
|
||||
else if (command.getArgs.length == 2) sid = command.getArgs()(1)
|
||||
} else if (command.getArgs.length == 1) {
|
||||
if ((command.getArgs()(0) startsWith s"${Subs.STICKERS}.") || (command.getArgs()(0) startsWith s"${Subs.STICKERS}#")) {
|
||||
sid = command.getArgs()(0) substring Subs.STICKERS.length+1
|
||||
}
|
||||
}
|
||||
if (sid == null) echo404
|
||||
else echoStickers(sid, chat, replyTo)
|
||||
val mid: String|Null =
|
||||
if (command.getArgs()(0) == Subs.STICKERS) {
|
||||
if (command.getArgs.length == 1) ""
|
||||
else if (command.getArgs.length == 2) command.getArgs()(1)
|
||||
else null
|
||||
} else if (command.getArgs.length == 1) {
|
||||
if ((command.getArgs()(0) startsWith s"${Subs.STICKERS}.") || (command.getArgs()(0) startsWith s"${Subs.STICKERS}#")) {
|
||||
command.getArgs()(0) substring Subs.STICKERS.length+1
|
||||
} else null
|
||||
} else null
|
||||
if (mid == null) echo404
|
||||
else echoStickers(mid)(using event.message.chat.id, event.message.messageId)
|
||||
}
|
||||
|
||||
private def echoStickers (sid: String, send_chat: Long, send_replyTo: Int): Unit = {
|
||||
if (sid isEmpty) TelegramStickers echoAllStickers(MornyCoeur.extra, send_chat, send_replyTo)
|
||||
else TelegramStickers echoStickerByID(sid, MornyCoeur.extra, send_chat, send_replyTo)
|
||||
private def echoStickers (mid: String)(using send_chat: Long, send_replyTo: Int)(using Update): Unit = {
|
||||
import scala.jdk.CollectionConverters.*
|
||||
if (mid isEmpty) for ((_key, _file_id) <- TelegramStickers.map asScala)
|
||||
echoSticker(_key, _file_id)
|
||||
else {
|
||||
try {
|
||||
val sticker = TelegramStickers getById mid
|
||||
echoSticker(sticker.getKey, sticker.getValue)
|
||||
} catch case _: NoSuchFieldException => {
|
||||
echo404
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def echoSticker (mid: String, file_id: String)(using send_chat: Long, send_replyTo: Int): Unit = {
|
||||
val send_mid = SendMessage(send_chat, mid)
|
||||
val send_sticker = SendSticker(send_chat, file_id)
|
||||
if (send_replyTo != -1) send_mid.replyToMessageId(send_replyTo)
|
||||
val result_send_mid = MornyCoeur.extra exec send_mid
|
||||
send_sticker.replyToMessageId(result_send_mid.message.messageId)
|
||||
MornyCoeur.extra exec send_sticker
|
||||
}
|
||||
|
||||
private[command] def echoVersion (using event: Update): Unit = {
|
||||
@ -123,7 +140,7 @@ object MornyInformation extends ITelegramCommand {
|
||||
|- Morny <code>${h(MornySystem.CODENAME toUpperCase)}</code>
|
||||
|- <code>${h(MornySystem.VERSION_BASE)}</code>$versionDeltaHTML${if (MornySystem.isGitBuild) "\n- " + versionGitHTML else ""}
|
||||
|coeur md5_hash:
|
||||
|- <code>${h(MornySystem.getJarMd5)}</code>
|
||||
|- <code>${h(MornySystem.getJarMD5)}</code>
|
||||
|coding timestamp:
|
||||
|- <code>${BuildConfig.CODE_TIMESTAMP}</code>
|
||||
|- <code>${h(formatDate(BuildConfig.CODE_TIMESTAMP, 0))} [UTC]</code>
|
||||
@ -147,7 +164,7 @@ object MornyInformation extends ITelegramCommand {
|
||||
|- <code>${Runtime.getRuntime.availableProcessors}</code> cores
|
||||
|coeur version:
|
||||
|- $getVersionAllFullTagHTML
|
||||
|- <code>${h(MornySystem.getJarMd5)}</code>
|
||||
|- <code>${h(MornySystem.getJarMD5)}</code>
|
||||
|- <code>${h(formatDate(BuildConfig.CODE_TIMESTAMP, 0))} [UTC]</code>
|
||||
|- [<code>${BuildConfig.CODE_TIMESTAMP}</code>]
|
||||
|continuous:
|
||||
|
@ -24,7 +24,7 @@ object MornyManagers {
|
||||
|
||||
val user = event.message.from
|
||||
|
||||
if (MornyCoeur.trustedInstance isTrusted user.id) {
|
||||
if (MornyCoeur.trusted isTrusted user.id) {
|
||||
|
||||
MornyCoeur.extra exec SendSticker(
|
||||
event.message.chat.id,
|
||||
@ -59,7 +59,7 @@ object MornyManagers {
|
||||
|
||||
val user = event.message.from
|
||||
|
||||
if (MornyCoeur.trustedInstance isTrusted user.id) {
|
||||
if (MornyCoeur.trusted isTrusted user.id) {
|
||||
|
||||
logger info s"call save from command by ${(TGToString as user) toStringLogTag}"
|
||||
MornyCoeur.callSaveData()
|
||||
|
@ -1,6 +1,5 @@
|
||||
package cc.sukazyo.cono.morny.bot.event
|
||||
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener
|
||||
import cc.sukazyo.cono.morny.data.TelegramStickers
|
||||
@ -20,6 +19,7 @@ object OnCallMe extends EventListener {
|
||||
if update.message.text == null then return false
|
||||
if update.message.chat.`type` != (Chat.Type Private) then return false
|
||||
|
||||
//noinspection ScalaUnnecessaryParentheses
|
||||
(update.message.text toLowerCase) match
|
||||
case "steam" | "sbeam" | "sdeam" =>
|
||||
requestItem(update.message.from, "<b>STEAM LIBRARY</b>")
|
||||
@ -51,7 +51,7 @@ object OnCallMe extends EventListener {
|
||||
private def requestLastDinner (req: Message): Unit = {
|
||||
var isAllowed = false
|
||||
var lastDinnerData: Message|Null = null
|
||||
if (MornyCoeur.trustedInstance isTrustedForDinnerRead req.from.id) {
|
||||
if (MornyCoeur.trusted isTrusted_dinnerReader req.from.id) {
|
||||
lastDinnerData = (MornyCoeur.extra exec GetChat(MornyCoeur.config.dinnerChatId)).chat.pinnedMessage
|
||||
val sendResp = MornyCoeur.extra exec ForwardMessage(
|
||||
req.from.id,
|
||||
|
@ -46,6 +46,7 @@ object OnCallMsgSend extends EventListener {
|
||||
if e.url ne null then _parsed.url(e.url)
|
||||
if e.user ne null then _parsed.user(e.user)
|
||||
if e.language ne null then _parsed.language(e.language)
|
||||
if e.customEmojiId ne null then _parsed.language(e.language)
|
||||
entities += _parsed
|
||||
MessageToSend(_body, entities toArray, parseMode, target)
|
||||
case _ => null
|
||||
@ -59,7 +60,7 @@ object OnCallMsgSend extends EventListener {
|
||||
if message.text eq null then return false
|
||||
if !(message.text startsWith "*msg") then return false
|
||||
|
||||
if (!(MornyCoeur.trustedInstance isTrusted message.from.id))
|
||||
if (!(MornyCoeur.trusted isTrusted message.from.id))
|
||||
MornyCoeur.extra exec SendSticker(
|
||||
message.chat.id,
|
||||
TelegramStickers ID_403
|
||||
@ -71,7 +72,7 @@ object OnCallMsgSend extends EventListener {
|
||||
if (message.replyToMessage eq null) return answer404
|
||||
val messageToSend = MessageToSend from message.replyToMessage
|
||||
if ((messageToSend eq null) || (messageToSend.message eq null)) return answer404
|
||||
val sendResponse = MornyCoeur.getAccount execute messageToSend.toSendMessage()
|
||||
val sendResponse = MornyCoeur.account execute messageToSend.toSendMessage()
|
||||
|
||||
if (sendResponse isOk) {
|
||||
MornyCoeur.extra exec SendSticker(
|
||||
@ -104,7 +105,7 @@ object OnCallMsgSend extends EventListener {
|
||||
if _toSend eq null then return answer404
|
||||
else _toSend
|
||||
|
||||
val targetChatResponse = MornyCoeur.getAccount execute GetChat(messageToSend.targetId)
|
||||
val targetChatResponse = MornyCoeur.account execute GetChat(messageToSend.targetId)
|
||||
if (targetChatResponse isOk) {
|
||||
def getChatDescriptionHTML (chat: Chat): String =
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
@ -128,7 +129,7 @@ object OnCallMsgSend extends EventListener {
|
||||
}
|
||||
|
||||
if messageToSend.message eq null then return true
|
||||
val testSendResponse = MornyCoeur.getAccount execute messageToSend.toSendMessage(update.message.chat.id)
|
||||
val testSendResponse = MornyCoeur.account execute messageToSend.toSendMessage(update.message.chat.id)
|
||||
.replyToMessageId(update.message.messageId)
|
||||
if (!(testSendResponse isOk))
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
|
@ -2,7 +2,7 @@ package cc.sukazyo.cono.morny.bot.event
|
||||
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener
|
||||
import cc.sukazyo.cono.morny.bot.query.InlineQueryUnit
|
||||
import cc.sukazyo.cono.morny.bot.query.{InlineQueryUnit, MornyQueries}
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.InlineQueryResult
|
||||
import com.pengrad.telegrambot.request.AnswerInlineQuery
|
||||
@ -15,7 +15,7 @@ object OnInlineQuery extends EventListener {
|
||||
|
||||
override def onInlineQuery (using update: Update): Boolean = {
|
||||
|
||||
val results: List[InlineQueryUnit[_]] = MornyCoeur.queryManager query update
|
||||
val results: List[InlineQueryUnit[_]] = MornyQueries query update
|
||||
|
||||
var cacheTime = Int.MaxValue
|
||||
var isPersonal = InlineQueryUnit.defaults.IS_PERSONAL
|
||||
|
@ -2,7 +2,7 @@ package cc.sukazyo.cono.morny.bot.event
|
||||
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.daemon.MornyDaemons
|
||||
import cc.sukazyo.cono.morny.daemon.{MedicationTimer, MornyDaemons}
|
||||
import com.pengrad.telegrambot.model.{Message, Update}
|
||||
|
||||
object OnMedicationNotifyApply extends EventListener {
|
||||
@ -14,7 +14,7 @@ object OnMedicationNotifyApply extends EventListener {
|
||||
|
||||
private def editedMessageProcess (edited: Message): Boolean = {
|
||||
if edited.chat.id != MornyCoeur.config.medicationNotifyToChat then return false
|
||||
MornyDaemons.medicationTimerInstance.refreshNotificationWrite(edited)
|
||||
MedicationTimer.refreshNotificationWrite(edited)
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ object OnTelegramCommand extends EventListener {
|
||||
if (!(inputCommand.getCommand matches "^\\w+$"))
|
||||
logger debug "not command"
|
||||
false
|
||||
else if ((inputCommand.getTarget ne null) && (inputCommand.getTarget ne MornyCoeur.getUsername))
|
||||
else if ((inputCommand.getTarget ne null) && (inputCommand.getTarget ne MornyCoeur.username))
|
||||
logger debug "not morny command"
|
||||
false
|
||||
else
|
||||
|
@ -5,7 +5,7 @@ import com.pengrad.telegrambot.model.Update
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
class MornyQueries {
|
||||
object MornyQueries {
|
||||
|
||||
private val queryInstances = Set[ITelegramQuery](
|
||||
RawText,
|
||||
|
@ -14,7 +14,7 @@ object MyInformation extends ITelegramQuery {
|
||||
|
||||
override def query (event: Update): List[InlineQueryUnit[_]] | Null = {
|
||||
|
||||
if (event.inlineQuery.query == null || (event.inlineQuery.query isBlank)) return null
|
||||
if ((event.inlineQuery.query ne null) || (event.inlineQuery.query nonEmpty)) return null
|
||||
|
||||
List(
|
||||
InlineQueryUnit(InlineQueryResultArticle(
|
||||
|
@ -0,0 +1,90 @@
|
||||
package cc.sukazyo.cono.morny.daemon
|
||||
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import com.pengrad.telegrambot.model.{Message, MessageEntity}
|
||||
import com.pengrad.telegrambot.request.{EditMessageText, SendMessage}
|
||||
import com.pengrad.telegrambot.response.SendResponse
|
||||
|
||||
import java.time.{LocalDateTime, ZoneOffset}
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.language.implicitConversions
|
||||
|
||||
object MedicationTimer extends Thread {
|
||||
|
||||
private val NOTIFY_MESSAGE = "🍥⏲"
|
||||
private val DAEMON_THREAD_NAME_DEF = "MedicationTimer"
|
||||
|
||||
private val use_timeZone = MornyCoeur.config.medicationTimerUseTimezone
|
||||
import scala.jdk.CollectionConverters.SetHasAsScala
|
||||
private val notify_atHour: Set[Int] = MornyCoeur.config.medicationNotifyAt.asScala.toSet.map(_.intValue)
|
||||
private val notify_toChat = MornyCoeur.config.medicationNotifyToChat
|
||||
|
||||
this.setName(DAEMON_THREAD_NAME_DEF)
|
||||
|
||||
private var lastNotify_messageId: Int|Null = _
|
||||
|
||||
override def run (): Unit = {
|
||||
logger info "Medication Timer started."
|
||||
while (!this.isInterrupted) {
|
||||
try {
|
||||
waitToNextRoutine()
|
||||
sendNotification()
|
||||
} catch
|
||||
case _: InterruptedException =>
|
||||
interrupt()
|
||||
logger info "MedicationTimer was interrupted, will be exit now"
|
||||
case ill: IllegalArgumentException =>
|
||||
logger warn "MedicationTimer will not work due to: " + ill.getMessage
|
||||
interrupt()
|
||||
case e =>
|
||||
logger error
|
||||
s"""unexpected error occurred on NotificationTimer
|
||||
|${exceptionLog(e)}"""
|
||||
.stripMargin
|
||||
MornyReport.exception(e)
|
||||
}
|
||||
logger info "Medication Timer stopped."
|
||||
}
|
||||
|
||||
private def sendNotification(): Unit = {
|
||||
val sendResponse: SendResponse = MornyCoeur.extra exec SendMessage(notify_toChat, NOTIFY_MESSAGE)
|
||||
if sendResponse isOk then lastNotify_messageId = sendResponse.message.messageId
|
||||
else lastNotify_messageId = null
|
||||
}
|
||||
|
||||
def refreshNotificationWrite (edited: Message): Unit = {
|
||||
if lastNotify_messageId != (edited.messageId toInt) then return
|
||||
import cc.sukazyo.cono.morny.util.CommonFormat.formatDate
|
||||
val editTime = formatDate(edited.editDate*1000, use_timeZone.getTotalSeconds/60/60)
|
||||
val entities = ArrayBuffer.empty[MessageEntity]
|
||||
if edited.entities ne null then entities ++= edited.entities
|
||||
entities += MessageEntity(MessageEntity.Type.italic, edited.text.length + "\n-- ".length, editTime.length)
|
||||
MornyCoeur.extra exec EditMessageText(
|
||||
notify_toChat,
|
||||
edited.messageId,
|
||||
edited.text + s"\n-- $editTime --"
|
||||
).entities(entities toArray:_*)
|
||||
lastNotify_messageId = null
|
||||
}
|
||||
|
||||
@throws[IllegalArgumentException]
|
||||
private[daemon] def calcNextRoutineTimestamp (baseTimeMillis: Long, zone: ZoneOffset, notifyAt: Set[Int]): Long = {
|
||||
if (notifyAt isEmpty) throw new IllegalArgumentException("notify time is not set")
|
||||
var time = LocalDateTime.ofEpochSecond(
|
||||
baseTimeMillis/1000, ((baseTimeMillis%1000)*1000*1000) toInt,
|
||||
zone
|
||||
).withMinute(0).withSecond(0).withNano(0)
|
||||
time = time plusHours 1
|
||||
while (!(notifyAt contains (time getHour))) {
|
||||
time = time plusHours 1
|
||||
}
|
||||
(time toInstant zone) toEpochMilli
|
||||
}
|
||||
|
||||
@throws[InterruptedException | IllegalArgumentException]
|
||||
private def waitToNextRoutine (): Unit = {
|
||||
Thread sleep calcNextRoutineTimestamp(System.currentTimeMillis, use_timeZone, notify_atHour)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package cc.sukazyo.cono.morny.daemon
|
||||
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
|
||||
object MornyDaemons {
|
||||
|
||||
def start (): Unit = {
|
||||
logger info "ALL Morny Daemons starting..."
|
||||
// TrackerDataManager.init();
|
||||
MedicationTimer.start()
|
||||
MornyReport.onMornyLogin()
|
||||
logger info "Morny Daemons started."
|
||||
|
||||
}
|
||||
|
||||
def stop (): Unit = {
|
||||
logger.info("ALL Morny Daemons stopping...")
|
||||
// TrackerDataManager.DAEMON.interrupt();
|
||||
MedicationTimer.interrupt()
|
||||
// TrackerDataManager.trackingLock.lock();
|
||||
try { MedicationTimer.join() }
|
||||
catch case e: InterruptedException =>
|
||||
e.printStackTrace(System.out)
|
||||
MornyReport.onMornyExit(MornyCoeur.exitReason)
|
||||
logger.info("ALL Morny Daemons STOPPED.")
|
||||
}
|
||||
|
||||
}
|
122
src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala
Normal file
122
src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala
Normal file
@ -0,0 +1,122 @@
|
||||
package cc.sukazyo.cono.morny.daemon
|
||||
|
||||
import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig}
|
||||
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException
|
||||
import com.pengrad.telegrambot.request.{BaseRequest, SendMessage}
|
||||
import com.pengrad.telegrambot.response.BaseResponse
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.bot.command.MornyInformation
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.model.User
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString
|
||||
|
||||
object MornyReport {
|
||||
|
||||
private def unsupported: Boolean = (!MornyCoeur.available) || (MornyCoeur.config.reportToChat == -1)
|
||||
|
||||
private def executeReport[T <: BaseRequest[T, R], R<: BaseResponse] (report: T): Unit = {
|
||||
if unsupported then return
|
||||
try {
|
||||
MornyCoeur.extra exec report
|
||||
} catch case e: EventRuntimeException.ActionFailed => {
|
||||
logger warn
|
||||
s"""cannot execute report to telegram:
|
||||
|${exceptionLog(e) indent 4}
|
||||
| tg-api response:
|
||||
|${(e.getResponse toString) indent 4}"""
|
||||
.stripMargin
|
||||
}
|
||||
}
|
||||
|
||||
def exception (e: Throwable, description: String|Null = null): Unit = {
|
||||
if unsupported then return
|
||||
def _tgErrFormat: String = e match
|
||||
case api: EventRuntimeException.ActionFailed =>
|
||||
// language=html
|
||||
"\n\ntg-api error:\n<pre><code>%s</code></pre>"
|
||||
.formatted(GsonBuilder().setPrettyPrinting().create.toJson(api.getResponse))
|
||||
case _ => ""
|
||||
executeReport(SendMessage(
|
||||
MornyCoeur.config.reportToChat,
|
||||
// language=html
|
||||
s"""<b>▌Coeur Unexpected Exception </b>
|
||||
|${if description ne null then h(description)+"\n" else ""}
|
||||
|<pre><code>${h(exceptionLog(e))}</code></pre>$_tgErrFormat"""
|
||||
.stripMargin
|
||||
).parseMode(ParseMode HTML))
|
||||
}
|
||||
|
||||
def unauthenticatedAction (action: String, user: User): Unit = {
|
||||
if unsupported then return
|
||||
executeReport(SendMessage(
|
||||
MornyCoeur.config.reportToChat,
|
||||
// language=html
|
||||
s"""<b>▌User unauthenticated action</b>
|
||||
|action: ${h(action)}
|
||||
|by user ${(TGToString as user) fullnameRefHtml}"""
|
||||
.stripMargin
|
||||
).parseMode(ParseMode HTML))
|
||||
}
|
||||
|
||||
def onMornyLogin(): Unit = {
|
||||
executeReport(SendMessage(
|
||||
MornyCoeur.config.reportToChat,
|
||||
// language=html
|
||||
s"""<b>▌Morny Logged in</b>
|
||||
|-v ${MornyInformation.getVersionAllFullTagHTML}
|
||||
|as user ${MornyCoeur.username}
|
||||
|
|
||||
|as config fields:
|
||||
|${sectionConfigFields(MornyCoeur.config)}"""
|
||||
.stripMargin
|
||||
).parseMode(ParseMode HTML))
|
||||
}
|
||||
|
||||
def sectionConfigFields (config: MornyConfig): String = {
|
||||
val echo = StringBuilder()
|
||||
for (field <- config.getClass.getFields) {
|
||||
// language=html
|
||||
echo ++= s"- <i><u>${field.getName}</u></i> "
|
||||
try {
|
||||
if (field.isAnnotationPresent(classOf[MornyConfig.Sensitive])) {
|
||||
echo ++= /*language=html*/ ": <i>sensitive_field</i>"
|
||||
} else {
|
||||
val value = field.get(config)
|
||||
// language=html
|
||||
echo ++= "= " ++= (if value eq null then "<i>null</i>" else s"<code>${h(value.toString)}</code>")
|
||||
}
|
||||
|
||||
} catch
|
||||
// noinspection ScalaUnnecessaryParentheses
|
||||
case e: (IllegalAccessException|IllegalArgumentException|NullPointerException) =>
|
||||
// language=html
|
||||
echo ++= s": <i>${h("<read-error>")}</i>"
|
||||
logger error
|
||||
s"""error while reading config field ${field.getName}
|
||||
|${exceptionLog(e)}""".stripMargin
|
||||
exception(e, s"error while reading config field ${field.getName}")
|
||||
echo ++= "\n"
|
||||
}
|
||||
echo dropRight 1 toString
|
||||
}
|
||||
|
||||
def onMornyExit (causedBy: AnyRef|Null): Unit = {
|
||||
if unsupported then return
|
||||
val causedTag = causedBy match
|
||||
case u: User => (TGToString as u) fullnameRefHtml
|
||||
case n if n == null => "UNKNOWN reason"
|
||||
case a: AnyRef => /*language=html*/ s"<code>${h(a.toString)}</code>"
|
||||
executeReport(SendMessage(
|
||||
MornyCoeur.config.reportToChat,
|
||||
// language=html
|
||||
s"""<b>▌Morny Exited</b>
|
||||
|from user @${MornyCoeur.username}
|
||||
|
|
||||
|by: $causedTag"""
|
||||
.stripMargin
|
||||
).parseMode(ParseMode HTML))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package cc.sukazyo.cono.morny.data
|
||||
|
||||
import cc.sukazyo.cono.morny.MornyAssets
|
||||
|
||||
import scala.language.postfixOps
|
||||
import scala.util.Using
|
||||
import java.io.IOException
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport
|
||||
|
||||
object TelegramImages {
|
||||
|
||||
class AssetsFileImage (assetsPath: String) {
|
||||
|
||||
private var cache: Array[Byte]|Null = _
|
||||
|
||||
def get:Array[Byte] =
|
||||
if cache eq null then read()
|
||||
if cache eq null then throw IllegalStateException("Failed to get assets file image.")
|
||||
cache
|
||||
|
||||
private def read (): Unit = {
|
||||
Using ((MornyAssets.pack getResource assetsPath)read) { stream =>
|
||||
try { this.cache = stream.readAllBytes() }
|
||||
catch case e: IOException => {
|
||||
logger error
|
||||
s"""Cannot read resource file:
|
||||
|${exceptionLog(e)}""".stripMargin
|
||||
MornyReport.exception(e, "Cannot read resource file.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val IMG_ABOUT: AssetsFileImage = AssetsFileImage("images/featured-image@0.5x.jpg")
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package cc.sukazyo.cono.morny.data;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 存放 bot 使用到的贴纸
|
||||
* @since 0.4.2.0
|
||||
*/
|
||||
public class TelegramStickers {
|
||||
|
||||
public static final String ID_ONLINE_STATUS_RETURN = "CAACAgEAAx0CW-CvvgAC5eBhhhODGRuu0pxKLwoQ3yMsowjviAACcycAAnj8xgVVU666si1utiIE";
|
||||
public static final String ID_HELLO = "CAACAgEAAxkBAAMnYYYWKNXO4ibo9dlsmDctHhhV6fIAAqooAAJ4_MYFJJhrHS74xUAiBA";
|
||||
public static final String ID_EXIT = "CAACAgEAAxkBAAMoYYYWt8UjvP0N405SAyvg2SQZmokAAkMiAAJ4_MYFw6yZLu06b-MiBA";
|
||||
public static final String ID_403 = "CAACAgEAAxkBAAMqYYYa_7hpXH6hMOYMX4Nh8AVYd74AAnQnAAJ4_MYFRdmmsQKLDZgiBA";
|
||||
public static final String ID_404 = "CAACAgEAAx0CSQh32gABA966YbRJpbmi2lCHINBDuo1DknSTsbsAAqUoAAJ4_MYFUa8SIaZriAojBA";
|
||||
public static final String ID_WAITING = "CAACAgEAAx0CSQh32gABA-8DYbh7W2VhJ490ucfZMUMrgMR2FW4AAm4nAAJ4_MYFjx6zpxJPWsQjBA";
|
||||
public static final String ID_SENT = "CAACAgEAAx0CSQh32gABA--zYbiyU_wOijEitp-0tSl_k7W6l3gAAgMmAAJ4_MYF4GrompjXPx4jBA";
|
||||
public static final String ID_SAVED = "CAACAgEAAx0CSQh32gABBExuYdB_G0srfhQldRWkBYxWzCOv4-IAApooAAJ4_MYFcjuNZszfQcQjBA";
|
||||
public static final String ID_PROGYNOVA = "CAACAgUAAxkBAAICm2KEuL7UQqNP7vSPCg2DHJIND6UsAAKLAwACH4WSBszIo722aQ3jJAQ";
|
||||
public static final String ID_NETWORK_ERR = "CAACAgEAAxkBAAID0WNJgNEkD726KW4vZeFlw0FlVVyNAAIXJgACePzGBb50o7O1RbxoKgQ";
|
||||
|
||||
@Nonnull
|
||||
public static Map<String, String> map () {
|
||||
final LinkedHashMap<String, String> mapping = new LinkedHashMap<>();
|
||||
for (Field object : TelegramStickers.class.getFields()) {
|
||||
if (object.getType()==String.class && object.getName().startsWith("ID_")) {
|
||||
try {
|
||||
mapping.put(object.getName(), (String)object.get(""));
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Map.Entry<String, String> getById (@Nonnull String stickerFieldID)
|
||||
throws NoSuchFieldException {
|
||||
try {
|
||||
// normally get the sticker and echo
|
||||
Field field = TelegramStickers.class.getField(stickerFieldID);
|
||||
return Map.entry(field.getName(), (String)field.get(""));
|
||||
} catch (IllegalAccessException e) {
|
||||
// java-reflect get sticker FILE_ID failed
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package cc.sukazyo.cono.morny.internal
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.collection.immutable as simm
|
||||
import java.util as j
|
||||
|
||||
object ScalaJavaConv {
|
||||
|
||||
def jSetInteger2simm (data: j.Set[Integer]): simm.Set[Int] =
|
||||
data.asScala.toSet.map(_.intValue)
|
||||
|
||||
}
|
@ -8,7 +8,7 @@ public class MornyCLI {
|
||||
|
||||
public static void main (String[] args) {
|
||||
|
||||
System.out.print("$ java -jar morny-coeur-"+MornySystem.VERSION_FULL+".jar " );
|
||||
System.out.print("$ java -jar morny-coeur-"+MornySystem.VERSION_FULL()+".jar " );
|
||||
String x;
|
||||
try (Scanner line = new Scanner(System.in)) { x = line.nextLine(); }
|
||||
ServerMain.main(UniversalCommand.format(x));
|
||||
|
@ -8,6 +8,8 @@ import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
import static cc.sukazyo.cono.morny.internal.ScalaJavaConv.jSetInteger2simm;
|
||||
|
||||
public class TestMedicationTimer {
|
||||
|
||||
@ParameterizedTest
|
||||
@ -21,12 +23,12 @@ public class TestMedicationTimer {
|
||||
2125-11-18T23:45:27.062+00, +00, 2125-11-19T07:00:00+00
|
||||
""")
|
||||
void testCalcNextRoutineTimestamp (ZonedDateTime base, ZoneOffset zoneHour, ZonedDateTime expected)
|
||||
throws MedicationTimer.NoNotifyTimeTag {
|
||||
throws IllegalArgumentException {
|
||||
final Set<Integer> at = Set.of(7, 19, 21);
|
||||
System.out.println("base.toInstant().toEpochMilli() = " + base.toInstant().toEpochMilli());
|
||||
Assertions.assertEquals(
|
||||
expected.toInstant().toEpochMilli(),
|
||||
MedicationTimer.calcNextRoutineTimestamp(base.toInstant().toEpochMilli(), zoneHour, at)
|
||||
MedicationTimer.calcNextRoutineTimestamp(base.toInstant().toEpochMilli(), zoneHour, jSetInteger2simm(at))
|
||||
);
|
||||
System.out.println(" ok");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user