scala port stage3 (not tested)

This commit is contained in:
A.C.Sukazyo Eyre 2023-09-10 22:43:39 +08:00
parent 1a31a22cd9
commit ddfe77350e
Signed by: Eyre_S
GPG Key ID: C17CE40291207874
70 changed files with 712 additions and 1423 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
package cc.sukazyo.cono.morny.bot.command;
public class Roll {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.");
}
}

View File

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

View File

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

View File

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

View File

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

View 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
}

View 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"
}

View 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")
}

View 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
}
}
}

View File

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

View 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>"
}
}

View 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
}

View File

@ -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.")

View File

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

View File

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

View File

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

View File

@ -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(

View File

@ -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())
}

View File

@ -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(
喵呜.抱抱,
喵呜.揉揉,

View File

@ -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:

View File

@ -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()

View File

@ -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,

View File

@ -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(

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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(

View File

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

View File

@ -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.")
}
}

View 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))
}
}

View File

@ -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")
}

View File

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

View File

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

View File

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

View File

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