Compare commits

..

No commits in common. "92aa0e260e69e8c5920c2bdc10e0ded3726e2fcb" and "213798dab70a630f3b1ef777d21dc54a1a2eb1d3" have entirely different histories.

179 changed files with 5807 additions and 5273 deletions

View File

@ -4,12 +4,12 @@
.vscode/
.gradle/
.settings/
/src/test/java/test/*
/src/test/resources/test/*
#build
/build/
/bin/
.metals/
.bloop/
.project
lcoal.properties

File diff suppressed because it is too large Load Diff

5
.gitignore vendored
View File

@ -4,13 +4,12 @@
.vscode/
.gradle/
.settings/
/src/test/java/test/*
/src/test/resources/test/*
#build
/build/
/bin/
/out/
.metals/
.bloop/
.project
lcoal.properties

View File

@ -1,9 +1,8 @@
plugins {
id 'scala'
id 'java'
id 'java-library'
id 'application'
id 'maven-publish'
id "io.github.ysohda.scalatest" version "0.32.1"
id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'com.github.gmazzo.buildconfig' version '4.1.2'
id 'org.ajoberstar.grgit' version '5.2.0'
@ -51,9 +50,7 @@ final long proj_code_time = proj_clean ? grgit.head().dateTime.toInstant().toEpo
final JavaVersion proj_java = JavaVersion.VERSION_17
final Charset proj_file_encoding = StandardCharsets.UTF_8
final proj_scala_api = 3
//final proj_scala_lib = proj_scala_api+'.4.0-RC1-bin-20230901-89e8dba-NIGHTLY'
final proj_scala_lib = proj_scala_api+'.3.1'
String publish_local_url = null
String publish_remote_url = null
String publish_remote_username = null
@ -75,7 +72,6 @@ repositories {
dependencies {
api "org.scala-lang:scala3-library_3:${proj_scala_lib}"
compileOnlyApi "com.github.spotbugs:spotbugs-annotations:${lib_spotbugs_v}"
implementation "cc.sukazyo:messiva:${lib_messiva_v}"
@ -85,50 +81,45 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:${lib_okhttp_v}"
implementation "com.google.code.gson:gson:${lib_gson_v}"
testImplementation "org.scalatest:scalatest_$proj_scala_api:${lib_scalatest_v}"
testImplementation "org.scalatest:scalatest-freespec_$proj_scala_api:${lib_scalatest_v}"
testRuntimeOnly "org.scala-lang.modules:scala-xml_$proj_scala_api:${lib_scalamodule_xml_v}"
testRuntimeOnly 'com.vladsch.flexmark:flexmark-all:0.64.6' // for generating HTML report // required by gradle-scalatest plugin
testImplementation platform("org.junit:junit-bom:${lib_junit_v}")
testImplementation "org.junit.jupiter:junit-jupiter"
}
tasks.withType(JavaCompile).configureEach {
sourceCompatibility proj_java.getMajorVersion()
targetCompatibility proj_java.getMajorVersion()
options.encoding = proj_file_encoding.name()
}
tasks.withType(ScalaCompile).configureEach {
sourceCompatibility proj_java.getMajorVersion()
targetCompatibility proj_java.getMajorVersion()
options.encoding = proj_file_encoding.name()
scalaCompileOptions.encoding = proj_file_encoding.name()
scalaCompileOptions.additionalParameters.add "-language:postfixOps"
// scalaCompileOptions.additionalParameters.add("-Yexplicit-nulls")
// scalaCompileOptions.additionalParameters.add "-language:experimental.saferExceptions"
}
tasks.withType(Javadoc).configureEach {
options.encoding = proj_file_encoding.name()
}
//tasks.withType(ScalaDoc).configureEach {
//}
test {
}
application {
mainClass = proj_application_main
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
java {
sourceCompatibility proj_java
targetCompatibility proj_java
withSourcesJar()
}
tasks.withType(JavaCompile).configureEach {
options.encoding = proj_file_encoding.name()
}
tasks.withType(Javadoc).configureEach {
options.encoding = proj_file_encoding.name()
options.docEncoding = proj_file_encoding.name()
options.charSet = proj_file_encoding.name()
}
tasks.test {
useJUnitPlatform()
}
buildConfig {
packageName(proj_package)
@ -159,7 +150,7 @@ shadowJar {
}
@SuppressWarnings('GrMethodMayBeStatic')
@SuppressWarnings("all")
boolean isCleanBuild () {
if (grgit == null) return false
Set<String> changes = grgit.status().unstaged.allChanges + grgit.status().staged.allChanges

View File

@ -5,17 +5,16 @@ MORNY_ARCHIVE_NAME = morny-coeur
MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono
MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s
VERSION = 1.0.0-RC5
VERSION = 1.0.0-RC4
USE_DELTA = true
VERSION_DELTA = scala1
USE_DELTA = false
VERSION_DELTA =
CODENAME = beiping
# dependencies
lib_spotbugs_v = 4.7.3
lib_scalamodule_xml_v = 2.2.0
lib_messiva_v = 0.1.1
lib_resourcetools_v = 0.2.2
@ -25,4 +24,4 @@ lib_javatelegramapi_v = 6.2.0
lib_okhttp_v = 4.11.0
lib_gson_v = 2.10.1
lib_scalatest_v = 3.2.17
lib_junit_v = 5.10.0

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -0,0 +1,57 @@
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 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 (Exception e) {
final StringWriter stackTrace = new StringWriter();
e.printStackTrace(new PrintWriter(stackTrace));
return stackTrace.toString();
}
}

View File

@ -0,0 +1,31 @@
package cc.sukazyo.cono.morny;
import java.io.IOException;
/**
* Some of the static information of Morny.
*/
public class MornyAbout {
/**
* ASCII art of Morny Featured Image.
* <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

@ -0,0 +1,19 @@
package cc.sukazyo.cono.morny;
import cc.sukazyo.restools.ResourcesPackage;
/**
* Morny assets manager.
*
* @see #pack
* @since 1.0.0-RC4
*/
public class MornyAssets {
/**
* Instance mirror of the Morny assets, the assets root is <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

@ -0,0 +1,317 @@
package cc.sukazyo.cono.morny;
import cc.sukazyo.cono.morny.bot.api.OnUpdate;
import cc.sukazyo.cono.morny.bot.command.MornyCommands;
import cc.sukazyo.cono.morny.bot.event.EventListeners;
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;
/** 当前 Morny 的 telegram 命令管理器 */
private final MornyCommands commandManager = new MornyCommands();
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");
EventListeners.registerAllListeners();
INSTANCE.account.setUpdatesListener(OnUpdate::onNormalUpdate);
if (config.commandLoginRefresh) {
logger.info("resetting telegram command list");
commandManager().automaticUpdateList();
}
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) {
commandManager.automaticRemoveList();
}
}
/**
* 为程序在虚拟机上添加退出钩子
*/
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 MornyCommands commandManager () {
return INSTANCE.commandManager;
}
@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

@ -129,7 +129,7 @@ public class MornyConfig {
* End Configs | ConfigBuilder *
* ======================================= */
private MornyConfig (@Nonnull Prototype prototype) throws CheckFailure {
public MornyConfig (@Nonnull Prototype prototype) throws CheckFailure {
this.telegramBotApiServer = prototype.telegramBotApiServer;
this.telegramBotApiServer4File = prototype.telegramBotApiServer4File;
if (prototype.telegramBotKey == null) throw new CheckFailure.NullTelegramBotKey();
@ -159,10 +159,6 @@ 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,145 @@
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

@ -0,0 +1,42 @@
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

@ -0,0 +1,296 @@
package cc.sukazyo.cono.morny;
import cc.sukazyo.cono.morny.util.CommonFormat;
import javax.annotation.Nonnull;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import static cc.sukazyo.cono.morny.Log.logger;
/**
* 程序启动入口<br>
* <br>
* 会处理程序传入的参数和选项等数据并执行对应的启动方式<br>
*
* @since 0.4.0.0
*/
public class ServerMain {
public static final long systemStartupTime = System.currentTimeMillis();
private static final String THREAD_MORNY_INIT = "morny-init";
/**
* 程序入口也是参数处理器<br>
* <br>
* {@code -} 开头的参数会被解析为选项<br>
* <br>
* 支持以下选项
* <ul>
* <li>
* {@code --version} 只输出版本信息不运行主程序此参数会导致其它所有参数失效优先级最高
* </li>
* <li>
* {@code --only-hello} 只输出欢迎字符画({@link MornyAbout#MORNY_PREVIEW_IMAGE_ASCII})不运行主程序
* 不要同时使用 {@code --no-hello}原因见下
* </li>
* <li>
* {@code --token} <b>主程序模式的必选项</b><br>
* 用于 bot 启动的 telegram bot api token
* </li>
* <li>
* {@code --username} {@link MornyCoeur#getUsername() bot username} 预定义
* </li>
* <li>
* {@code --api} 设定 {@link MornyCoeur#getAccount() bot client} 使用的 telegram bot api server
* 需要注意的是如果带有后缀 {@code /bot} 则会单独设定 api server
* 而不会适应性的同时为 {@code --api-files} 设定值
* </li>
* <li>
* {@code --api-files} 单独设定 {@link MornyCoeur#getAccount() bot client} 使用的 telegram bot file api server
* </li>
* <li>
* {@code --report-to} 设定 {@link cc.sukazyo.cono.morny.daemon.MornyReport} 的运行报告要发送到的 telegram 频道
* </li>
* <li>
* {@code --no-hello} 不在主程序启动时输出用于欢迎消息的字符画
* {@code --only-hello} 参数不兼容 会导致程序完全没有任何输出
* </li>
* <li>
* {@code --outdated-block} 会使得 {@link MornyConfig#eventIgnoreOutdated}
* 赋值为程序启动的时间从而造成阻挡程序启动之前的消息事件处理效果
* </li>
* <li>
* {@code --auto-cmd} (下面两个)选项 {@code --auto-cmd-list} {@code --auto-cmd-remove} 的合并版本
* </li>
* <li>
* {@code --auto-cmd-list} 使 morny 在启动时自动依据程序本体更新登录 bot 的命令列表
* </li>
* <li>
* {@code --auto-cmd-remove} 使 morny 在关闭时自动依据程序本体删除 bot 的命令列表
* </li>
* </ul>
* <s>除去选项之外第一个参数会被赋值为 bot telegram bot api token</s>
* <s>第二个参数会被赋值为 bot username 限定名其余的参数会被认定为无法理解</s><br>
* <b> {@code 0.4.2.3}token username 的赋值已被选项组支持</b><br>
* <b> {@code 0.5.0.4}旧的直接通过参数为 bot token & username 赋值的方式已被删除</b>
* 使用参数所进行取值的 token username 已被转移至 {@code --token} {@code --username} 参数<br>
*
* @see MornyCoeur#init
* @since 0.4.0.0
* @param args 参数组
*/
public static void main (@Nonnull String[] args) {
//#
//# 启动参数设置区块
//#
final MornyConfig.Prototype config = new MornyConfig.Prototype();
boolean versionEchoMode = false;
boolean welcomeEchoMode = false;
boolean showWelcome = true;
config.eventOutdatedTimestamp = systemStartupTime;
List<String> unknownArgs = new ArrayList<>();
//# 从命令行参数设置启动参数
for (int i = 0; i < args.length; i++) {
if (args[i].startsWith("-")) {
switch (args[i]) {
case "-d", "--dbg", "--debug" -> {
Log.debug(true);
continue;
}
case "--outdated-block", "-ob" -> {
config.eventIgnoreOutdated = true;
continue;
}
case "--no-hello", "-hf", "--quiet", "-q" -> {
showWelcome = false;
continue;
}
case "--only-hello", "-ho", "-o", "-hi" -> {
welcomeEchoMode = true;
continue;
}
case "--version", "-v" -> {
versionEchoMode = true;
continue;
}
case "--token", "-t" -> {
i++;
config.telegramBotKey = args[i];
continue;
}
case "--username", "-u" -> {
i++;
config.telegramBotUsername = args[i];
continue;
}
case "--master", "-mm" -> {
i++;
config.trustedMaster = Long.parseLong(args[i]);
continue;
}
case "--trusted-chat", "-trs" -> {
i++;
config.trustedChat = Long.parseLong(args[i]);
continue;
}
// noinspection SpellCheckingInspection
case "--trusted-reader-dinner", "-trsd" -> {
i++;
config.dinnerTrustedReaders.add(Long.parseLong(args[i]));
continue;
}
case "--dinner-chat", "-chd" -> {
i++;
config.dinnerChatId = Long.parseLong(args[i]);
continue;
}
case "--auto-cmd", "-cmd", "-c" -> {
config.commandLoginRefresh = true;
config.commandLogoutClear = true;
continue;
}
case "--auto-cmd-list", "-ca" -> {
config.commandLoginRefresh = true;
continue;
}
case "--auto-cmd-remove", "-cr" -> {
config.commandLogoutClear = true;
continue;
}
case "--api", "-a" -> {
i++;
config.telegramBotApiServer = args[i];
continue;
}
case "--api-files", "files-api", "-af" -> {
i++;
config.telegramBotApiServer4File = args[i];
continue;
}
case "--report-to" -> {
i++;
config.reportToChat = Long.parseLong(args[i]);
continue;
}
case "--medication-notify-chat", "-medc" -> {
i++;
config.medicationNotifyToChat = Long.parseLong(args[i]);
continue;
}
case "--medication-notify-timezone", "-medtz" -> {
i++;
config.medicationTimerUseTimezone = ZoneOffset.ofHours(Integer.parseInt(args[i]));
continue;
}
case "--medication-notify-times", "-medt" -> {
i++;
for (String u : args[i].split(","))
config.medicationNotifyAt.add(Integer.parseInt(u));
continue;
}
}
}
unknownArgs.add(args[i]);
}
//# 从环境变量设置启动参数
String propToken = null;
String propTokenKey = null;
for (String iKey : MornyConfig.PROP_TOKEN_KEY) {
if (System.getenv(iKey) != null) {
propToken = System.getenv(iKey);
propTokenKey = iKey;
}
}
//#
//# 启动信息输出
//# 启动相关参数的检查和处理
//#
if (showWelcome) logger.info(MornyAbout.MORNY_PREVIEW_IMAGE_ASCII);
if (welcomeEchoMode) return;
unknownArgs.forEach(arg -> logger.warn("Can't understand arg to some meaning :\n " + arg));
if (Log.debug())
logger.warn("Debug log output enabled.\n It may lower your performance, make sure that you are not in production environment.");
logger.debug("Debug log output enabled.");
if (versionEchoMode) {
logger.info(String.format("""
Morny Cono Version
- version :
Morny %s
%s%s
- md5hash :
%s
- gitstat :
%s
- co.time :
%d
%s [UTC]""",
MornySystem.CODENAME.toUpperCase(),
MornySystem.VERSION_BASE,
MornySystem.isUseDelta() ? ""+MornySystem.VERSION_DELTA : "",
MornySystem.getJarMd5(),
MornySystem.isGitBuild() ? (String.format(
"on commit %s\n %s",
MornySystem.isCleanBuild() ? "- clean-build" : "<δ/non-clean-build>",
BuildConfig.COMMIT
)) : "<non-git-build>",
BuildConfig.CODE_TIMESTAMP,
CommonFormat.formatDate(BuildConfig.CODE_TIMESTAMP, 0)
));
return;
}
logger.info(String.format("""
ServerMain.java Loaded >>>
- version %s
- Morny %s
- <%s> [%d]""",
MornySystem.VERSION_FULL,
MornySystem.CODENAME.toUpperCase(),
MornySystem.getJarMd5(), BuildConfig.CODE_TIMESTAMP
));
//#
//# Coeur 参数检查和正式启动主程序
//#
if (propToken != null) {
config.telegramBotKey = propToken;
logger.info("Parameter <token> set by EnvVar $"+propTokenKey);
}
Thread.currentThread().setName(THREAD_MORNY_INIT);
try {
MornyCoeur.init(new MornyConfig(config));
} catch (MornyConfig.CheckFailure.NullTelegramBotKey ignore) {
logger.info("Parameter required has no value:\n --token.");
} catch (MornyConfig.CheckFailure e) {
logger.error("Unknown failure occurred while starting ServerMain!:");
e.printStackTrace(System.out);
}
}
}

View File

@ -0,0 +1,66 @@
package cc.sukazyo.cono.morny.bot.api;
import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull;
@SuppressWarnings("unused")
public abstract class EventListener {
public boolean onMessage (@Nonnull Update update) {
return false;
}
public boolean onEditedMessage (@Nonnull Update update) {
return false;
}
public boolean onChannelPost (@Nonnull Update update) {
return false;
}
public boolean onEditedChannelPost (@Nonnull Update update) {
return false;
}
public boolean onInlineQuery (@Nonnull Update update) {
return false;
}
public boolean onChosenInlineResult (@Nonnull Update update) {
return false;
}
public boolean onCallbackQuery (@Nonnull Update update) {
return false;
}
public boolean onShippingQuery (@Nonnull Update update) {
return false;
}
public boolean onPreCheckoutQuery (@Nonnull Update update) {
return false;
}
public boolean onPoll (@Nonnull Update update) {
return false;
}
public boolean onPollAnswer (@Nonnull Update update) {
return false;
}
public boolean onMyChatMemberUpdated (@Nonnull Update update) {
return false;
}
public boolean onChatMemberUpdated (@Nonnull Update update) {
return false;
}
public boolean onChatJoinRequest (@Nonnull Update update) {
return false;
}
}

View File

@ -0,0 +1,118 @@
package cc.sukazyo.cono.morny.bot.api;
import cc.sukazyo.cono.morny.Log;
import cc.sukazyo.cono.morny.daemon.MornyReport;
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException;
import com.google.gson.GsonBuilder;
import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import static cc.sukazyo.cono.morny.Log.logger;
public class EventListenerManager {
private static final List<EventListener> listeners = new ArrayList<>();
private static class EventPublisher extends Thread {
private final Function<EventListener, Boolean> exec;
public EventPublisher(@Nonnull Update update, @Nonnull Function<EventListener, Boolean> exec) {
this.setName("EVT"+update.updateId());
this.exec = exec;
}
@Override
public void run () {
for (EventListener x : listeners) {
try {
if (exec.apply(x)) return;
} catch (Exception e) {
final StringBuilder errorMessage = new StringBuilder();
errorMessage.append("Event throws unexpected exception:\n");
errorMessage.append(Log.exceptionLog(e).indent(4));
if (e instanceof EventRuntimeException.ActionFailed) {
errorMessage.append("\ntg-api action: response track: ");
errorMessage.append(new GsonBuilder().setPrettyPrinting().create().toJson(
((EventRuntimeException.ActionFailed)e).getResponse()
).indent(4)).append('\n');
}
logger.error(errorMessage.toString());
MornyReport.exception(e, "on event running");
}
}
}
}
public static void addListener (@Nonnull EventListener... listeners) {
EventListenerManager.listeners.addAll(Arrays.asList(listeners));
}
public static void publishMessageEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onMessage(update)).start();
}
public static void publishEditedMessageEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onEditedMessage(update)).start();
}
public static void publishChannelPostEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onChannelPost(update)).start();
}
public static void publishEditedChannelPostEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onEditedChannelPost(update)).start();
}
public static void publishInlineQueryEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onInlineQuery(update)).start();
}
public static void publishChosenInlineResultEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onChosenInlineResult(update)).start();
}
public static void publishCallbackQueryEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onCallbackQuery(update)).start();
}
public static void publishShippingQueryEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onShippingQuery(update)).start();
}
public static void publishPreCheckoutQueryEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onPreCheckoutQuery(update)).start();
}
public static void publishPollEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onPoll(update)).start();
}
public static void publishPollAnswerEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onPollAnswer(update)).start();
}
public static void publishMyChatMemberUpdatedEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onMyChatMemberUpdated(update)).start();
}
public static void publishChatMemberUpdatedEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onChatMemberUpdated(update)).start();
}
public static void publishChatJoinRequestEvent (@Nonnull Update update) {
new EventPublisher(update, x -> x.onChatJoinRequest(update)).start();
}
}

View File

@ -0,0 +1,36 @@
package cc.sukazyo.cono.morny.bot.api;
import com.pengrad.telegrambot.model.request.InlineQueryResult;
public class InlineQueryUnit<T extends InlineQueryResult<T>> {
public static final int DEFAULT_INLINE_CACHE_TIME = 300;
public static final boolean DEFAULT_INLINE_PERSONAL_RESP = false;
private int cacheTime = DEFAULT_INLINE_CACHE_TIME;
private boolean isPersonal = DEFAULT_INLINE_PERSONAL_RESP;
public final T result;
public InlineQueryUnit (T result) {
this.result = result;
}
public int cacheTime () {
return cacheTime;
}
public InlineQueryUnit<T> cacheTime (int cacheTime) {
this.cacheTime = cacheTime;
return this;
}
public boolean isPersonal () {
return isPersonal;
}
public InlineQueryUnit<T> isPersonal (boolean isPersonal) {
this.isPersonal = isPersonal;
return this;
}
}

View File

@ -0,0 +1,59 @@
package cc.sukazyo.cono.morny.bot.api;
import com.pengrad.telegrambot.UpdatesListener;
import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull;
import java.util.List;
public class OnUpdate {
public static int onNormalUpdate (@Nonnull List<Update> updates) {
for (Update update : updates) {
if (update.message() != null) {
EventListenerManager.publishMessageEvent(update);
}
if (update.editedMessage() != null) {
EventListenerManager.publishEditedMessageEvent(update);
}
if (update.channelPost() != null) {
EventListenerManager.publishChannelPostEvent(update);
}
if (update.editedChannelPost() != null) {
EventListenerManager.publishEditedChannelPostEvent(update);
}
if (update.inlineQuery() != null) {
EventListenerManager.publishInlineQueryEvent(update);
}
if (update.chosenInlineResult() != null) {
EventListenerManager.publishChosenInlineResultEvent(update);
}
if (update.callbackQuery() != null) {
EventListenerManager.publishCallbackQueryEvent(update);
}
if (update.shippingQuery() != null) {
EventListenerManager.publishShippingQueryEvent(update);
}
if (update.preCheckoutQuery() != null) {
EventListenerManager.publishPreCheckoutQueryEvent(update);
}
if (update.poll() != null) {
EventListenerManager.publishPollEvent(update);
}
if (update.pollAnswer() != null) {
EventListenerManager.publishPollAnswerEvent(update);
}
if (update.myChatMember() != null) {
EventListenerManager.publishMyChatMemberUpdatedEvent(update);
}
if (update.chatMember() != null) {
EventListenerManager.publishChatMemberUpdatedEvent(update);
}
if (update.chatJoinRequest() != null) {
EventListenerManager.publishChatJoinRequestEvent(update);
}
}
return UpdatesListener.CONFIRMED_UPDATES_ALL;
}
}

View File

@ -0,0 +1,58 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Chat;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.DeleteMessage;
import com.pengrad.telegrambot.request.GetChatMember;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static cc.sukazyo.cono.morny.Log.logger;
public class DirectMsgClear implements ISimpleCommand {
@Nonnull @Override public String getName () { return "r"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
logger.debug("Executing command /r");
if (event.message().replyToMessage() == null) return;
logger.trace("Message is a reply");
if (event.message().replyToMessage().from().id() != MornyCoeur.getUserid()) return;
logger.trace("Message is from me");
if (System.currentTimeMillis()/1000 - event.message().replyToMessage().date() > 48*60*60) return;
logger.trace("Message is not older than 48 hours");
final boolean isTrusted = MornyCoeur.trustedInstance().isTrusted(event.message().from().id());
if (
isTrusted || (
event.message().replyToMessage().replyToMessage() != null &&
event.message().replyToMessage().replyToMessage().from().id().equals(event.message().from().id())
)
) {
MornyCoeur.extra().exec(new DeleteMessage(
event.message().chat().id(), event.message().replyToMessage().messageId()
));
if (event.message().chat().type() == Chat.Type.Private || (
MornyCoeur.extra().exec(
new GetChatMember(event.message().chat().id(), event.message().from().id())
).chatMember().canDeleteMessages()
)) {
MornyCoeur.extra().exec(new DeleteMessage(
event.message().chat().id(), event.message().messageId()
));
}
} else logger.trace("User is not trusted");
}
}

View File

@ -0,0 +1,208 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.daemon.MornyReport;
import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.CommonConvert;
import cc.sukazyo.cono.morny.util.CommonEncrypt;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape;
import com.pengrad.telegrambot.model.PhotoSize;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.GetFile;
import com.pengrad.telegrambot.request.SendDocument;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendSticker;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Base64;
import static cc.sukazyo.cono.morny.Log.logger;
public class Encryptor implements ITelegramCommand {
@Nonnull @Override public String getName () { return "encrypt"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Nonnull @Override public String getParamRule () { return "[algorithm|(l)] [(uppercase)]"; }
@Nonnull @Override public String getDescription () { return "通过指定算法加密回复的内容 (目前只支持文本)"; }
@Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
// show a simple help page
// the first paragraph lists available encrypt algorithms, and its aliases.
// with the separator "---",
// the second paragraphs shows the mods available and its aliases.
if (!command.hasArgs() || (command.getArgs()[0].equals("l") && command.getArgs().length==1)) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(), """
<b><u>base64</u></b>, b64
<b><u>base64url</u></b>, base64u, b64u
<b><u>base64decode</u></b>, base64d, b64d
<b><u>base64url-decode</u></b>, base64ud, b64ud
<b><u>sha1</u></b>
<b><u>sha256</u></b>
<b><u>sha512</u></b>
<b><u>md5</u></b>
---
<b><i>uppercase</i></b>, upper, u <i>(sha1/sha256/sha512/md5 only)</i>
"""
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
return;
}
// param1 is the encrypting algorithm, it MUST EXIST.
// so the mod will be set in param2.
// and for now only support UPPERCASE mod, so it exists in param2, or there should no any params.
boolean modUpperCase = false;
if (command.getArgs().length > 1) {
if (command.getArgs().length < 3 && (
command.getArgs()[1].equalsIgnoreCase("uppercase") ||
command.getArgs()[1].equalsIgnoreCase("u") ||
command.getArgs()[1].equalsIgnoreCase("upper")
)) {
modUpperCase = true;
} else {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(), TelegramStickers.ID_404
).replyToMessageId(event.message().messageId()));
return;
}
}
// for now, only support reply to A TEXT MESSAGE or ONE UNIVERSAL FILE
// if the replied message contains a UNIVERSAL FILE, it will use the file and will not use the text with it
// do not support TELEGRAM INLINE IMAGE/VIDEO/AUDIO yet
// do not support MULTI_FILE yet
// if there's no text message in reply, it will report null as result.
boolean inputText;
byte[] data;
String dataName;
if (event.message().replyToMessage() != null && event.message().replyToMessage().document() != null) {
inputText = false;
try {
data = MornyCoeur.getAccount().getFileContent(MornyCoeur.extra().exec(new GetFile(
event.message().replyToMessage().document().fileId()
)).file());
} catch (IOException e) {
logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage());
MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI");
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_NETWORK_ERR
).replyToMessageId(event.message().messageId()));
return;
}
dataName = event.message().replyToMessage().document().fileName();
} else if (event.message().replyToMessage() != null && event.message().replyToMessage().photo() != null) {
inputText = false;
try {
PhotoSize originPhoto = null;
long photoSize = 0;
for (PhotoSize size : event.message().replyToMessage().photo()) if (photoSize < (long)size.width() *size.height()) {
originPhoto = size;
photoSize = (long)size.width() *size.height();
} // found max size (original) image in available sizes
if (originPhoto==null) throw new IOException("no photo object from api.");
data = MornyCoeur.getAccount().getFileContent(MornyCoeur.extra().exec(new GetFile(
originPhoto.fileId()
)).file());
} catch (IOException e) {
logger.warn("NetworkRequest error: TelegramFileAPI:\n\t" + e.getMessage());
MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI");
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_NETWORK_ERR
).replyToMessageId(event.message().messageId()));
return;
}
dataName = "photo"+CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(String.valueOf(System.currentTimeMillis()))).substring(32-12).toUpperCase()+".png";
} else if (event.message().replyToMessage() != null && event.message().replyToMessage().text() != null) {
inputText = true;
data = event.message().replyToMessage().text().getBytes(CommonEncrypt.ENCRYPT_STANDARD_CHARSET);
dataName = null;
} else {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"<i><u>null</u></i>"
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
return;
}
boolean echoString = true;
String resultString = null;
byte[] result = null;
String resultName = null;
switch (command.getArgs()[0]) {
case "base64", "b64", "base64url", "base64u", "b64u" -> {
final Base64.Encoder b64tool = command.getArgs()[0].contains("u") ? Base64.getUrlEncoder() : Base64.getEncoder();
result = b64tool.encode(data);
if (!inputText) {
echoString = false;
resultName = dataName+".b64.txt";
} else {
resultString = new String(result, CommonEncrypt.ENCRYPT_STANDARD_CHARSET);
}
}
case "base64decode", "base64d", "b64d", "base64url-decode", "base64ud", "b64ud" -> {
final Base64.Decoder b64tool = command.getArgs()[0].contains("u") ? Base64.getUrlDecoder() : Base64.getDecoder();
try { result = b64tool.decode(data); }
catch (IllegalArgumentException e) {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(), TelegramStickers.ID_404
).replyToMessageId(event.message().messageId()));
return;
}
if (!inputText) {
echoString = false;
resultName = CommonEncrypt.base64FilenameLint(dataName);
} else {
resultString = new String(result, CommonEncrypt.ENCRYPT_STANDARD_CHARSET);
}
}
case "md5" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(data));
case "sha1" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashSha1(data));
case "sha256" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashSha256(data));
case "sha512" -> resultString = CommonConvert.byteArrayToHex(CommonEncrypt.hashSha512(data));
default -> {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(), TelegramStickers.ID_404
).replyToMessageId(event.message().messageId()));
return;
}
}
if (modUpperCase) {
// modUpperCase support only algorithm that showed as HEX value.
// it means md5, sha1, sha256, sha512 here.
// other will report wrong param.
switch (command.getArgs()[0]) {
case "md5", "sha1", "sha256", "sha512" -> {
assert resultString != null;
resultString = resultString.toUpperCase();
}
default -> {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(), TelegramStickers.ID_404
).replyToMessageId(event.message().messageId()));
return;
}
}
}
if (echoString) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"<pre><code>" + MsgEscape.escapeHtml(resultString) + "</code></pre>"
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
} else {
MornyCoeur.extra().exec(new SendDocument(
event.message().chat().id(),
result
).fileName(resultName).replyToMessageId(event.message().messageId()));
}
}
}

View File

@ -0,0 +1,99 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.MornyTrusted;
import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle;
import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.SendSticker;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* {@link OnEventHackHandle} 的命令行前端
* @since 0.4.2.0
*/
public class EventHack implements ITelegramCommand {
@Nonnull @Override public String getName () { return "event_hack"; }
@Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return "[(user|group|any)]"; }
@Nonnull @Override public String getDescription () { return "输出 bot 下一个获取到的事件序列化数据"; }
/**
* {@link OnEventHackHandle} 的命令行前端<br>
* <br>
* 实现了通过命令行进行 EventHack 功能<br>
* 支持三种模式默认为 {@link OnEventHackHandle.HackType#USER USER}
* {@link OnEventHackHandle.HackType#ANY ANY} 将会通过 {@link MornyTrusted#isTrusted(long)} 检查触发用户的权限
*
* @param event 命令基础参数触发的事件对象本身
* @param command 命令基础参数解析出的命令对象
* @since 0.4.2.0
*/
@Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
enum Status {
OK,
FORBIDDEN_FOR_ANY
}
Status status;
String x_mode = "";
if (command.hasArgs()) {
x_mode = command.getArgs()[0];
}
switch (x_mode) {
case "any" -> {
if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) {
OnEventHackHandle.registerHack(
event.message().messageId(),
event.message().from().id(),
event.message().chat().id(),
OnEventHackHandle.HackType.ANY
);
status = Status.OK;
} else {
status = Status.FORBIDDEN_FOR_ANY;
}
}
case "group" -> {
OnEventHackHandle.registerHack(
event.message().messageId(),
event.message().from().id(),
event.message().chat().id(),
OnEventHackHandle.HackType.GROUP
);
status = Status.OK;
}
default -> {
OnEventHackHandle.registerHack(
event.message().messageId(),
event.message().from().id(),
event.message().chat().id(),
OnEventHackHandle.HackType.USER
);
status = Status.OK;
}
}
switch (status) {
case OK -> MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_WAITING
).replyToMessageId(event.message().messageId())
);
case FORBIDDEN_FOR_ANY -> MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_403
).replyToMessageId(event.message().messageId())
);
}
}
}

View File

@ -0,0 +1,85 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.User;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.GetChatMember;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.response.GetChatMemberResponse;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class GetUsernameAndId implements ITelegramCommand {
@Nonnull @Override public String getName () { return "user"; }
@Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return "[userid]"; }
@Nonnull @Override public String getDescription () { return "获取指定或回复的用户相关信息"; }
@Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
final String[] args = command.getArgs();
// 不支持大于一个参数
if (args.length > 1) { MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"[Unavailable] Too much arguments."
).replyToMessageId(event.message().messageId())); return; }
// 发送者自己的 id
long userId = event.message().from().id();
// 如果有回复某个人则使用被回复人的 id
if (event.message().replyToMessage()!= null) {
userId = event.message().replyToMessage().from().id();
}
// 如果有指定 id则使用指定的 id
if (args.length > 0) {
try {
userId = Long.parseLong(args[0]);
} catch (NumberFormatException e) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"[Unavailable] " + e.getMessage()
).replyToMessageId(event.message().messageId()));
return;
}
}
// 重新获取用户对象
final GetChatMemberResponse response = MornyCoeur.getAccount().execute(
new GetChatMember(event.message().chat().id(), userId)
);
if (response.chatMember() == null) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"[Unavailable] user not found."
).replyToMessageId(event.message().messageId()));
return;
}
// 获取并发送用户信息
final User user = response.chatMember().user();
if (user.id() == 136817688) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"<code>$__channel_identify</code>"
));
return;
}
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
TelegramUserInformation.informationOutputHTML(user)
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
}
}

View File

@ -0,0 +1,19 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public interface ISimpleCommand {
@Nonnull
String getName();
@Nullable
String[] getAliases();
void execute (@Nonnull InputCommand command, @Nonnull Update event);
}

View File

@ -0,0 +1,12 @@
package cc.sukazyo.cono.morny.bot.command;
import javax.annotation.Nonnull;
public interface ITelegramCommand extends ISimpleCommand {
@Nonnull
String getParamRule();
@Nonnull
String getDescription();
}

View File

@ -0,0 +1,85 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.data.ip186.IP186QueryResponse;
import cc.sukazyo.cono.morny.data.ip186.IP186QueryHandler;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendMessage;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
/**
* {@value IP186QueryHandler#SITE_URL} 查询的 telegram 命令前端
* @since 0.4.2.10
*/
public class Ip186Query {
public static final String CMD_IP = "ip";
public static final String CMD_WHOIS = "whois";
public static class Ip implements ITelegramCommand {
@Nonnull @Override public String getName () { return CMD_IP; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Nonnull @Override public String getParamRule () { return "[ip]"; }
@Nonnull @Override public String getDescription () { return "通过 https://ip.186526.xyz 查询 ip 资料"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { exec(event, command); }
}
public static class Whois implements ITelegramCommand {
@Nonnull @Override public String getName () { return CMD_WHOIS; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Nonnull @Override public String getParamRule () { return "[domain]"; }
@Nonnull @Override public String getDescription () { return "通过 https://ip.186526.xyz 查询域名资料"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { exec(event, command); }
}
private static void exec (@Nonnull Update event, @Nonnull InputCommand command) {
String arg = null;
if (!command.hasArgs()) {
if (event.message().replyToMessage() != null) {
arg = event.message().replyToMessage().text();
}
} else if (command.getArgs().length > 1) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"[Unavailable] Too much arguments."
).replyToMessageId(event.message().messageId()));
return;
} else {
arg = command.getArgs()[0];
}
if (arg == null) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"[Unavailable] No ip defined."
).replyToMessageId(event.message().messageId()));
return;
}
try {
IP186QueryResponse response = switch (command.getCommand()) {
case CMD_IP -> IP186QueryHandler.queryIp(arg);
case CMD_WHOIS -> IP186QueryHandler.queryWhoisPretty(arg);
default -> throw new IllegalArgumentException("Unknown 186-IP query method " + command.getCommand());
};
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
escapeHtml(response.url()) + "\n<code>" + escapeHtml(response.body()) + "</code>"
).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId()));
} catch (Exception e) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"[Exception] in query:\n<code>" + escapeHtml(e.getMessage()) + "</code>"
).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId()));
}
}
}

View File

@ -0,0 +1,297 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.event.OnUniMeowTrigger;
import cc.sukazyo.cono.morny.daemon.MornyReport;
import cc.sukazyo.cono.morny.data.MornyJrrp;
import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString;
import com.pengrad.telegrambot.model.BotCommand;
import com.pengrad.telegrambot.model.DeleteMyCommands;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendSticker;
import com.pengrad.telegrambot.request.SetMyCommands;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import static cc.sukazyo.cono.morny.Log.logger;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
public class MornyCommands {
private final Map<String, ISimpleCommand> commands = new LinkedHashMap<>();
private void pushCommandTo (@Nonnull String name, @Nonnull ISimpleCommand instance) {
if (commands.containsKey(name)) {
logger.warn(String.format("""
Telegram command instance named "%s" already exists and will be override by another command instance
- current: %s
- new : %s""",
name,
commands.get(name).getClass().getName(),
instance.getClass().getName()
));
}
commands.put(name, instance);
}
public void register (@Nonnull ISimpleCommand... list) {
for (ISimpleCommand instance : list) {
final String[] aliases = instance.getAliases();
pushCommandTo(instance.getName(), instance);
if (aliases!=null) for (String alias : aliases) pushCommandTo(alias, instance);
}
}
@SuppressWarnings("NonAsciiCharacters")
public MornyCommands () {
register(
new ON(),
new Hello(), /* new {@link HelloOnStart}, */
new MornyInfoOnHello(),
new GetUsernameAndId(),
new EventHack(),
new Nbnhhsh(),
new Ip186Query.Ip(),
new Ip186Query.Whois(),
new Encryptor(),
new SaveData(),
new MornyInformation(),
new Version(),
new MornyRuntime(),
new Jrrp(),
new Exit(), new ExitAlias()
);
// 特殊的命令
register(
new Testing(),
new DirectMsgClear()
);
// 统一注册这些奇怪的东西&.&
register(
new 私わね(),
new 喵呜.Progynova()
);
// special: 注册出于兼容使用的特别 event 的数据
OnUniMeowTrigger.register(
new 喵呜.抱抱(),
new 喵呜.揉揉(),
new 喵呜.蹭蹭(),
new 喵呜.贴贴()
);
}
public boolean execute (@Nonnull InputCommand command, @Nonnull Update event) {
if (commands.containsKey(command.getCommand())) {
commands.get(command.getCommand()).execute(command, event);
return true;
}
return nonCommandExecutable(event, command);
}
public void automaticUpdateList () {
BotCommand[] commandList = getCommandListTelegram();
automaticRemoveList();
MornyCoeur.extra().exec(new SetMyCommands(
commandList
));
logger.info("automatic updated telegram command list :\n" + commandListToString(commandList));
}
public void automaticRemoveList () {
MornyCoeur.extra().exec(new DeleteMyCommands());
logger.info("cleaned up command list.");
}
private String commandListToString (@Nonnull BotCommand[] list) {
StringBuilder builder = new StringBuilder();
for (BotCommand signal : list) {
builder.append(signal.command()).append(" - ").append(signal.description()).append("\n");
}
return builder.substring(0, builder.length()-1);
}
public BotCommand[] getCommandListTelegram () {
final List<BotCommand> telegramFormatListing = new ArrayList<>();
commands.forEach((regKey, command) -> {
if (command instanceof ITelegramCommand && regKey.equals(command.getName())) {
telegramFormatListing.add(formatTelegramCommandListLine(
command.getName(),
((ITelegramCommand)command).getParamRule(),
((ITelegramCommand)command).getDescription()
));
if (command.getAliases() != null) for (String alias : command.getAliases()) {
telegramFormatListing.add(formatTelegramCommandListLine(alias, "", ""));
}
}
});
return telegramFormatListing.toArray(BotCommand[]::new);
}
private BotCommand formatTelegramCommandListLine (@Nonnull String commandName, @Nonnull String paramRule, @Nonnull String intro) {
return new BotCommand(commandName, paramRule.isEmpty() ? (intro) : (paramRule+" - "+intro));
}
private boolean nonCommandExecutable (Update event, InputCommand command) {
if (command.getTarget() == null) return false; // 无法解析的命令转交事件链后代处理
else { // 无法解析的显式命令格式报错找不到命令
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_404
).replyToMessageId(event.message().messageId())
);
return true;
}
}
/// /// /// /// /// /// /// /// ///
///
/// Old Simple Command Block
///
private static class ON implements ITelegramCommand {
@Nonnull @Override public String getName () { return "o"; }
@Nullable
@Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "检查是否在线"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandOnExec(event); }
}
private static void onCommandOnExec (@Nonnull Update event) {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_ONLINE_STATUS_RETURN
).replyToMessageId(event.message().messageId())
);
}
private static class Hello implements ITelegramCommand {
@Nonnull @Override public String getName () { return "hello"; }
@Nullable @Override public String[] getAliases () { return new String[]{"hi"}; }
@Nonnull @Override public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "打招呼"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandHelloExec(event); }
}
/**
* {@link Hello} on special command /start
* Deprecated due to new {@link MornyInfoOnHello}
*/
@Deprecated @SuppressWarnings("unused")
private static class HelloOnStart implements ISimpleCommand { @Nonnull @Override public String getName () { return "start"; }@Nullable @Override public String[] getAliases () { return new String[0]; }@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandHelloExec(event); }}
private static void onCommandHelloExec (@Nonnull Update event) {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_HELLO
).replyToMessageId(event.message().messageId())
);
}
private static class Exit implements ITelegramCommand {
@Nonnull @Override public String getName () { return "exit"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Nonnull @Override public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "关闭 Bot (仅可信成员)"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandExitExec(event); }
}
private static class ExitAlias implements ISimpleCommand {
@Nonnull @Override public String getName () { return "quit"; }
@Nullable @Override public String[] getAliases () { return new String[]{"stop"}; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandExitExec(event); }
}
private static void onCommandExitExec (@Nonnull Update event) {
if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_EXIT
).replyToMessageId(event.message().messageId())
);
logger.info("Morny exited by user " + TGToString.as(event.message().from()).toStringLogTag());
MornyCoeur.exit(0, event.message().from());
} else {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_403
).replyToMessageId(event.message().messageId())
);
logger.info("403 exited tag from user " + TGToString.as(event.message().from()).toStringLogTag());
MornyReport.unauthenticatedAction("/exit", event.message().from());
}
}
private static class Version implements ISimpleCommand {
@Nonnull @Override public String getName () { return "version"; }
@Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Deprecated public String getParamRule () { return ""; }
@Nonnull @Deprecated public String getDescription () { return "检查 Bot 版本信息"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformation.echoVersion(event); }
}
private static class MornyRuntime implements ISimpleCommand {
@Nonnull @Override public String getName () { return "runtime"; }
@Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Deprecated public String getParamRule () { return ""; }
@Nonnull @Deprecated public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { MornyInformation.echoRuntime(event); }
}
private static class Jrrp implements ITelegramCommand {
@Nonnull @Override public String getName () { return "jrrp"; }
@Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "获取 (假的) jrrp"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandJrrpExec(event); }
}
private static void onCommandJrrpExec (Update event) {
final double jrrp = MornyJrrp.getJrrpFromTelegramUser(event.message().from(), System.currentTimeMillis());
final String endChar = jrrp>70 ? "!" : jrrp>30 ? ";" : "...";
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
String.format(
"%s 在(utc的)今天的运气指数是———— <code>%.2f%%</code> %s",
TGToString.as(event.message().from()).fullnameRefHtml(),
jrrp, escapeHtml(endChar)
)
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
}
private static class SaveData implements ITelegramCommand {
@Nonnull @Override public String getName () { return "save"; }
@Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "保存缓存数据到文件(仅可信成员)"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onSaveDataExec(event); }
}
/**
* @since 0.4.3.0
*/
private static void onSaveDataExec (Update event) {
if (MornyCoeur.trustedInstance().isTrusted(event.message().from().id())) {
logger.info("called save from command by " + TGToString.as(event.message().from()).toStringLogTag());
MornyCoeur.callSaveData();
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_SAVED
).replyToMessageId(event.message().messageId())
);
} else {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_403
).replyToMessageId(event.message().messageId())
);
logger.info("403 call save tag from user " + TGToString.as(event.message().from()).toStringLogTag());
MornyReport.unauthenticatedAction("/save", event.message().from());
}
}
}

View File

@ -0,0 +1,44 @@
package cc.sukazyo.cono.morny.bot.command;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendPhoto;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* The implementation of Telegram special command `/start`.
*
* @see MornyInformation related class where some data comes from.
*
* @since 1.0.0-RC4
*/
public class MornyInfoOnHello implements ISimpleCommand {
@Nonnull @Override public String getName() { return "start"; }
@Nullable @Override public String[] getAliases() { return new String[0]; }
// @Override public String getParamRule() { return ""; }
// @Override public String getDescription() { return "" }
@Override
public void execute(@Nonnull InputCommand command, @Nonnull Update event) {
MornyCoeur.extra().exec(new SendPhoto(
event.message().chat().id(),
MornyInformation.getAboutPic()
).caption("""
欢迎使用 <b>Morny Cono</b><i>来自安妮的侍从小鼠</i>
Morny 具有各种各样的功能
%s
你可以随时通过 /info 重新获得这些信息""".formatted(MornyInformation.getMornyAboutLinksHTML())
).parseMode(ParseMode.HTML));
}
}

View File

@ -0,0 +1,301 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.BuildConfig;
import cc.sukazyo.cono.morny.MornyAbout;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.MornySystem;
import cc.sukazyo.cono.morny.data.TelegramImages;
import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.tgapi.ExtraAction;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendPhoto;
import com.pengrad.telegrambot.request.SendSticker;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Objects;
import static cc.sukazyo.cono.morny.util.CommonFormat.formatDate;
import static cc.sukazyo.cono.morny.util.CommonFormat.formatDuration;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
public class MornyInformation implements ITelegramCommand {
private static final String SUB_STICKER = "stickers";
private static final String SUB_RUNTIME = "runtime";
private static final String SUB_VERSION = "version";
private static final String SUB_VERSION_2 = "v";
@Nonnull @Override public String getName () { return "info"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Nonnull @Override public String getParamRule () { return "[(version|runtime|stickers[.IDs])]"; }
@Nonnull @Override public String getDescription () { return "输出当前 Morny 的各种信息"; }
@Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
if (!command.hasArgs()) {
echoInfo(event.message().chat().id(), event.message().messageId());
return;
}
final String action = command.getArgs()[0];
if (action.startsWith(SUB_STICKER)) {
echoStickers(command, event);
} else if (action.equals(SUB_RUNTIME)) {
echoRuntime(event);
} else if (action.equals(SUB_VERSION) || action.equals(SUB_VERSION_2)) {
echoVersion(event);
} else {
echo404(event);
}
}
/**
* Subcommand <u>/info</u> without params.
*
* @since 1.0.0-RC4
*/
public void echoInfo (long chatId, int replayToMessage) {
MornyCoeur.extra().exec(new SendPhoto(
chatId,
getAboutPic()
).caption("""
<b>Morny Cono</b>
来自安妮的侍从小鼠
%s""".formatted(getMornyAboutLinksHTML())
).parseMode(ParseMode.HTML).replyToMessageId(replayToMessage));
}
/**
* subcommand <u>/info stickers</u>
*
* @see #SUB_STICKER
*/
public void echoStickers (@Nonnull InputCommand command, @Nonnull Update event) {
final long echoTo = event.message().chat().id();
final int replyToMessage = event.message().messageId();
String id = null;
if (command.getArgs()[0].equals(SUB_STICKER)) {
if (command.getArgs().length == 1) {
id = "";
} else if (command.getArgs().length == 2) {
id = command.getArgs()[1];
}
} else if (command.getArgs().length == 1) {
if (command.getArgs()[0].startsWith(SUB_STICKER+".") || command.getArgs()[0].startsWith(SUB_STICKER+"#")) {
id = command.getArgs()[0].substring(SUB_STICKER.length()+1);
}
}
if (id == null) { echo404(event); return; }
echoStickers(id, echoTo, replyToMessage);
}
/**
* telegram 输出一个或全部 sticker.
*
* @param id
* sticker {@link TelegramStickers} 中的字段名
* 使用 {@link ""}(空字符串)(不是{@link null}) 表示输出全部 sticker
* @param chatId 目标 chat id
* @param messageId 要回复的消息 id依据 {@link TelegramStickers#echoStickerByID(String, ExtraAction, long, int) 上游}
* 逻辑使用 {@link -1} 表示不回复消息
*
* @see TelegramStickers#echoStickerByID(String, ExtraAction, long, int)
* @see TelegramStickers#echoAllStickers(ExtraAction, long, int)
*/
public static void echoStickers (@Nonnull String id, long chatId, int messageId) {
if (id.isEmpty()) TelegramStickers.echoAllStickers(MornyCoeur.extra(), chatId, messageId);
else TelegramStickers.echoStickerByID(id, MornyCoeur.extra(), chatId, messageId);
}
/**
* Subcommand <u>/info runtime</u>.
*
* @see #SUB_RUNTIME
*
* @since 1.0.0-alpha4
*/
public static void echoRuntime (@Nonnull Update event) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
String.format("""
system:
- <code>%s</code>
- <code>%s</code> (<code>%s</code>) <code>%s</code>
java runtime:
- <code>%s</code>
- <code>%s</code>
vm memory:
- <code>%d</code> / <code>%d</code> MB
- <code>%d</code> cores
coeur version:
- %s
- <code>%s</code>
- <code>%s [UTC]</code>
- [<code>%d</code>]
continuous:
- <code>%s</code>
- [<code>%d</code>]
- <code>%s [UTC]</code>
- [<code>%d</code>]""",
// system
escapeHtml(getRuntimeHostName()==null ? "<unknown-host>" : getRuntimeHostName()),
escapeHtml(System.getProperty("os.name")),
escapeHtml(System.getProperty("os.arch")),
escapeHtml(System.getProperty("os.version")),
// java
escapeHtml(System.getProperty("java.vm.vendor")+"."+System.getProperty("java.vm.name")),
escapeHtml(System.getProperty("java.vm.version")),
// memory
Runtime.getRuntime().totalMemory() / 1024 / 1024,
Runtime.getRuntime().maxMemory() / 1024 / 1024,
Runtime.getRuntime().availableProcessors(),
// version
getVersionAllFullTagHtml(),
escapeHtml(MornySystem.getJarMd5()),
escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0)),
BuildConfig.CODE_TIMESTAMP,
// continuous
escapeHtml(formatDuration(System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp)),
System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp,
escapeHtml(formatDate(MornyCoeur.coeurStartTimestamp, 0)),
MornyCoeur.coeurStartTimestamp
)
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
}
/**
* Subcommand <u>/info version</u> or <u>/info v</u>.
*
* @see #SUB_VERSION
* @see #SUB_VERSION_2
*
* @since 1.0.0-alpha4
*/
public static void echoVersion (@Nonnull Update event) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
String.format(
"""
version:
- Morny <code>%s</code>
- <code>%s</code>%s%s
core md5_hash:
- <code>%s</code>
coding timestamp:
- <code>%d</code>
- <code>%s [UTC]</code>""",
escapeHtml(MornySystem.CODENAME.toUpperCase()),
escapeHtml(MornySystem.VERSION_BASE),
MornySystem.isUseDelta() ? String.format("-δ<code>%s</code>", escapeHtml(Objects.requireNonNull(MornySystem.VERSION_DELTA))) : "",
MornySystem.isGitBuild() ? "\n- git "+getVersionGitTagHtml() : "",
escapeHtml(MornySystem.getJarMd5()),
BuildConfig.CODE_TIMESTAMP,
escapeHtml(formatDate(BuildConfig.CODE_TIMESTAMP, 0))
)
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
}
/**
* 取得 {@link MornySystem} git commit 相关版本信息的 HTML 格式化标签.
*
* @return 格式类似于 <u>{@code 28e8c82a.δ}</u> 的以 HTML 方式格式化的版本号组件
* 其中 {@code .δ} 对应着 {@link MornySystem#isCleanBuild}
* commit tag 字段如果支援 {@link MornySystem#currentCodePath} 则会以链接形式解析否则则为 code 格式
* <small>为了对 telegram api html 格式兼容所以不支援嵌套链接与code标签</small>
* 如果 {@link MornySystem#isGitBuild} {@link false}则方法会返回 {@link ""}
*
* @since 1.0.0-beta2
*/
@Nonnull
public static String getVersionGitTagHtml () {
if (!MornySystem.isGitBuild()) return "";
final StringBuilder g = new StringBuilder();
final String cp = MornySystem.currentCodePath();
if (cp == null) g.append(String.format("<code>%s</code>", BuildConfig.COMMIT.substring(0, 8)));
else g.append(String.format("<a href='%s'>%s</a>", MornySystem.currentCodePath(), BuildConfig.COMMIT.substring(0, 8)));
if (!MornySystem.isCleanBuild()) g.append(".<code>δ</code>");
return g.toString();
}
/**
* 取得完整 Morny 版本的 HTML 格式化标签.
* <p>
* 相比于 {@link MornySystem#VERSION_FULL}这个版本号还包含了 {@link MornySystem#CODENAME 版本 codename}
* 各个部分也被以 HTML 的格式进行了格式化以可以更好的在富文本中插入使用.
* @return 基于 HTML 标签进行了格式化了的类似于
* <u><code>{@link MornySystem#VERSION_BASE 5.38.2-alpha1}{@link MornySystem#isUseDelta() -δ}{@link MornySystem#VERSION_DELTA tt}{@link MornySystem#isGitBuild() +git.}{@link #getVersionGitTagHtml() 28e8c82a.δ}*{@link MornySystem#CODENAME TOKYO}</code></u>
* 的版本号
* @since 1.0.0-beta2
*/
@Nonnull
public static String getVersionAllFullTagHtml () {
final StringBuilder v = new StringBuilder();
v.append("<code>").append(MornySystem.VERSION_BASE).append("</code>");
if (MornySystem.isUseDelta()) v.append("-δ<code>").append(MornySystem.VERSION_DELTA).append("</code>");
if (MornySystem.isGitBuild()) v.append("+git.").append(getVersionGitTagHtml());
v.append("*<code>").append(MornySystem.CODENAME.toUpperCase()).append("</code>");
return v.toString();
}
/**
* 获取 coeur 运行时的宿主机的主机名
* @return coeur 宿主机主机名或者 {@link null} 表示获取失败
*/
@Nullable
public static String getRuntimeHostName () {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
return null;
}
}
/**
* Get the about-pic (intro picture or featured image) of Morny.
*
* @return the Telegram file binary data of the about-pic.
* @throws IllegalStateException {@link TelegramImages.AssetsFileImage#get() get() image data} may
* throws {@link IllegalStateException} while read error.
*/
@Nonnull
public static byte[] getAboutPic () {
return TelegramImages.IMG_ABOUT.get();
}
private static void echo404 (@Nonnull Update event) {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_404
).replyToMessageId(event.message().messageId()));
}
/**
* The formatted about links of Morny Cono and Morny Coeur.
* <p>
* With the Telegram HTML formatting, used in <u>/info</u> and <u>/start</u>.
* Provided the end user the links that can find resources about Morny.
*/
@Nonnull
public static String getMornyAboutLinksHTML () {
return """
<a href='%s'>source code</a> | <a href='%s'>backup</a>
<a href='%s'>反馈 / issue tracker</a>
<a href='%s'>使用说明书 / user guide & docs</a>""".formatted(
MornyAbout.MORNY_SOURCECODE_LINK, MornyAbout.MORNY_SOURCECODE_SELF_HOSTED_MIRROR_LINK,
MornyAbout.MORNY_ISSUE_TRACKER_LINK,
MornyAbout.MORNY_USER_GUIDE_LINK
);
}
}

View File

@ -0,0 +1,80 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendMessage;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.data.NbnhhshQuery;
import com.pengrad.telegrambot.request.SendSticker;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
public class Nbnhhsh implements ITelegramCommand {
@Nonnull @Override public String getName () { return "nbnhhsh"; }
@Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return "[text]"; }
@Nonnull @Override public String getDescription () { return "检索文本内 nbnhhsh 词条"; }
@Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
class TagNoContent extends Exception {}
try {
String queryTarget;
if (event.message().replyToMessage() != null && event.message().replyToMessage().text() != null)
queryTarget = event.message().replyToMessage().text();
else if (command.hasArgs())
queryTarget = stringsConnecting(command.getArgs(), " ", 0, command.getArgs().length-1);
else {
throw new TagNoContent();
}
NbnhhshQuery.GuessResult response = NbnhhshQuery.sendGuess(queryTarget);
StringBuilder message = new StringBuilder("<a href=\"https://lab.magiconch.com/nbnhhsh/\">## Result of nbnhhsh query :</a>");
for (NbnhhshQuery.Word word : response.words) {
if (word.trans != null && word.trans.length == 0) word.trans = null;
if (word.inputting != null && word.inputting.length == 0) word.inputting = null;
if (word.trans == null && word.inputting == null) continue;
message.append("\n\n<b>[[ ").append(escapeHtml(word.name)).append(" ]]</b>");
if (word.trans != null) for (String trans : word.trans) {
message.append("\n* <i>").append(escapeHtml(trans)).append("</i>");
}
if (word.inputting != null) {
if (word.trans != null) message.append("\n");
message.append(" maybe:");
for (String trans : word.inputting) {
message.append("\n` <i>").append(escapeHtml(trans)).append("</i>");
}
}
}
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
message.toString()
).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId()));
} catch (TagNoContent tag) {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(), TelegramStickers.ID_404
).replyToMessageId(event.message().messageId()));
} catch (Exception e) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"[Exception] in query:\n<code>" + escapeHtml(e.getMessage()) + "</code>"
).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId()));
}
}
}

View File

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

View File

@ -0,0 +1,36 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendMessage;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class Testing implements ISimpleCommand {
@Nonnull
@Override
public String getName () {
return "test";
}
@Nullable
@Override
public String[] getAliases () {
return null;
}
@Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"<b>Just</b> a TEST command."
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
}
}

View File

@ -0,0 +1,7 @@
/**
* 一系列的 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

@ -0,0 +1,82 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendSticker;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* <b>WARNING</b> that {@link cc.sukazyo.cono.morny.bot.event.OnTelegramCommand}
* 并不能够处理非 english word 字符之外的命令.
* <p>
* 出于这个限制以下几个命令目前都无法使用
* @see 抱抱
* @see 揉揉
* @see 蹭蹭
* @see 贴贴
*/
@SuppressWarnings("NonAsciiCharacters")
public class 喵呜 {
public static class 抱抱 implements ISimpleCommand {
@Nonnull @Override public String getName () { return "抱抱"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
replyingSet(event, "抱抱", "抱抱");
}
}
public static class 揉揉 implements ISimpleCommand {
@Nonnull @Override public String getName () { return "揉揉"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
replyingSet(event, "蹭蹭", "摸摸");
}
}
public static class 蹭蹭 implements ISimpleCommand {
@Nonnull @Override public String getName () { return "蹭蹭"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
replyingSet(event, "揉揉", "蹭蹭");
}
}
public static class 贴贴 implements ISimpleCommand {
@Nonnull @Override public String getName () { return "贴贴"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
replyingSet(event, "贴贴", "贴贴");
}
}
private static void replyingSet (@Nonnull Update event, @Nonnull String whileRec, @Nonnull String whileNew) {
final boolean isNew = event.message().replyToMessage() == null;
final Message target = isNew ? event.message() : event.message().replyToMessage();
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
isNew ? whileNew : whileRec
).replyToMessageId(target.messageId()).parseMode(ParseMode.HTML));
}
public static class Progynova implements ITelegramCommand {
@Nonnull @Override public String getName () { return "install"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Nonnull @Override public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "抽取一个神秘盒子"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_PROGYNOVA
).replyToMessageId(event.message().messageId()));
}
}
}

View File

@ -0,0 +1,40 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.SendMessage;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import static cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue;
@SuppressWarnings("NonAsciiCharacters")
public class 私わね implements ISimpleCommand {
@Nonnull
@Override public String getName () { return "me"; }
@Nullable
@Override public String[] getAliases () { return null; }
@Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
if (probabilityTrue(521)) {
// 可以接入未来的心情系统如果有的话
// final String text = switch (ThreadLocalRandom.current().nextInt(11)) {
// case 0,7,8,9,10 -> "才不是";
// case 1,2,3,6 -> "才不是!";
// case 4,5 -> "才不是..";
// default -> throw new IllegalStateException("Unexpected random value in 私わね command.");
// };
final String text = "/打假";
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
text
).replyToMessageId(event.message().messageId()));
}
}
}

View File

@ -0,0 +1,42 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.bot.api.EventListenerManager;
public class EventListeners {
public static final OnTelegramCommand COMMANDS_LISTENER = new OnTelegramCommand();
// public static final OnActivityRecord ACTIVITY_RECORDER = new OnActivityRecord();
public static final OnUserSlashAction USER_SLASH_ACTION = new OnUserSlashAction();
public static final OnUpdateTimestampOffsetLock UPDATE_TIMESTAMP_OFFSET_LOCK = new OnUpdateTimestampOffsetLock();
public static final OnInlineQueries INLINE_QUERY = new OnInlineQueries();
public static final OnCallMe CALL_ME = new OnCallMe();
public static final OnEventHackHandle EVENT_HACK_HANDLE = new OnEventHackHandle();
// static final OnKuohuanhuanNeedSleep KUOHUANHUAN_NEED_SLEEP = new OnKuohuanhuanNeedSleep();
public static final OnUserRandoms USER_RANDOMS = new OnUserRandoms();
public static final OnCallMsgSend CALL_MSG_SEND = new OnCallMsgSend();
public static final OnMedicationNotifyApply MEDICATION_NOTIFY_APPLY = new OnMedicationNotifyApply();
public static final OnRandomlyTriggered RANDOMLY_TRIGGERED = new OnRandomlyTriggered();
public static final OnUniMeowTrigger UNI_MEOW_TRIGGER = new OnUniMeowTrigger();
public static final OnQuestionMarkReply QUESTION_MARK_REPLY = new OnQuestionMarkReply();
public static void registerAllListeners () {
EventListenerManager.addListener(
// ACTIVITY_RECORDER,
UPDATE_TIMESTAMP_OFFSET_LOCK,
/* write functional event behind here */
// KUOHUANHUAN_NEED_SLEEP,
COMMANDS_LISTENER,
UNI_MEOW_TRIGGER,
RANDOMLY_TRIGGERED,
USER_RANDOMS,
QUESTION_MARK_REPLY,
USER_SLASH_ACTION,
INLINE_QUERY,
CALL_ME,
CALL_MSG_SEND,
MEDICATION_NOTIFY_APPLY,
EVENT_HACK_HANDLE
);
}
}

View File

@ -0,0 +1,28 @@
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 extends 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 super.onMessage(update);
}
}

View File

@ -0,0 +1,181 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.CommonFormat;
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape;
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString;
import com.pengrad.telegrambot.model.Chat;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.ForwardMessage;
import com.pengrad.telegrambot.request.GetChat;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendSticker;
import com.pengrad.telegrambot.response.SendResponse;
import javax.annotation.Nonnull;
/**
* 通过 bot 呼叫主人的事件监听管理类
* @since 0.4.2.1
*/
public class OnCallMe extends EventListener {
/**
* 主人的 telegram user id同时被用于 chat id<br>
* 跟随 {@link cc.sukazyo.cono.morny.MornyConfig#trustedMaster} 的值
* @since 0.4.2.1
*/
private static final long ME = MornyCoeur.config().trustedMaster;
/**
* 监听私聊 bot 的消息进行呼叫关键字匹配
* 如果成功将会执行呼叫函数并向呼叫者回显{@link TelegramStickers#ID_WAITING "已呼叫"贴纸}
*
* @param update 事件基础参数消息事件所属的 tgapi:update 对象
* @return 事件基础返回值是否已完成处理事件<br>
* 如果匹配到呼叫则返回{@code true}反之返回{@code false}
*/
@Override
public boolean onMessage (@Nonnull Update update) {
if (update.message().text() == null)
return false;
if (update.message().chat().type() != Chat.Type.Private)
return false;
switch (update.message().text().toLowerCase()) {
case "steam", "sbeam", "sdeam" ->
requestSteamJoin(update);
case "hana paresu", "花宫", "内群" ->
requestHanaParesuJoin(update);
case "dinner", "lunch", "breakfast", "meal", "eating", "安妮今天吃什么" ->
requestLastDinner(update);
default -> {
if (update.message().text().startsWith("cc::")) {
requestCustomCall(update);
break;
}
return false;
}
}
MornyCoeur.extra().exec(new SendSticker(
update.message().chat().id(),
TelegramStickers.ID_SENT
).replyToMessageId(update.message().messageId())
);
return true;
}
/**
* 执行 steam library 呼叫<br>
* 将会向 {@link #ME} 发送
*
* @param event 执行呼叫的tg事件
*/
private static void requestSteamJoin (Update event) {
MornyCoeur.extra().exec(new SendMessage(
ME, String.format(
"""
request <b>STEAM LIBRARY</b>
from %s""",
TGToString.as(event.message().from()).fullnameRefHtml()
)
).parseMode(ParseMode.HTML));
}
/**
* 执行花宫呼叫<br>
* 将会向 {@link #ME} 发送
*
* @param event 执行呼叫的tg事件
*/
private static void requestHanaParesuJoin (Update event) {
MornyCoeur.extra().exec(new SendMessage(
ME, String.format(
"""
request <b>Hana Paresu</b>
from %s""",
TGToString.as(event.message().from()).fullnameRefHtml()
)
).parseMode(ParseMode.HTML));
}
/**
* 对访问最近一次的饭局的请求进行回复<br>
*
* @param event 执行呼叫的tg事件
*/
private static void requestLastDinner (Update event) {
boolean isAllowed = false;
Message lastDinnerData = null;
if (MornyCoeur.trustedInstance().isTrustedForDinnerRead(event.message().from().id())) {
lastDinnerData = MornyCoeur.extra().exec(new GetChat(MornyCoeur.config().dinnerChatId)).chat().pinnedMessage();
SendResponse sendResp = MornyCoeur.extra().exec(new ForwardMessage(
event.message().from().id(),
lastDinnerData.forwardFromChat().id(),
lastDinnerData.forwardFromMessageId()
));
MornyCoeur.extra().exec(new SendMessage(
event.message().from().id(),
String.format("<i>on</i> <code>%s [UTC+8]</code>\n- <code>%s</code> <i>before</i>",
MsgEscape.escapeHtml(
CommonFormat.formatDate((long)lastDinnerData.forwardDate()*1000, 8)
), MsgEscape.escapeHtml(
CommonFormat.formatDuration(System.currentTimeMillis()-(long)lastDinnerData.forwardDate()*1000)
)
)
).replyToMessageId(sendResp.message().messageId()).parseMode(ParseMode.HTML));
isAllowed = true;
} else {
MornyCoeur.extra().exec(new SendSticker(
event.message().from().id(),
TelegramStickers.ID_403
).replyToMessageId(event.message().messageId()));
}
MornyCoeur.extra().exec(new SendMessage(
ME, String.format(
"""
request <b>Last Annie Dinner</b>
from %s
%s""",
TGToString.as(event.message().from()).fullnameRefHtml(),
isAllowed ? "Allowed and returned " + String.format(
"https://t.me/c/%d/%d", Math.abs(lastDinnerData.forwardFromChat().id()+1000000000000L), lastDinnerData.forwardFromMessageId()
) : "Forbidden by perm check."
)
).parseMode(ParseMode.HTML));
}
/**
* 执行自定义呼叫<br>
* 将会向 {@link #ME} 发送一个 request 数据消息和转发的原始请求消息<br>
* <br>
* <u>known issue</u><ul>
* <li>无法处理与转发带有媒体的消息</li>
* </ul>
* <br>
* 现在你可以通过这个 bot 来呼叫主人sukazyo任何事情了
* <s>但是直接私聊sukazyo不好吗</s>
*
* @param event 执行呼叫的tg事件
* @since 0.4.2.2
*/
private static void requestCustomCall (Update event) {
MornyCoeur.extra().exec(new SendMessage(
ME, String.format(
"""
request <u>[???]</u>
from %s""",
TGToString.as(event.message().from()).fullnameRefHtml()
)
).parseMode(ParseMode.HTML));
MornyCoeur.extra().exec(new ForwardMessage(
ME,
event.message().chat().id(),
event.message().messageId()
));
}
}

View File

@ -0,0 +1,222 @@
package cc.sukazyo.cono.morny.bot.event;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.pengrad.telegrambot.model.Chat;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.MessageEntity;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.GetChat;
import com.pengrad.telegrambot.request.SendMessage;
import com.pengrad.telegrambot.request.SendSticker;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.data.TelegramStickers;
import com.pengrad.telegrambot.response.GetChatResponse;
import com.pengrad.telegrambot.response.SendResponse;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
public class OnCallMsgSend extends EventListener {
private static final Pattern REGEX_MSG_SENDREQ_DATA_HEAD = Pattern.compile("^\\*msg(-?\\d+)(\\*\\S+)?(?:\\n([\\s\\S]+))?$");
private record MessageToSend (
@Nullable String message,
@Nullable MessageEntity[] entities,
@Nullable ParseMode parseMode,
long targetId
) { }
@Override
public boolean onMessage(@Nonnull Update update) {
// 执行体检查
if (update.message().chat().type() != Chat.Type.Private) return false;
if (update.message().text() == null) return false;
if (!update.message().text().startsWith("*msg")) return false;
// 权限检查
if (!MornyCoeur.trustedInstance().isTrusted(update.message().from().id())) {
MornyCoeur.extra().exec(new SendSticker(
update.message().chat().id(),
TelegramStickers.ID_403
).replyToMessageId(update.message().messageId()));
return true;
}
Message msgsendReqRaw; // 用户书写的发送请求原文
MessageToSend msgsendReqBody; // 解析后的发送请求实例
// *msgsend 发送标识
// 处理发送要求
if (update.message().text().equals("*msgsend")) {
// 发送体处理
if (update.message().replyToMessage() == null) return answer404(update);
msgsendReqBody = parseRequest(update.message().replyToMessage());
if (msgsendReqBody == null || msgsendReqBody.message == null) return answer404(update);
// 执行发送任务
SendResponse sendResponse = MornyCoeur.getAccount().execute(parseMessageToSend(msgsendReqBody));
if (!sendResponse.isOk()) { // 发送失败
MornyCoeur.extra().exec(new SendMessage(
update.message().chat().id(),
String.format("""
<b><u>%d</u> FAILED</b>
<code>%s</code>""",
sendResponse.errorCode(),
sendResponse.description()
)
).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML));
} else { // 发送成功信号
MornyCoeur.extra().exec(new SendSticker(
update.message().chat().id(),
TelegramStickers.ID_SENT
).replyToMessageId(update.message().messageId()));
}
return true;
// 发送完成/失败 - 事件结束
}
// *msg 检查标识
if (update.message().text().equals("*msg")) { // 处理对曾经的原文的检查
if (update.message().replyToMessage() == null) {
return answer404(update);
}
msgsendReqRaw = update.message().replyToMessage();
} else if (update.message().text().startsWith("*msg")) { // 对接受到的原文进行检查
msgsendReqRaw = update.message();
} else {
return answer404(update); // 未定义的动作
}
// 对发送请求的用户原文进行解析
msgsendReqBody = parseRequest(msgsendReqRaw);
if (msgsendReqBody == null) {
return answer404(update);
}
// 输出发送目标信息
GetChatResponse targetChatReq = MornyCoeur.getAccount().execute(new GetChat(msgsendReqBody.targetId()));
if (!targetChatReq.isOk()) {
MornyCoeur.extra().exec(new SendMessage(
update.message().chat().id(),
String.format("""
<b><u>%d</u> FAILED</b>
<code>%s</code>""",
targetChatReq.errorCode(),
targetChatReq.description()
)
).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML));
} else {
MornyCoeur.extra().exec(new SendMessage(
update.message().chat().id(),
targetChatReq.chat().type() == Chat.Type.Private ? (
String.format("""
<i><u>%d</u>@%s</i>
🔒 <b>%s</b> %s""",
msgsendReqBody.targetId(),
escapeHtml(targetChatReq.chat().type().name()),
escapeHtml(targetChatReq.chat().firstName()+(targetChatReq.chat().lastName()==null?"":" "+targetChatReq.chat().lastName())),
targetChatReq.chat().username()==null?
String.format("<a href='tg://user?id=%d'>@@</a>", targetChatReq.chat().id()):
(escapeHtml("@"+targetChatReq.chat().username()))
)
) : (
String.format("""
<i><u>%d</u>@%s</i>:::
%s <b>%s</b>%s""",
msgsendReqBody.targetId(),
escapeHtml(targetChatReq.chat().type().name()),
switch (targetChatReq.chat().type()) {
case group -> "💭";
case channel -> "📢";
case supergroup -> "💬";
default -> "⭕️";
},
escapeHtml(targetChatReq.chat().title()),
targetChatReq.chat().username() != null?String.format(
" @%s", escapeHtml(targetChatReq.chat().username())
):""
)
)
).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML));
}
// 发送文本测试
if (msgsendReqBody.message == null) return true;
final SendResponse testSendResp = MornyCoeur.getAccount().execute(
parseMessageToSend(msgsendReqBody, update.message().chat().id()).replyToMessageId(update.message().messageId())
);
if (!testSendResp.isOk()) {
MornyCoeur.extra().exec(new SendMessage(
update.message().chat().id(),
String.format("""
<b><u>%d</u> FAILED</b>
<code>%s</code>""",
testSendResp.errorCode(),
testSendResp.description()
)
).replyToMessageId(update.message().messageId()).parseMode(ParseMode.HTML));
}
return true;
}
@Nullable
private static MessageToSend parseRequest (@Nonnull Message requestBody) {
final Matcher matcher = REGEX_MSG_SENDREQ_DATA_HEAD.matcher(requestBody.text());
if (matcher.matches()) {
long targetId = Long.parseLong(matcher.group(1));
ParseMode parseMode = matcher.group(2) == null ? null : switch (matcher.group(2)) {
case "*markdown", "*md", "*m↓" -> ParseMode.MarkdownV2;
case "*md1" -> ParseMode.Markdown;
case "*html" -> ParseMode.HTML;
default -> null;
};
final int offset = "*msg".length()+matcher.group(1).length()+(matcher.group(2)==null?0:matcher.group(2).length())+1;
final ArrayList<MessageEntity> entities = new ArrayList<>();
if (requestBody.entities() != null) for (MessageEntity entity : requestBody.entities()) {
final MessageEntity parsed = new MessageEntity(entity.type(), entity.offset() - offset, entity.length());
if (entity.url() != null) parsed.url(entity.url());
if (entity.user() != null) parsed.user(entity.user());
if (entity.language() != null) parsed.language(entity.language());
entities.add(parsed);
}
return new MessageToSend(matcher.group(3), entities.toArray(MessageEntity[]::new), parseMode, targetId);
}
return null;
}
@Nonnull
private static SendMessage parseMessageToSend (@Nonnull MessageToSend body) {
return parseMessageToSend(body, body.targetId);
}
@Nonnull
private static SendMessage parseMessageToSend (@Nonnull MessageToSend body, long targetId) {
SendMessage sendingBody = new SendMessage(targetId, body.message);
if (body.entities != null) sendingBody.entities(body.entities);
if (body.parseMode != null) sendingBody.parseMode(body.parseMode);
return sendingBody;
}
private static boolean answer404 (@Nonnull Update update) {
MornyCoeur.extra().exec(new SendSticker(
update.message().chat().id(),
TelegramStickers.ID_404
).replyToMessageId(update.message().messageId()));
return true;
}
}

View File

@ -0,0 +1,145 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape;
import com.google.gson.GsonBuilder;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendMessage;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;
import static cc.sukazyo.cono.morny.Log.logger;
/**
* 事件劫持与序列化工具.
* @since 0.4.2.0
*/
public class OnEventHackHandle extends EventListener {
/** 事件劫持请求列表 */
private static final Map<String, Hacker> hackers = new HashMap<>();
/**
* 触发事件劫持的限定条件.
* @since 0.4.2.0
*/
public enum HackType {
/** 只有相同用户发起的事件才会被触发 */
USER,
/** 只有相同群组内发生的事件才会触发 */
GROUP,
/** 任何事件都可以触发 */
ANY
}
public record Hacker(long fromChatId, long fromMessageId) {
@Override public String toString() {
return fromChatId + "/" + fromMessageId;
}
}
/**
* @since 0.4.2.0
*/
public static void registerHack(long fromMessageId, long fromUserId, long fromChatId, @Nonnull HackType type) {
String rec = null;
switch (type) {
case USER -> rec = String.format("((%d))", fromUserId);
case GROUP -> rec = String.format("{{%d}}", fromChatId);
case ANY -> rec = "[[]]";
}
hackers.put(rec, new Hacker(fromChatId, fromMessageId));
logger.debug("add hacker track " + rec);
}
private boolean onEventHacked (Update update, long chatId, long fromUser) {
logger.debug(String.format("got event signed {{%d}}((%d))", chatId, fromUser));
Hacker x;
x = hackers.remove(String.format("((%d))", fromUser));
if (x == null) x = hackers.remove(String.format("{{%d}}", chatId));
if (x == null) x = hackers.remove("[[]]");
if (x == null) return false;
logger.debug("hacked event by " + x);
MornyCoeur.extra().exec(new SendMessage(x.fromChatId, String.format(
"<code>%s</code>",
MsgEscape.escapeHtml(new GsonBuilder().setPrettyPrinting().create().toJson(update))
)).parseMode(ParseMode.HTML).replyToMessageId((int)x.fromMessageId));
return true;
}
@Override
public boolean onMessage (@Nonnull Update update) {
return onEventHacked(update, update.message().chat().id(), update.message().from().id());
}
@Override
public boolean onEditedMessage (@Nonnull Update update) {
return onEventHacked(update, update.editedMessage().chat().id(), update.editedMessage().from().id());
}
@Override
public boolean onChannelPost (@Nonnull Update update) {
return onEventHacked(update, update.channelPost().chat().id(), update.channelPost().chat().id());
}
@Override
public boolean onEditedChannelPost (@Nonnull Update update) {
return onEventHacked(update, update.editedChannelPost().chat().id(), update.editedChannelPost().chat().id());
}
@Override
public boolean onInlineQuery (@Nonnull Update update) {
return onEventHacked(update, 0, update.inlineQuery().from().id());
}
@Override
public boolean onChosenInlineResult (@Nonnull Update update) {
return onEventHacked(update, 0, update.chosenInlineResult().from().id());
}
@Override
public boolean onCallbackQuery (@Nonnull Update update) {
return onEventHacked(update, 0, update.callbackQuery().from().id());
}
@Override
public boolean onShippingQuery (@Nonnull Update update) {
return onEventHacked(update, 0, update.shippingQuery().from().id());
}
@Override
public boolean onPreCheckoutQuery (@Nonnull Update update) {
return onEventHacked(update, 0, update.preCheckoutQuery().from().id());
}
@Override
public boolean onPoll (@Nonnull Update update) {
return onEventHacked(update, 0, 0);
}
@Override
public boolean onPollAnswer (@Nonnull Update update) {
return onEventHacked(update, 0, update.pollAnswer().user().id());
}
@Override
public boolean onMyChatMemberUpdated (@Nonnull Update update) {
return onEventHacked(update, update.myChatMember().chat().id(), update.myChatMember().from().id());
}
@Override
public boolean onChatMemberUpdated (@Nonnull Update update) {
return onEventHacked(update, update.chatMember().chat().id(), update.chatMember().from().id());
}
@Override
public boolean onChatJoinRequest (@Nonnull Update update) {
return onEventHacked(update, update.chatJoinRequest().chat().id(), update.chatJoinRequest().from().id());
}
}

View File

@ -0,0 +1,47 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.InlineQueryResult;
import com.pengrad.telegrambot.request.AnswerInlineQuery;
import javax.annotation.Nonnull;
import java.util.List;
/**
* telegram inlineQuery 功能的处理类
* 也是一个 InlineQueryManager还没做
*
* @since 0.4.1.3
*/
public class OnInlineQueries extends EventListener {
/**
* @since 0.4.1.3
*/
@Override
public boolean onInlineQuery (@Nonnull Update update) {
List<InlineQueryUnit<?>> results = MornyCoeur.queryManager().query(update);
int cacheTime = Integer.MAX_VALUE;
boolean isPersonal = InlineQueryUnit.DEFAULT_INLINE_PERSONAL_RESP;
InlineQueryResult<?>[] inlineQueryResults = new InlineQueryResult<?>[results.size()];
for (int i = 0; i < results.size(); i++) {
inlineQueryResults[i] = results.get(i).result;
if (cacheTime > results.get(i).cacheTime()) cacheTime = results.get(i).cacheTime();
if (results.get(i).isPersonal()) isPersonal = true;
}
if (results.size() == 0) return false;
MornyCoeur.extra().exec(new AnswerInlineQuery(
update.inlineQuery().id(), inlineQueryResults
).cacheTime(cacheTime).isPersonal(isPersonal));
return true;
}
}

View File

@ -0,0 +1,39 @@
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 extends 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

@ -0,0 +1,29 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.daemon.MornyDaemons;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull;
public class OnMedicationNotifyApply extends EventListener {
@Override
public boolean onEditedChannelPost (@Nonnull Update update) {
return editedMessageProcess(update.editedChannelPost());
}
@Override
public boolean onEditedMessage (@Nonnull Update update) {
return editedMessageProcess(update.editedMessage());
}
private boolean editedMessageProcess (Message edited) {
if (edited.chat().id() != MornyCoeur.config().medicationNotifyToChat) return false;
MornyDaemons.medicationTimerInstance.refreshNotificationWrite(edited);
return true;
}
}

View File

@ -0,0 +1,38 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.SendMessage;
import javax.annotation.Nonnull;
import java.util.Set;
import static cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue;
public class OnQuestionMarkReply extends EventListener {
/**
* 一个 unicode 的问号字符列表. 不仅有半角全角问号也包含了变体问号和叹号结合的问好以及 uni-emoji 问号
* @since 1.0.0-RC3.2
*/
public static final Set<Character> QUESTION_MARKS = Set.of('?', '', '¿', '⁈', '⁇', '‽', '❔', '❓');
@Override
public boolean onMessage (@Nonnull Update update) {
if (update.message().text() == null) return false;
if (!probabilityTrue(8)) return false;
for (char c : update.message().text().toCharArray()) {
if (!QUESTION_MARKS.contains(c)) return false;
}
MornyCoeur.extra().exec(new SendMessage(
update.message().chat().id(), update.message().text()
).replyToMessageId(update.message().messageId()));
return true;
}
}

View File

@ -0,0 +1,26 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.bot.api.EventListener;
public class OnRandomlyTriggered extends 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

@ -0,0 +1,30 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull;
import static cc.sukazyo.cono.morny.Log.logger;
public class OnTelegramCommand extends EventListener {
@Override
public boolean onMessage (@Nonnull Update event) {
if (event.message().text() == null || !event.message().text().startsWith("/") || event.message().text().startsWith("/ ")) {
logger.debug("not command");
return false; // 检测到非(命令格式)文本忽略掉命令处理
}
final InputCommand command = new InputCommand(event.message().text().substring(1));
if (!command.getCommand().matches("^\\w+$")) { logger.debug("not command");return false; }
logger.debug("is command");
if (command.getTarget() != null && !MornyCoeur.getUsername().equals(command.getTarget())) {
return true; // 检测到命令并非针对 morny退出整个事件处理链
}
return MornyCoeur.commandManager().execute(command, event); // 转交命令管理器执行命令
}
}

View File

@ -0,0 +1,36 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.bot.command.ISimpleCommand;
import cc.sukazyo.cono.morny.util.tgapi.InputCommand;
import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
public class OnUniMeowTrigger extends EventListener {
private static final Map<String, ISimpleCommand> triggers = new HashMap<>();
public static void register (ISimpleCommand... list) {
for (ISimpleCommand cmd : list)
triggers.put(cmd.getName(), cmd);
}
@Override
public boolean onMessage (@Nonnull Update event) {
if (event.message().text() == null) return false;
AtomicBoolean ok = new AtomicBoolean(false);
triggers.forEach((name, command) -> {
name = "/" + name;
if (name.equals(event.message().text())) {
command.execute(new InputCommand(name), event);
ok.set(true);
}
});
return ok.get();
}
}

View File

@ -0,0 +1,56 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull;
/**
* 阻止 {@link cc.sukazyo.cono.morny.MornyConfig#eventOutdatedTimestamp 指定时间} 之前的事件处理.
* <p>
* 只支持以下事件
* <ul>
* <li>{@link EventListener#onMessage(Update) 收到消息}</li>
* <li>{@link EventListener#onEditedMessage(Update) 消息被更新}</li>
* <li>{@link EventListener#onChannelPost(Update) 收到频道消息}</li>
* <li>{@link EventListener#onEditedChannelPost(Update) 频道消息被更新}</li>
* </ul>
* @see #isOutdated 时间判断
*/
public class OnUpdateTimestampOffsetLock extends EventListener {
/**
* 检查传入时间是否在要求时间之前"过期".
* @param timestamp 传入时间秒级
* @return 如果传入时间在要求时间<u>之前</u>返回true反之false
* @since 0.4.2.7
*/
public boolean isOutdated(long timestamp) {
return timestamp < MornyCoeur.config().eventOutdatedTimestamp/1000;
}
@Override
public boolean onMessage (@Nonnull Update update) {
return isOutdated(update.message().date());
}
/** @since 0.4.2.6 */
@Override
public boolean onEditedMessage (@Nonnull Update update) {
return isOutdated(update.editedMessage().editDate());
}
/** @since 0.4.2.6 */
@Override
public boolean onChannelPost (@Nonnull Update update) {
return isOutdated(update.channelPost().date());
}
/** @since 0.4.2.6 */
@Override
public boolean onEditedChannelPost (@Nonnull Update update) {
return isOutdated(update.editedChannelPost().editDate());
}
}

View File

@ -0,0 +1,42 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.SendMessage;
import javax.annotation.Nonnull;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cc.sukazyo.cono.morny.util.CommonRandom.iif;
public class OnUserRandoms extends EventListener {
private static final Pattern USER_OR_QUERY = Pattern.compile("(.+)(?:还是|or)(.+)");
private static final Pattern USER_IF_QUERY = Pattern.compile("(.+)[吗?|]+$");
@Override
public boolean onMessage (@Nonnull Update update) {
if (update.message().text() == null) return false;
if (!update.message().text().startsWith("/")) return false;
final String query = update.message().text().substring(1);
String result = null;
Matcher matcher;
if ((matcher = USER_OR_QUERY.matcher(query)).find()) {
result = iif() ? matcher.group(1) : matcher.group(2);
} else if ((matcher = USER_IF_QUERY.matcher(query)).matches()) {
result = (iif()?"":"") + matcher.group(1);
}
if (result == null) return false;
MornyCoeur.extra().exec(new SendMessage(
update.message().chat().id(), result
).replyToMessageId(update.message().messageId()));
return true;
}
}

View File

@ -0,0 +1,84 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.util.UniversalCommand;
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.SendMessage;
import javax.annotation.Nonnull;
import static cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
public class OnUserSlashAction extends EventListener {
@Override
public boolean onMessage (@Nonnull Update event) {
final String text = event.message().text();
if (text == null) return false;
if (text.startsWith("/"))
{
/// Due to @Lapis_Apple, we stopped slash action function at .DP7 groups.
/// It may be enabled after some updates when the function will not be conflicted to other bots.
// if (event.message().chat().id() == ) return false;
//{ if (event.message().chat().title() != null && event.message().chat().title().contains(".DP7")) {
// logger.info(String.format("""
// Chat slash action ignored due to the following keyword.
// - %s
// - ".DP7\"""",
// TGToString.as(event.message().chat()).toStringFullNameId()
// ));
// return false;
// }
final String[] action = UniversalCommand.format(text);
action[0] = action[0].substring(1);
if (action[0].matches("^\\w+(@\\w+)?$")) {
return false; // 忽略掉 Telegram 命令格式的输入
} else if (action[0].contains("/")) {
return false; // 忽略掉疑似目录格式的输入
}
final boolean isHardParse = "".equals(action[0]);
/* 忽略空数据 */ if (isHardParse && action.length < 2) { return false; }
final String verb = isHardParse ? action[1] : action[0];
final boolean hasObject = action.length != (isHardParse?2:1);
final String object =
hasObject ?
stringsConnecting(action, " ", isHardParse?2:1, action.length-1) :
"";
final Message origin = event.message();
final Message target = (event.message().replyToMessage() == null ? (
origin
): (
event.message().replyToMessage()
));
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
String.format(
"%s %s%s %s %s!",
TGToString.as(origin).getSenderFirstNameRefHtml(),
escapeHtml(verb), escapeHtml((hasObject?"":"")),
origin==target ?
"<a href='tg://user?id="+TGToString.as(target).getSenderId()+"'>自己</a>" :
TGToString.as(target).getSenderFirstNameRefHtml(),
escapeHtml(hasObject ? object+" " : "")
)
).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId()));
return true;
}
return false;
}
}

View File

@ -0,0 +1,15 @@
package cc.sukazyo.cono.morny.bot.query;
import javax.annotation.Nullable;
import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit;
import com.pengrad.telegrambot.model.Update;
import java.util.List;
public interface ITelegramQuery {
@Nullable
List<InlineQueryUnit<?>> query (Update event);
}

View File

@ -0,0 +1,31 @@
package cc.sukazyo.cono.morny.bot.query;
import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit;
import com.pengrad.telegrambot.model.Update;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
public class MornyQueries {
private final List<ITelegramQuery> queryInstances = new ArrayList<>();
public MornyQueries () {
queryInstances.add(new RawText());
queryInstances.add(new MyInformation());
queryInstances.add(new ShareToolTwitter());
queryInstances.add(new ShareToolBilibili());
}
@Nonnull
public List<InlineQueryUnit<?>> query (@Nonnull Update event) {
final List<InlineQueryUnit<?>> results = new ArrayList<>();
for (ITelegramQuery instance : queryInstances) {
final List<InlineQueryUnit<?>> r = instance.query(event);
if (r!=null) results.addAll(r);
}
return results;
}
}

View File

@ -0,0 +1,35 @@
package cc.sukazyo.cono.morny.bot.query;
import javax.annotation.Nullable;
import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.InlineQueryResultArticle;
import com.pengrad.telegrambot.model.request.InputTextMessageContent;
import com.pengrad.telegrambot.model.request.ParseMode;
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation;
import java.util.Collections;
import java.util.List;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds;
public class MyInformation implements ITelegramQuery {
public static final String ID_PREFIX = "[morny/info/me]";
public static final String TITLE = "My Account Information";
@Override
@Nullable
public List<InlineQueryUnit<?>> query(Update event) {
if (!(event.inlineQuery().query() == null || "".equals(event.inlineQuery().query()))) return null;
return Collections.singletonList(new InlineQueryUnit<>(new InlineQueryResultArticle(
inlineIds(ID_PREFIX), TITLE,
new InputTextMessageContent(
TelegramUserInformation.informationOutputHTML(event.inlineQuery().from())
).parseMode(ParseMode.HTML)
)).isPersonal(true).cacheTime(10));
}
}

View File

@ -0,0 +1,31 @@
package cc.sukazyo.cono.morny.bot.query;
import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit;
import javax.annotation.Nullable;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.InlineQueryResultArticle;
import com.pengrad.telegrambot.model.request.InputTextMessageContent;
import java.util.Collections;
import java.util.List;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds;
public class RawText implements ITelegramQuery {
public static final String ID_PREFIX = "[morny/r/text]";
public static final String TITLE = "Raw Text";
@Override
@Nullable
public List<InlineQueryUnit<?>> query (Update event) {
if (event.inlineQuery().query() == null || "".equals(event.inlineQuery().query())) return null;
return Collections.singletonList(new InlineQueryUnit<>(new InlineQueryResultArticle(
inlineIds(ID_PREFIX, event.inlineQuery().query()), TITLE,
new InputTextMessageContent(event.inlineQuery().query())
)));
}
}

View File

@ -0,0 +1,80 @@
package cc.sukazyo.cono.morny.bot.query;
import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit;
import cc.sukazyo.cono.morny.util.BiliTool;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.InlineQueryResultArticle;
import com.pengrad.telegrambot.model.request.InputTextMessageContent;
import com.pengrad.telegrambot.model.request.ParseMode;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cc.sukazyo.cono.morny.Log.logger;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds;
public class ShareToolBilibili implements ITelegramQuery {
public static final String TITLE_BILI_AV = "[bilibili] Share video / av";
public static final String TITLE_BILI_BV = "[bilibili] Share video / BV";
public static final String ID_PREFIX_BILI_AV = "[morny/share/bili/av]";
public static final String ID_PREFIX_BILI_BV = "[morny/share/bili/bv]";
public static final Pattern REGEX_BILI_VIDEO = Pattern.compile("^(?:(?:https?://)?(?:www\\.)?bilibili\\.com(?:/s)?/video/((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))/?(\\?(?:p=(\\d+))?.*)?|(?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$");
private static final String SHARE_FORMAT_HTML = "<a href='%s'>%s</a>";
@Nullable
@Override
public List<InlineQueryUnit<?>> query (Update event) {
if (event.inlineQuery().query() == null) return null;
final Matcher regex = REGEX_BILI_VIDEO.matcher(event.inlineQuery().query());
if (regex.matches()) {
logger.debug(String.format(
"====== Share Tool Bilibili Catch ok\n1: %s\n2: %s\n3: %s\n4: %s\n5: %s\n6: %s\n7: %s",
regex.group(1), regex.group(2), regex.group(3), regex.group(4),
regex.group(5), regex.group(6), regex.group(7)
));
// get video id from input, also get video part id
String av = regex.group(2)==null ? regex.group(6)==null ? null : regex.group(6) : regex.group(2);
String bv = regex.group(3)==null ? regex.group(7)==null ? null : regex.group(7) : regex.group(3);
logger.trace(String.format("catch id av[%s] bv[%s]", av, bv));
final int part = regex.group(5)==null ? -1 : Integer.parseInt(regex.group(5));
logger.trace(String.format("catch part [%s]", part));
if (av == null) {
assert bv != null;
av = String.valueOf(BiliTool.toAv(bv));
logger.trace(String.format("converted bv[%s] to av[%s]", bv, av));
} else {
bv = BiliTool.toBv(Long.parseLong(av));
logger.trace(String.format("converted av[%s] to bv[%s]", av, bv));
}
// build standard share links
final String linkPartParam = part==-1 ? "" : "?p="+part;
final String linkAv = "https://www.bilibili.com/video/av"+av + linkPartParam;
final String linkBv = "https://www.bilibili.com/video/BV"+bv + linkPartParam;
final String idAv = "av"+av;
final String idBv = "BV"+bv;
logger.trace("built all data.");
// build share message element
List<InlineQueryUnit<?>> result = new ArrayList<>();
result.add(new InlineQueryUnit<>(new InlineQueryResultArticle(
inlineIds(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av,
new InputTextMessageContent(String.format(SHARE_FORMAT_HTML, linkAv, idAv)).parseMode(ParseMode.HTML)
)));
result.add(new InlineQueryUnit<>(new InlineQueryResultArticle(
inlineIds(ID_PREFIX_BILI_BV+bv), TITLE_BILI_BV+bv,
new InputTextMessageContent(String.format(SHARE_FORMAT_HTML, linkBv, idBv)).parseMode(ParseMode.HTML)
)));
return result;
}
return null;
}
}

View File

@ -0,0 +1,50 @@
package cc.sukazyo.cono.morny.bot.query;
import cc.sukazyo.cono.morny.bot.api.InlineQueryUnit;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.InlineQueryResultArticle;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds;
public class ShareToolTwitter implements ITelegramQuery {
public static final String TITLE_VX = "[tweet] Share as VxTwitter";
public static final String TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)";
public static final String ID_PREFIX_VX = "[morny/share/twitter/vxtwi]";
public static final String ID_PREFIX_VX_COMBINED = "[morny/share/twitter/vxtwi_combine]";
public static final Pattern REGEX_TWEET_LINK = Pattern.compile(
"^(?:https?://)?((?:(?:c\\.)?vx|fx|www\\.)?twitter\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(\\?[\\w&=-]+)?$");
@Nullable
@Override
public List<InlineQueryUnit<?>> query (@Nonnull Update event) {
if (event.inlineQuery().query() == null) return null;
final Matcher regex = REGEX_TWEET_LINK.matcher(event.inlineQuery().query());
if (regex.matches()) {
List<InlineQueryUnit<?>> result = new ArrayList<>();
result.add(new InlineQueryUnit<>(new InlineQueryResultArticle(
inlineIds(ID_PREFIX_VX+event.inlineQuery().query()), TITLE_VX,
String.format("https://vxtwitter.com/%s", regex.group(2))
)));
result.add(new InlineQueryUnit<>(new InlineQueryResultArticle(
inlineIds(ID_PREFIX_VX_COMBINED+event.inlineQuery().query()), TITLE_VX_COMBINED,
String.format("https://c.vxtwitter.com/%s", regex.group(2))
)));
return result;
}
return null;
}
}

View File

@ -0,0 +1,98 @@
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

@ -0,0 +1,34 @@
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

@ -0,0 +1,159 @@
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 Exception 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

@ -0,0 +1,174 @@
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

@ -0,0 +1,46 @@
package cc.sukazyo.cono.morny.data;
import cc.sukazyo.cono.morny.util.CommonConvert;
import cc.sukazyo.cono.morny.util.CommonEncrypt;
import com.pengrad.telegrambot.model.User;
/**
* Morny jrrp 运算类.
*
* @see #getJrrpFromTelegramUser(User,long)
* @see #calcJrrpXmomi(long,long)
* @since 0.4.2.9
*/
public class MornyJrrp {
/**
* 通过 telegram 用户和时间戳作为参数获取 jrrp.
*
* @see #calcJrrpXmomi 当前版本的实现算法 {@code Xmomi}
* @since 0.4.2.9
* @param user telegram 用户
* @param timestamp 时间戳
* @return 通过当前版本的算法计算出的用户 jrrp 取值为 {@code [0.00, 100.00]}
*/
public static double getJrrpFromTelegramUser (User user, long timestamp) {
return calcJrrpXmomi(user.id(), timestamp / (1000 * 60 * 60 * 24)) * 100.0;
}
/**
* {@code Xmomi} 版本的 jrrp 算法.
* <p>
* 算法规则为将用户id与日期戳链接为 <u><code>uid@daystamp</code></u> 这样的字符串
* 然后通过 MD5 计算出字符串的哈希值取哈希值前4个字节将其作为16进制数值表示法转换为取值为 {@code [0x0000, 0xffff]} 的数值
* 得到的数值除以区间最大值 {@code 0xffff} 即可得到一个分布在 {@code [0.0, 1.0]} 之间的分布值
* 这个分布值乘以 {@code 100.0}即为计算得到的 jrrp 数值
*
* @since 0.4.2.9
* @param userId telegram 用户 uid
* @param dayStamp unix 时间戳转换为日期单位后的数值. 数值应该在转换前转换时区
* @return 算法得到的 jrrp 取值为 {@code [0.00. 100.00]}
*/
public static double calcJrrpXmomi (long userId, long dayStamp) {
return (double)Long.parseLong(CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(userId + "@" + dayStamp)).substring(0, 4), 16) / (double)0xffff;
}
}

View File

@ -0,0 +1,47 @@
package cc.sukazyo.cono.morny.data;
import java.io.IOException;
import com.google.gson.Gson;
import cc.sukazyo.cono.morny.util.OkHttpPublic.MediaTypes;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class NbnhhshQuery {
public static class Word {
public String name;
public String[] trans;
public String[] inputting;
}
public static class GuessResult {
public Word[] words;
}
public record GuessReq (String text) {}
public static final String API_URL = "https://lab.magiconch.com/api/nbnhhsh/";
public static final String API_GUESS_METHOD = "guess/";
private static final OkHttpClient httpClient = new OkHttpClient();
public static GuessResult sendGuess (String text) throws IOException {
final String reqJsonText = new Gson().toJson(new GuessReq(text));
Request request = new Request.Builder()
.url(API_URL + API_GUESS_METHOD)
.post(RequestBody.create(reqJsonText, MediaTypes.JSON))
.build();
try (Response response = httpClient.newCall(request).execute()) {
final ResponseBody body = response.body();
if (body == null) throw new IOException("Null body.");
final String x = "{ \"words\": " + body.string() + " }";
return new Gson().fromJson(x, GuessResult.class);
}
}
}

View File

@ -0,0 +1,81 @@
package cc.sukazyo.cono.morny.data;
import cc.sukazyo.cono.morny.MornyAssets;
import cc.sukazyo.cono.morny.daemon.MornyReport;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import static cc.sukazyo.cono.morny.Log.exceptionLog;
import static cc.sukazyo.cono.morny.Log.logger;
/**
* The images of morny will use.
*
* @since 1.0.0-RC4
*/
public class TelegramImages {
/**
* Image that stored in the {@link MornyAssets#pack}.
* <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

@ -0,0 +1,87 @@
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,89 @@
package cc.sukazyo.cono.morny.data.ip186;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import javax.annotation.Nonnull;
import java.io.IOException;
/**
* 通过 {@value #SITE_URL} 进行 {@link #queryIp ip}/{@link #queryWhois whois} 数据查询的工具类
*
* @since 0.4.2.10
*/
public class IP186QueryHandler {
/**
* 请求所使用的 HTTP API 站点链接
* @since 0.4.2.10
*/
public static final String SITE_URL = "https://ip.186526.xyz/";
/**
* 进行 {@link #queryIp ip 查询}时所使用的 API 参数.<br>
* 目的使 API 直接返回原始数据
*/
private static final String QUERY_IP_PARAM = "type=json&format=true";
/**
* 进行 {@link #queryWhois whois 查询}时所使用的 API 参数.<br>
* 目的使 API 直接返回原始数据
*/
private static final String QUERY_WHOIS_PARAM = "type=plain";
/** 请求时使用的 OkHttp 请求工具实例 */
private static final OkHttpClient httpClient = new OkHttpClient();
/**
* 通过 {@value #SITE_URL} 获取 ip 信息.
* @see #QUERY_IP_PARAM 发送请求时所使用的 API 参数
* @param ip 需要进行查询的 ip
* @return 查询结果data 根据 {@value #SITE_URL} 的规则以 json 序列化
* @throws IOException 任何请求或解析错误
*/
@Nonnull
public static IP186QueryResponse queryIp (String ip) throws IOException {
final String requestUrl = SITE_URL + ip;
return commonQuery(requestUrl, QUERY_IP_PARAM);
}
/**
* 通过 {@value #SITE_URL} 获取域名信息.
* @see #QUERY_WHOIS_PARAM 发送请求时所使用的 API 参数
* @param domain 需要进行查询的域名
* @return 查询结果data 根据 {@value #SITE_URL} 的规则以 plain 序列化
* @throws IOException 任何请求或解析错误
*/
@Nonnull
public static IP186QueryResponse queryWhois (String domain) throws IOException {
final String requestUrl = SITE_URL + "whois/" + domain;
return commonQuery(requestUrl, QUERY_WHOIS_PARAM);
}
/**
* {@link #queryWhois(String)} 的结果进行裁剪.
* <br>
* 将会删除返回内容中 {@code >>> XXX <<<} 行以后的注释串
* 以达到只保留重要信息的目的
*
* @see #queryWhois(String)
*/
@Nonnull
public static IP186QueryResponse queryWhoisPretty (String domain) throws IOException {
final IP186QueryResponse raw = queryWhois(domain);
return new IP186QueryResponse(raw.url(), raw.body().substring(0, raw.body().indexOf("<<<")+3));
}
@Nonnull
private static IP186QueryResponse commonQuery (String requestUrl, String queryIpParam) throws IOException {
Request request = new Request.Builder().url(requestUrl + "?" + queryIpParam).build();
try (Response response = httpClient.newCall(request).execute()) {
final ResponseBody body = response.body();
if (body == null) throw new IOException("Null body.");
return new IP186QueryResponse(requestUrl, body.string());
}
}
}

View File

@ -0,0 +1,11 @@
package cc.sukazyo.cono.morny.data.ip186;
/**
* {@link IP186QueryHandler} 的请求结果数据的通用封装类.
*
* @since 0.4.2.10
* @param url 请求数据的<u>人类可读的</u>来源链接<b>并非api链接</b>
* @param body API 传回的数据内容
*/
public record IP186QueryResponse(String url, String body) {
}

View File

@ -0,0 +1,98 @@
package cc.sukazyo.cono.morny.util;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;
public class BiliTool {
private static final long V_CONV_XOR = 177451812L;
private static final long V_CONV_ADD = 8728348608L;
private static final char[] BV_TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF".toCharArray();
private static final int TABLE_INT = BV_TABLE.length;
private static final Map<Character, Integer> BV_TABLE_REVERSED = new HashMap<>();
static { for (int i = 0; i < BV_TABLE.length; i++) BV_TABLE_REVERSED.put(BV_TABLE[i], i); }
private static final char[] BV_TEMPLATE = "1 4 1 7 ".toCharArray();
private static final int[] BV_TEMPLATE_FILTER = new int[]{9, 8, 1, 6, 2, 4};
public static class IllegalFormatException extends RuntimeException {
private IllegalFormatException (String bv, String reason) {
super("`%s` is not a valid 10 digits base58 BV id: %s".formatted(bv, reason));
}
private IllegalFormatException (String bv, int length) {
this(bv, "length is %d.".formatted(length));
}
private IllegalFormatException (String bv, char c, int location) {
this(bv, "char `%s` is not in base58 char table (in position %d)".formatted(c, location));
}
}
/**
* Convert a <a href="https://www.bilibili.com/">Bilibili</a> AV video id format to BV id format.
* <p>
* the AV id is a number; the BV id is a special base58 number, it shows as String in programming.<br>
* eg:<br>
* while the link <i>{@code https://www.bilibili.com/video/BV17x411w7KC/}</i>
* shows the same with <i>{@code https://www.bilibili.com/video/av170001/}</i>,
* the AV id is <u>{@code 170001}</u>, the BV id is <u>{@code 17x411w7KC}</u>
* <p>
* for now , the BV id has 10 digits.
* the method <b>available while the <u>av-id < 2^27</u></b>, while it theoretically available when the av-id < 2^30.
* <p>
* this method allows input only 10 digits base58 BV id, if the input is not formatted by this method, it will throw
* an Exception.
*
* @see <a href="https://www.zhihu.com/question/381784377/answer/1099438784">mcfx的回复: 如何看待 2020 3 23 日哔哩哔哩将稿件的av 变更为BV </a>
*
* @param bv the BV id, a string in (a special) base58 number format, <b>without "BV" prefix</b>.
* @return the AV id corresponding to this bv id in <a href="https://www.bilibili.com/">Bilibili</a>, formatted as a number.
* @throws IllegalFormatException if the input BV id is not the 10 digits base58 String.
*/
@Nonnegative
public static long toAv (@Nonnull String bv) throws IllegalFormatException {
long av = 0;
if (bv.length() != 10)
throw new IllegalFormatException(bv, bv.length());
for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) {
final Integer tableToken = BV_TABLE_REVERSED.get(bv.charAt(BV_TEMPLATE_FILTER[i]));
if (tableToken == null)
throw new IllegalFormatException(bv, bv.charAt(BV_TEMPLATE_FILTER[i]), BV_TEMPLATE_FILTER[i]);
av += tableToken * Math.pow(TABLE_INT,i);
}
return (av-V_CONV_ADD)^V_CONV_XOR;
}
/**
* Convert a <a href="https://www.bilibili.com/">Bilibili</a> BV video id format to AV id format.
* <p>
* the AV id is a number; the BV id is a special base58 number, it shows as String in programming.<br>
* eg:<br>
* while the link <i>{@code https://www.bilibili.com/video/BV17x411w7KC/}</i>
* shows the same with <i>{@code https://www.bilibili.com/video/av170001/}</i>,
* the AV id is <u>{@code 170001}</u>, the BV id is <u>{@code 17x411w7KC}</u>
* <p>
* for now , the BV id has 10 digits.
* the method <b>available while the <u>av-id < 2^27</u></b>, while it theoretically available when the av-id < 2^30.
*
* @see <a href="https://www.zhihu.com/question/381784377/answer/1099438784">mcfx的回复: 如何看待 2020 3 23 日哔哩哔哩将稿件的av 变更为BV </a>
*
* @param av the (base10) AV id.
* @return the AV id corresponding to this bv id in <a href="https://www.bilibili.com/">Bilibili</a>,
* as a (special) base 58 number format <b>without "BV" prefix</b>.
*/
@Nonnull
public static String toBv (@Nonnegative long av) {
av = (av^V_CONV_XOR)+V_CONV_ADD;
final char[] bv = BV_TEMPLATE.clone();
for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) {
bv[BV_TEMPLATE_FILTER[i]] = BV_TABLE[(int)(Math.floor(av/(Math.pow(TABLE_INT, i)))%TABLE_INT)];
}
return String.copyValueOf(bv);
}
}

View File

@ -0,0 +1,61 @@
package cc.sukazyo.cono.morny.util;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
/**
* 进行简单类型转换等工作的类.
*/
public class CommonConvert {
/**
* 将字节数组转换成 hex 字符串.
* @param b 字节数组
* @return String 格式的字节数组的 hex 每个字节当中没有分隔符
* @see #byteToHex(byte)
*/
@Nonnull
public static String byteArrayToHex(@Nonnull byte[] b){
StringBuilder sb = new StringBuilder();
for (byte value : b) {
sb.append(byteToHex(value));
}
return sb.toString();
}
/**
* 将一个字节转换成十六进制 hex 字符串.
* @param b 字节值
* @return String 格式的字节的 hex 小写
*/
@Nonnull
public static String byteToHex(byte b) {
final String hex = Integer.toHexString(b & 0xff);
return hex.length()<2?"0"+hex:hex;
}
/**
* 将一个字符串数组按照一定规则连接.
* <p>
* 连接的方式类似于"数据1+分隔符+数据2+分隔符+...+数据n-1+分隔符+数据n"
*
* @param array 需要进行连接的字符串数组数组中每一个元素会是一个数据
* @param connector 在每两个传入数据中插入的分隔符
* @param startIndex 从传入的数据组中的哪一个位置开始第一个元素的位置是 {@code 0}
* @param stopIndex 从传入的数据组中的哪一个位置停止元素位置计算方式同上
* @return 连接好的字符串
*/
@Nonnull
public static String stringsConnecting (
@Nonnull String[] array, @Nonnull String connector, @Nonnegative int startIndex, @Nonnegative int stopIndex
) {
final StringBuilder builder = new StringBuilder();
for (int i = startIndex; i < stopIndex; i++) {
builder.append(array[i]);
builder.append(connector);
}
builder.append(array[stopIndex]);
return builder.toString();
}
}

View File

@ -0,0 +1,144 @@
package cc.sukazyo.cono.morny.util;
import javax.annotation.Nonnull;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* 用于数据加密或编解码的工具类.
* <p>
* 出于 java std Base64 {@link Base64.Encoder encode}/{@link Base64.Decoder decode} 十分好用在此不再进行包装
*/
public class CommonEncrypt {
/**
* 在使用加密算法处理字符串时默认会使用的字符串编码.
* <p>
* Morny 使用 UTF-8 编码因为这是一般而言加解密工具的默认行为
*/
public static final Charset ENCRYPT_STANDARD_CHARSET = StandardCharsets.UTF_8;
@Nonnull
private static byte[] hashAsJavaMessageDigest(String algorithm, @Nonnull byte[] data) {
try {
return MessageDigest.getInstance(algorithm).digest(data);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
/**
* 取得数据的 md5 散列值.
*
* @param data byte 数组形式的数据体
* @return 二进制(byte数组)格式的数据的 md5 散列值
*/
@Nonnull
public static byte[] hashMd5 (@Nonnull byte[] data) {
return hashAsJavaMessageDigest("md5", data);
}
/**
* 取得一个字符串的 md5 散列值.
* <p>
* 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析
*
* @param originString 要进行散列的字符串
* @return 二进制(byte数组)格式的 md5 散列值
*/
@Nonnull
public static byte[] hashMd5 (String originString) {
return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET));
}
/**
* 取得数据的 sha1 散列值.
*
* @param data byte 数组形式的数据体
* @return 二进制(byte数组)格式的数据的 sha1 散列值
*/
@Nonnull
public static byte[] hashSha1 (@Nonnull byte[] data) {
return hashAsJavaMessageDigest("sha1", data);
}
/**
* 取得一个字符串的 sha1 散列值.
* <p>
* 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析
*
* @param originString 要进行散列的字符串
* @return 二进制(byte数组)格式的 sha1 散列值
*/
@Nonnull
public static byte[] hashSha1 (String originString) {
return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET));
}
/**
* 取得数据的 sha256 散列值.
*
* @param data byte 数组形式的数据体
* @return 二进制(byte数组)格式的数据的 sha256 散列值
*/
@Nonnull
public static byte[] hashSha256 (@Nonnull byte[] data) {
return hashAsJavaMessageDigest("sha256", data);
}
/**
* 取得一个字符串的 sha256 散列值.
* <p>
* 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析
*
* @param originString 要进行散列的字符串
* @return 二进制(byte数组)格式的 sha256 散列值
*/
@Nonnull
public static byte[] hashSha256 (String originString) {
return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET));
}
/**
* 取得数据的 sha512 散列值.
*
* @param data byte 数组形式的数据体
* @return 二进制(byte数组)格式的数据的 sha512 散列值
*/
@Nonnull
public static byte[] hashSha512 (@Nonnull byte[] data) {
return hashAsJavaMessageDigest("md5", data);
}
/**
* 取得一个字符串的 sha512 散列值.
* <p>
* 输入的字符串将会以 {@link #ENCRYPT_STANDARD_CHARSET 默认的 UTF-8} 编码进行解析
*
* @param originString 要进行散列的字符串
* @return 二进制(byte数组)格式的 sha512 散列值
*/
@Nonnull
public static byte[] hashSha512 (String originString) {
return hashMd5(originString.getBytes(ENCRYPT_STANDARD_CHARSET));
}
@Nonnull
public static String base64FilenameLint (String inputName) {
if (inputName.endsWith(".b64")) {
return inputName.substring(0, inputName.length()-".b64".length());
} else if (inputName.endsWith(".b64.txt")) {
return inputName.substring(0, inputName.length()-".b64.txt".length());
} else if (inputName.endsWith(".base64")) {
return inputName.substring(0, inputName.length()-".base64".length());
} else if (inputName.endsWith(".base64.txt")) {
return inputName.substring(0, inputName.length()-".base64.txt".length());
} else {
return inputName;
}
}
}

View File

@ -0,0 +1,30 @@
package cc.sukazyo.cono.morny.util;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
public class CommonFormat {
public static final String DATE_TIME_PATTERN_FULL_MILLIS = "yyyy-MM-dd HH:mm:ss:SSS";
public static String formatDate (long timestamp, int utcOffset) {
return DateTimeFormatter.ofPattern(DATE_TIME_PATTERN_FULL_MILLIS).format(LocalDateTime.ofInstant(
Instant.ofEpochMilli(timestamp),
ZoneId.ofOffset("UTC", ZoneOffset.ofHours(utcOffset))
));
}
public static String formatDuration (long duration) {
StringBuilder sb = new StringBuilder();
if (duration > 1000 * 60 * 60 * 24) sb.append(duration / (1000*60*60*24)).append("d ");
if (duration > 1000 * 60 * 60) sb.append(duration / (1000*60*60) % 24).append("h ");
if (duration > 1000 * 60) sb.append(duration / (1000*60) % 60).append("min ");
if (duration > 1000) sb.append(duration / 1000 % 60).append("s ");
sb.append(duration % 1000).append("ms");
return sb.toString();
}
}

View File

@ -0,0 +1,44 @@
package cc.sukazyo.cono.morny.util;
import javax.annotation.Nonnegative;
import java.util.concurrent.ThreadLocalRandom;
public class CommonRandom {
/**
* 通过 {@link ThreadLocalRandom} 以指定的一定几率返回 true.
* @param probability 一个正整数决定在样本空间中有多大的可能性为 true
* @param base 一个正整数决定样本空间有多大
* @return {@code base} 分之 {@code probability} 的几率返回值为 {@link true}.
* 如果 {@code probability} 大于 {@code base}也就是为 true 的可能性大于 100%则会永远为 true
* @throws IllegalArgumentException
* 当参数 base 或是 probability 不为正整数时
* @since 1.0.0-RC3.2
*/
public static boolean probabilityTrue (@Nonnegative int probability, @Nonnegative int base) {
if (probability < 1) throw new IllegalArgumentException("the probability must be a positive value!");
if (base < 1) throw new IllegalArgumentException("the probability base must be a positive value!");
return probability > ThreadLocalRandom.current().nextInt(base);
}
/**
* 以一定几率返回 true.
* @return {@code probabilityIn} 分之 {@link 1} 的几率为 {@link true}.
* @see #probabilityTrue(int, int)
* @since 1.0.0-RC3.2
*/
public static boolean probabilityTrue (@Nonnegative int probabilityIn) {
return (probabilityTrue(1, probabilityIn));
}
/**
* 通过 {@link ThreadLocalRandom} 实现的随机 boolean 取值.
* @return 随机的 {@link true} {@link false}各占(近似)一半可能性.
* @see ThreadLocalRandom#nextBoolean()
* @since 1.0.0-RC3.2
*/
public static boolean iif () {
return ThreadLocalRandom.current().nextBoolean();
}
}

View File

@ -0,0 +1,28 @@
package cc.sukazyo.cono.morny.util;
import javax.annotation.Nonnull;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class FileUtils {
@Nonnull
public static String getMD5Three (@Nonnull String path) throws IOException, NoSuchAlgorithmException {
final BigInteger bi;
final byte[] buffer = new byte[8192];
int len;
final MessageDigest md = MessageDigest.getInstance("MD5");
final FileInputStream fis = new FileInputStream(path);
while ((len = fis.read(buffer)) != -1) {
md.update(buffer, 0, len);
}
fis.close();
final byte[] b = md.digest();
bi = new BigInteger(1, b);
return bi.toString(16);
}
}

View File

@ -0,0 +1,13 @@
package cc.sukazyo.cono.morny.util;
import okhttp3.MediaType;
public class OkHttpPublic {
public static class MediaTypes {
public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
}
}

View File

@ -0,0 +1,49 @@
package cc.sukazyo.cono.morny.util;
import javax.annotation.Nonnull;
import java.util.ArrayList;
public class UniversalCommand {
@Nonnull
public static String[] format (@Nonnull String com) {
final ArrayList<String> arr = new ArrayList<>();
final StringBuilder tmp = new StringBuilder();
final char[] coma = com.toCharArray();
for (int i = 0; i < coma.length; i++) {
if (coma[i] == ' ') {
if (!tmp.toString().equals("")) { arr.add(tmp.toString()); }
tmp.setLength(0);
} else if (coma[i] == '"') {
while (true) {
i++;
if (i >= coma.length) {
break;
} else if (coma[i] == '"') {
break;
} else if (coma[i] == '\\' && i+1 < coma.length && (coma[i+1] == '"' || coma[i+1] == '\\')) {
i++;
tmp.append(coma[i]);
} else {
tmp.append(coma[i]);
}
}
} else if (coma[i] == '\\' && i+1 < coma.length && (coma[i+1] == ' ' || coma[i+1] == '"' || coma[i+1] == '\\')) {
i++;
tmp.append(coma[i]);
} else {
tmp.append(coma[i]);
}
}
if (!tmp.toString().equals("")) { arr.add(tmp.toString()); }
tmp.setLength(0);
final String[] out = new String[arr.size()];
arr.toArray(out);
return out;
}
}

View File

@ -32,7 +32,7 @@ public class ExtraAction {
public <T extends BaseRequest<T, R>, R extends BaseResponse> R exec (T req, String errorMessage) {
final R resp = bot.execute(req);
if (!resp.isOk()) throw new EventRuntimeException.ActionFailed(
(errorMessage.isEmpty() ? String.valueOf(resp.errorCode()) : errorMessage),
(errorMessage.equals("") ? String.valueOf(resp.errorCode()) : errorMessage),
resp
);
return resp;

View File

@ -0,0 +1,66 @@
package cc.sukazyo.cono.morny.util.tgapi;
import cc.sukazyo.cono.morny.util.UniversalCommand;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Arrays;
public class InputCommand {
private final String target;
private final String command;
private final String[] args;
private InputCommand (@Nullable String target, @Nonnull String command, @Nonnull String[] args) {
this.target = target;
this.command = command;
this.args = args;
}
public InputCommand (@Nonnull String[] inputArray) {
this(parseInputArray(inputArray));
}
public InputCommand (@Nonnull String input) {
this(UniversalCommand.format(input));
}
public InputCommand (@Nonnull InputCommand source) {
this(source.target, source.command, source.args);
}
public static InputCommand parseInputArray (@Nonnull String[] inputArray) {
final String[] cx = inputArray[0].split("@", 2);
final String[] args = new String[inputArray.length-1];
System.arraycopy(inputArray, 1, args, 0, inputArray.length - 1);
return new InputCommand(cx.length == 1 ? null : cx[1], cx[0], args);
}
@Nullable
public String getTarget () {
return target;
}
@Nonnull
public String getCommand () {
return command;
}
@Nonnull
public String[] getArgs () {
return args;
}
public boolean hasArgs () {
return args.length != 0;
}
@Override
@Nonnull
public String toString() {
return String.format("{{%s}@{%s}#{%s}}", command, target, Arrays.toString(args));
}
}

View File

@ -0,0 +1,35 @@
package cc.sukazyo.cono.morny.util.tgapi.event;
import com.pengrad.telegrambot.response.BaseResponse;
public class EventRuntimeException extends RuntimeException {
public EventRuntimeException () {
super();
}
public EventRuntimeException (String message) {
super(message);
}
public static class ActionFailed extends EventRuntimeException {
private final BaseResponse response;
public ActionFailed (BaseResponse response) {
super();
this.response = response;
}
public ActionFailed (String message, BaseResponse response) {
super(message);
this.response = response;
}
public BaseResponse getResponse() {
return response;
}
}
}

View File

@ -0,0 +1,15 @@
package cc.sukazyo.cono.morny.util.tgapi.formatting;
import javax.annotation.Nonnull;
public class MsgEscape {
@Nonnull
public static String escapeHtml (@Nonnull String raw) {
raw = raw.replaceAll("&", "&amp;");
raw = raw.replaceAll("<", "&lt;");
raw = raw.replaceAll(">", "&gt;");
return raw;
}
}

View File

@ -0,0 +1,18 @@
package cc.sukazyo.cono.morny.util.tgapi.formatting;
import cc.sukazyo.cono.morny.util.CommonConvert;
import cc.sukazyo.cono.morny.util.CommonEncrypt;
import javax.annotation.Nonnull;
public class NamedUtils {
public static String inlineIds (@Nonnull String tag) {
return inlineIds(tag, "");
}
public static String inlineIds (@Nonnull String tag, @Nonnull String taggedData) {
return CommonConvert.byteArrayToHex(CommonEncrypt.hashMd5(tag+taggedData));
}
}

View File

@ -0,0 +1,21 @@
package cc.sukazyo.cono.morny.util.tgapi.formatting;
import com.pengrad.telegrambot.model.Chat;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.User;
public class TGToString {
public static TGToStringFromChat as (Chat chat) {
return new TGToStringFromChat(chat);
}
public static TGToStringFromUser as (User user) {
return new TGToStringFromUser(user);
}
public static TGToStringFromMessage as (Message message) {
return new TGToStringFromMessage(message);
}
}

View File

@ -0,0 +1,22 @@
package cc.sukazyo.cono.morny.util.tgapi.formatting;
import com.pengrad.telegrambot.model.Chat;
public class TGToStringFromChat {
private final Chat data;
public TGToStringFromChat(Chat chat) {
this.data = chat;
}
public String toStringFullNameId() {
if (data.title() == null) {
throw new IllegalArgumentException("Cannot format private chat to group Name+Id format.");
}
return (data.username() == null) ?
(String.format("%s [%d]", data.title(), data.id())) :
(String.format("%s {%s}[%d]", data.title(), data.username(), data.id()));
}
}

View File

@ -0,0 +1,27 @@
package cc.sukazyo.cono.morny.util.tgapi.formatting;
import com.pengrad.telegrambot.model.Message;
import javax.annotation.Nonnull;
public class TGToStringFromMessage extends TGToString {
@Nonnull
private final Message message;
public TGToStringFromMessage (@Nonnull Message message) { this.message = message; }
@Nonnull
public String getSenderFirstNameRefHtml () {
return message.senderChat()==null ? TGToString.as(message.from()).firstnameRefHtml() : String.format(
"<a href='tg://user?id=%d'>%s</a>",
message.senderChat().id(),
MsgEscape.escapeHtml(message.senderChat().title())
);
}
public long getSenderId () {
return message.senderChat()==null ? message.from().id() : message.senderChat().id();
}
}

View File

@ -0,0 +1,53 @@
package cc.sukazyo.cono.morny.util.tgapi.formatting;
import com.pengrad.telegrambot.model.User;
public class TGToStringFromUser {
private final User data;
public TGToStringFromUser (User user) {
this.data = user;
}
public String fullname () {
return data.firstName() + (data.lastName()==null ? "" : " "+data.lastName());
}
public String fullnameRefHtml () {
return String.format(
"<a href='tg://user?id=%d'>%s</a>",
data.id(),
MsgEscape.escapeHtml(fullname())
);
}
public String fullnameRefMarkdown () {
return String.format(
"[%s](tg://user?id=%d)",
fullname(),
data.id()
);
}
public String firstnameRefHtml () {
return String.format(
"<a href='tg://user?id=%d'>%s</a>",
data.id(),
MsgEscape.escapeHtml(data.firstName())
);
}
public String firstnameRefMarkdown () {
return String.format(
"[%s](tg://user?id=%d)",
data.firstName(),
data.id()
);
}
public String toStringLogTag () {
return (data.username()==null ? fullname()+" " : "@"+data.username()) + "[" + data.id() + "]";
}
}

View File

@ -0,0 +1,85 @@
package cc.sukazyo.cono.morny.util.tgapi.formatting;
import com.pengrad.telegrambot.model.User;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
public class TelegramUserInformation {
public static final String DC_QUERY_SOURCE_SITE = "https://t.me/";
public static final Pattern DC_QUERY_PROCESSOR_REGEX = Pattern.compile("(cdn[1-9]).tele(sco.pe|gram-cdn.org)");
private static final OkHttpClient httpClient = new OkHttpClient();
@Nullable
public static String getDataCenterFromUsername (String username) {
final Request request = new Request.Builder().url(DC_QUERY_SOURCE_SITE + username).build();
try (Response response = httpClient.newCall(request).execute()) {
final ResponseBody body = response.body();
if (body == null) return "empty upstream response";
final Matcher matcher = DC_QUERY_PROCESSOR_REGEX.matcher(body.string());
if (matcher.find()) {
return matcher.group(1);
}
} catch (IOException e) {
return e.getMessage();
}
return null;
}
public static String informationOutputHTML (User user) {
final StringBuilder userInformation = new StringBuilder();
userInformation.append(String.format(
"""
userid :
- <code>%d</code>""",
user.id()
));
if (user.username() == null) {
userInformation.append("\nusername : <u>null</u>\ndatacenter : <u>null</u>");
} else {
userInformation.append(String.format(
"""
username :
- <code>%s</code>""",
escapeHtml(user.username())
));
// 依赖 username datacenter 查询
final String dataCenter = getDataCenterFromUsername(user.username());
if (dataCenter == null) { userInformation.append("\ndatacenter : <u>null</u>"); }
else { userInformation.append(String.format("\ndatacenter : <code>%s</code>", escapeHtml(dataCenter))); }
}
userInformation.append(String.format(
"""
display name :
- <code>%s</code>%s""",
escapeHtml(user.firstName()),
user.lastName()==null ? "" : String.format("\n- <code>%s</code>", escapeHtml(user.lastName()))
));
if (user.languageCode() != null) {
userInformation.append(String.format(
"""
language-code :
- <code>%s</code>""",
escapeHtml(user.languageCode())
));
}
return userInformation.toString();
}
}

View File

@ -1,29 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,9 +0,0 @@
package cc.sukazyo.cono.morny
import cc.sukazyo.restools.ResourcesPackage
object MornyAssets {
val pack: ResourcesPackage = ResourcesPackage(MornyAssets.getClass, "assets_morny")
}

View File

@ -1,149 +0,0 @@
package cc.sukazyo.cono.morny
import cc.sukazyo.cono.morny.bot.command.MornyCommands
import cc.sukazyo.cono.morny.daemon.MornyDaemons
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
import cc.sukazyo.cono.morny.MornyCoeur.THREAD_MORNY_EXIT
import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener
import cc.sukazyo.cono.morny.bot.event.MornyEventListeners
import cc.sukazyo.cono.morny.util.tgapi.ExtraAction
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

@ -1,49 +0,0 @@
package cc.sukazyo.cono.morny
import cc.sukazyo.cono.morny.internal.BuildConfigField
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
import cc.sukazyo.cono.morny.daemon.MornyReport
import cc.sukazyo.cono.morny.util.FileUtils
import java.io.IOException
import java.net.URISyntaxException
import java.security.NoSuchAlgorithmException
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
//noinspection ScalaWeakerAccess
@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

@ -1,16 +0,0 @@
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

@ -1,156 +0,0 @@
package cc.sukazyo.cono.morny
import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.MornyConfig.CheckFailure
import cc.sukazyo.cono.morny.util.CommonFormat
import java.time.ZoneOffset
import scala.collection.mutable.ArrayBuffer
import scala.language.postfixOps
object ServerMain {
private val THREAD_MORNY_INIT: String = "morny-init"
val systemStartupTime: Long = System.currentTimeMillis()
def main (args: Array[String]): Unit = {
val config = new MornyConfig.Prototype()
var mode_echoVersion = false
var mode_echoHello = false
var showHello = true
config.eventOutdatedTimestamp = systemStartupTime
val unknownArgs = ArrayBuffer[String]()
var i = 0
while (i < args.length) {
args(i) match {
case "-d" | "--dbg" | "--debug" => Log.debug(true)
case "--no-hello" | "-hf" | "--quiet" | "-q" => showHello = false
case "--only-hello" | "-ho" | "-o" | "-hi" => mode_echoHello = true
case "--version" | "-v" => mode_echoVersion = true
case "--outdated-block" | "-ob" => config.eventIgnoreOutdated = true
case "--api" | "-a" => i+=1 ; config.telegramBotApiServer = args(i)
case "--api-files" | "files-api" | "-af" => i+=1; config.telegramBotApiServer4File = args(i)
case "--token" | "-t" => i+=1 ; config.telegramBotKey = args(i)
case "--username" | "-u" => i+=1 ; config.telegramBotUsername = args(i)
case "--master" | "-mm" => i+=1 ; config.trustedMaster = args(i)toLong
case "--trusted-chat" | "-trs" => i+=1 ; config.trustedChat = args(i)toLong
case "--report-to" => i+=1; config.reportToChat = args(i)toLong
case "--trusted-reader-dinner" | "-trsd" => i+=1 ; config.dinnerTrustedReaders add (args(i)toLong)
case "--dinner-chat" | "-chd" => i+=1 ; config.dinnerChatId = args(i)toLong
case "--medication-notify-chat" | "-medc" => i+=1 ; config.medicationNotifyToChat = args(i)toLong
case "--medication-notify-timezone" | "-medtz" =>
i+=1
config.medicationTimerUseTimezone = ZoneOffset.ofHours(args(i)toInt)
case "--medication-notify-times" | "-medt" =>
i+=1
for (u <- args(i) split ",") {
config.medicationNotifyAt add (u toInt)
}
case "--auto-cmd-list" | "-ca" => config.commandLoginRefresh = true
case "--auto-cmd-remove" | "-cr" => config.commandLogoutClear = true
case "--auto-cmd" | "-cmd" | "-c" =>
config.commandLoginRefresh = true
config.commandLogoutClear = true
case _ => unknownArgs append args(i)
}
i+=1
}
/// Setup launch params from ENVIRONMENT
var propToken: String = null
var propTokenKey: String = null
for (iKey <- MornyConfig.PROP_TOKEN_KEY) {
if ((System getenv iKey) != null) {
propToken = System getenv iKey
propTokenKey = iKey
}
}
///
/// Output startup message
/// process startup params - like startup mode
///
if (showHello) logger info MornyAbout.MORNY_PREVIEW_IMAGE_ASCII
if (mode_echoHello) return;
if (unknownArgs.nonEmpty) logger warn
s"""Can't understand arg to some meaning
| ${unknownArgs mkString "\n "}"""
.stripMargin
if (Log debug)
logger warn
"""Debug log output enabled.
| It may lower your performance, make sure that you are not in production environment."""
.stripMargin
if (mode_echoVersion) {
logger info
s"""Morny Cono Version
|- version :
| Morny ${MornySystem.CODENAME toUpperCase}
| ${MornySystem.VERSION_BASE}${if (MornySystem.isUseDelta) "-δ"+MornySystem.VERSION_DELTA else ""}
|- md5hash :
| ${MornySystem.getJarMD5}
|- gitstat :
|${ if (MornySystem.isGitBuild) {
s""" on commit ${if (MornySystem.isCleanBuild) "- clean-build" else "<δ/non-clean-build>"}
| ${BuildConfig.COMMIT}"""
.stripMargin
} else " <non-git-build>"}
|- buildtd :
| ${BuildConfig.CODE_TIMESTAMP}
| ${CommonFormat.formatDate(BuildConfig.CODE_TIMESTAMP, 0)} [UTC]"""
.stripMargin
return
}
logger info
s"""ServerMain.java Loaded >>>
|- version ${MornySystem.VERSION_FULL}
|- Morny ${MornySystem.CODENAME toUpperCase}
|- <${MornySystem.getJarMD5}> [${BuildConfig.CODE_TIMESTAMP}]""".stripMargin
///
/// Check Coeur arguments
/// finally start Coeur Program
///
if (propToken != null) {
config.telegramBotKey = propToken
logger info s"Parameter <token> set by EnvVar $$$propTokenKey"
}
Thread.currentThread setName THREAD_MORNY_INIT
try
MornyCoeur.init(using config build)
catch {
case _: CheckFailure.NullTelegramBotKey =>
logger.info("Parameter required has no value:\n --token.")
case e: CheckFailure =>
logger.error("Unknown failure occurred while starting ServerMain!:")
e.printStackTrace(System.out)
}
}
}

View File

@ -1,22 +0,0 @@
package cc.sukazyo.cono.morny.bot.api
import com.pengrad.telegrambot.model.Update
trait EventListener {
def onMessage (using Update): Boolean = false
def onEditedMessage (using Update): Boolean = false
def onChannelPost (using Update): Boolean = false
def onEditedChannelPost (using Update): Boolean = false
def onInlineQuery (using Update): Boolean = false
def onChosenInlineResult (using Update): Boolean = false
def onCallbackQuery (using Update): Boolean = false
def onShippingQuery (using Update): Boolean = false
def onPreCheckoutQuery (using Update): Boolean = false
def onPoll (using Update): Boolean = false
def onPollAnswer (using Update): Boolean = false
def onMyChatMemberUpdated (using Update): Boolean = false
def onChatMemberUpdated (using Update): Boolean = false
def onChatJoinRequest (using Update): Boolean = false
}

View File

@ -1,84 +0,0 @@
package cc.sukazyo.cono.morny.bot.api
import cc.sukazyo.cono.morny.Log
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
import cc.sukazyo.cono.morny.daemon.MornyReport
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException
import com.google.gson.GsonBuilder
import com.pengrad.telegrambot.model.Update
import scala.collection.mutable
import scala.language.postfixOps
object EventListenerManager {
private val listeners = mutable.Queue.empty[EventListener]
def register (listeners: EventListener*): Unit =
this.listeners ++= listeners
private class EventRunner (using event: Update) extends Thread {
this setName s"evt-${event.updateId()}-nn"
private def updateThreadName (t: String): Unit =
this setName s"evt-${event.updateId()}-$t"
override def run (): Unit = {
for (i <- listeners) {
object status:
var _status = 0
def isOk: Boolean = _status > 0
def check (u: Boolean): Unit = if u then _status = _status + 1
try {
updateThreadName("message")
if event.message ne null then status check i.onMessage
updateThreadName("edited-message")
if event.editedMessage ne null then status check i.onEditedMessage
updateThreadName("channel-post")
if event.channelPost ne null then status check i.onChannelPost
updateThreadName("edited-channel-post")
if event.editedChannelPost ne null then status check i.onEditedChannelPost
updateThreadName("inline-query")
if event.inlineQuery ne null then status check i.onInlineQuery
updateThreadName("chosen-inline-result")
if event.chosenInlineResult ne null then status check i.onChosenInlineResult
updateThreadName("callback-query")
if event.callbackQuery ne null then status check i.onCallbackQuery
updateThreadName("shipping-query")
if event.shippingQuery ne null then status check i.onShippingQuery
updateThreadName("pre-checkout-query")
if event.preCheckoutQuery ne null then status check i.onPreCheckoutQuery
updateThreadName("poll")
if event.poll ne null then status check i.onPoll
updateThreadName("poll-answer")
if event.pollAnswer ne null then status check i.onPollAnswer
updateThreadName("my-chat-member")
if event.myChatMember ne null then status check i.onMyChatMemberUpdated
updateThreadName("chat-member")
if event.chatMember ne null then status check i.onChatMemberUpdated
updateThreadName("chat-join-request")
if event.chatJoinRequest ne null then status check i.onChatJoinRequest
} catch case e => {
val errorMessage = StringBuilder()
errorMessage ++= "Event throws unexpected exception:\n"
errorMessage ++= (exceptionLog(e) indent 4)
e match
case actionFailed: EventRuntimeException.ActionFailed =>
errorMessage ++= "\ntg-api action: response track: "
errorMessage ++= (GsonBuilder().setPrettyPrinting().create().toJson(
actionFailed.response
) indent 4) ++= "\n"
case _ =>
logger error errorMessage.toString
MornyReport.exception(e, "on event running")
}
if (status isOk) return
}
}
}
def publishUpdate (using Update): Unit = {
EventRunner().start()
}
}

View File

@ -1,17 +0,0 @@
package cc.sukazyo.cono.morny.bot.api
import com.pengrad.telegrambot.UpdatesListener
import com.pengrad.telegrambot.model.Update
import java.util
import scala.jdk.CollectionConverters.*
object TelegramUpdatesListener extends UpdatesListener {
override def process (updates: util.List[Update]): Int = {
for (update <- updates.asScala)
EventListenerManager.publishUpdate(using update)
UpdatesListener.CONFIRMED_UPDATES_ALL
}
}

View File

@ -1,58 +0,0 @@
package cc.sukazyo.cono.morny.bot.command
import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.data.TelegramStickers
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import com.pengrad.telegrambot.model.{Chat, Update}
import com.pengrad.telegrambot.request.{DeleteMessage, GetChatMember, SendSticker}
import scala.language.postfixOps
object DirectMsgClear extends ISimpleCommand {
override val name: String = "r"
override val aliases: Array[ICommandAlias] | Null = null
override def execute (using command: InputCommand, event: Update): Unit = {
logger debug "executing command /r"
if (event.message.replyToMessage == null) return;
logger trace "message is a reply"
if (event.message.replyToMessage.from.id != MornyCoeur.userid) return;
logger trace "message replied is from me"
if (System.currentTimeMillis/1000 - event.message.replyToMessage.date > 48*60*60) return;
logger trace "message is not outdated(48 hrs ago)"
val isTrusted = MornyCoeur.trusted isTrusted event.message.from.id
// todo:
// it does not work. due to the Telegram Bot API doesn't provide
// nested replyToMessage, so currently the trusted check by
// replyToMessage.replyToMessage will not work!
def _isReplyTrusted: Boolean =
if (event.message.replyToMessage.replyToMessage == null) false
else if (event.message.replyToMessage.replyToMessage.from.id == event.message.from.id) true
else false
if (isTrusted || _isReplyTrusted) {
MornyCoeur.extra exec DeleteMessage(
event.message.chat.id, event.message.replyToMessage.messageId
)
def _isPrivate: Boolean = event.message.chat.`type` == Chat.Type.Private
def _isPermission: Boolean =
(MornyCoeur.extra exec GetChatMember(event.message.chat.id, event.message.from.id))
.chatMember.canDeleteMessages
if (_isPrivate || _isPermission) {
MornyCoeur.extra exec DeleteMessage(event.message.chat.id, event.message.messageId)
}
} else MornyCoeur.extra exec SendSticker(
event.message.chat.id,
TelegramStickers ID_403
).replyToMessageId(event.message.messageId)
}
}

View File

@ -1,229 +0,0 @@
package cc.sukazyo.cono.morny.bot.command
import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.daemon.MornyReport
import cc.sukazyo.cono.morny.data.TelegramStickers
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import cc.sukazyo.cono.morny.util.CommonEncrypt
import cc.sukazyo.cono.morny.util.CommonEncrypt.*
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
import com.pengrad.telegrambot.model.{PhotoSize, Update}
import com.pengrad.telegrambot.model.request.ParseMode
import com.pengrad.telegrambot.request.{GetFile, SendDocument, SendMessage, SendSticker}
import java.io.IOException
import java.util.Base64
import scala.language.postfixOps
/** Provides Telegram Command __`/encrypt`__. */
object Encryptor extends ITelegramCommand {
override val name: String = "encrypt"
override val aliases: Array[ICommandAlias] | Null = null
override val paramRule: String = "[algorithm|(l)] [(uppercase)]"
override val description: String = "通过指定算法加密回复的内容 (目前只支持文本)"
override def execute (using command: InputCommand, event: Update): Unit = {
val args = command.args
// show a simple help page
if ((args isEmpty) || ((args(0) equals "l") && (args.length == 1)))
echoHelp(event.message.chat.id, event.message.messageId)
return
// for mod-params:
// mod-params is the args belongs to the encrypt algorithm.
// due to the algorithm is defined in the 1st (array(0)) arg,
// so the mod-params is which defined since the 2nd arg. also
// due to there's only one mod-param yet (it is uppercase),
// so the algorithm will be and must be in the 2nd arg.
/** inner function: is input `arg` means mod-param ''uppercase'' */
def _is_mod_u(arg: String): Boolean =
if (arg equalsIgnoreCase "uppercase") return true
if (arg equalsIgnoreCase "u") return true
if (arg equalsIgnoreCase "upper") return true
false
val mod_uppercase = if (args.length > 1) {
if (args.length < 3 && _is_mod_u(args(1))) true
else
MornyCoeur.extra exec SendSticker(
event.message.chat.id,
TelegramStickers ID_404
).replyToMessageId(event.message.messageId)
return
} else false
// BLOCK: get input
// for now, only support getting data from replied message, and
// this message CAN ONLY have texts or an universal file: if the
// universal files are not only one, only the first one can be get.
// - do NOT SUPPORT telegram inline image/video/autio yet
// - do NOT SUPPORT multi-file yet
// todo: support inline image/video/audio file and multi-files.
/** inner trait: the encryptable data abstract */
trait XEncryptable { /** standards data to [[Array]]`[`[[Byte]]`]` for processing */ val asByteArray: Array[Byte] }
/** inner class: the [[XEncryptable]] implementation of binary([[Array]]`[`[[Byte]]`]`) data (file or something) */
case class XFile (data: Array[Byte], name: String) extends XEncryptable:
val asByteArray: Array[Byte] = data
/** inner class: the [[XEncryptable]] implementation of [[String]] data */
case class XText (data: String) extends XEncryptable:
val asByteArray: Array[Byte] = data getBytes CommonEncrypt.ENCRYPT_STANDARD_CHARSET
val input: XEncryptable =
val _r = event.message.replyToMessage
if ((_r ne null) && (_r.document ne null)) {
try {XFile(
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}"
MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI")
return
} else if ((_r ne null) && (_r.photo ne null)) {
try {
var _photo_origin: PhotoSize = null
var _photo_size: Long = 0
for (size <- _r.photo)
val _size = (size.width longValue)*size.height
if (_photo_size < _size)
_photo_origin = size
_photo_size = _size
if (_photo_origin eq null) throw IllegalArgumentException("no photo from api.")
import cc.sukazyo.cono.morny.util.UseRandom.rand_id
XFile(
MornyCoeur.account getFileContent (MornyCoeur.extra exec GetFile(_photo_origin.fileId)).file,
s"photo$rand_id.png"
)
} catch
case e: IOException =>
logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}"
MornyReport.exception(e, "NetworkRequest error: TelegramFileAPI")
return
case e: IllegalArgumentException =>
logger warn s"FileProcess error: PhotoSize:\n\t${e.getMessage}"
MornyReport.exception(e, "FileProcess error: PhotoSize")
return
} else if ((_r ne null) && (_r.text ne null)) {
XText(_r.text)
} else {
MornyCoeur.extra exec SendMessage(
event.message.chat.id,
"<i><u>null</u></i>"
).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId)
return
}
// END BLOCK: get input
// BLOCK: encrypt
/** inner class: encrypt result implementation of text-like (can be described as [[String]]). */
trait EXTextLike { val text: String }
/** inner class: encrypt result implementation of a file */
case class EXFile (result: Array[Byte], resultName: String)
/** inner class: [[EXTextLike]] implementation of just normal text */
case class EXText (text: String) extends EXTextLike
/** inner class: [[EXTextLike]] implementation of a special type: hash value */
case class EXHash (text: String) extends EXTextLike
/** generate encrypt result by making normal encrypt: output type == input type */
def genResult_encrypt (source: XEncryptable, processor: Array[Byte]=>Array[Byte], filenameProcessor: String=>String): EXFile|EXText = {
source match
case x_file: XFile => EXFile(processor(x_file asByteArray), filenameProcessor(x_file.name))
case x: XText => EXText(String(processor(x asByteArray), CommonEncrypt.ENCRYPT_STANDARD_CHARSET))
}
/** generate encrypt result by making hash: output type == hash value */
def genResult_hash (source: XEncryptable, processor: Array[Byte]=>Array[Byte]): EXHash =
val hashed = processor(source asByteArray) toHex;
EXHash(if mod_uppercase then hashed toUpperCase else hashed)
val result: EXHash|EXFile|EXText = args(0) match
case "base64" | "b64" | "base64url" | "base64u" | "b64u" =>
val _tool_b64 =
if args(0) contains "u" then Base64.getUrlEncoder
else Base64.getEncoder
genResult_encrypt(
input,
_tool_b64.encode,
n => n+".b64.txt"
)
case "base64decode" | "base64d" | "b64d" | "base64url-decode" | "base64ud" | "b64ud" =>
val _tool_b64d =
if args(0) contains "u" then Base64.getUrlDecoder
else Base64.getDecoder
try { genResult_encrypt(
input,
_tool_b64d.decode,
CommonEncrypt.lint_base64FileName
) } catch case _: IllegalArgumentException =>
MornyCoeur.extra exec SendSticker(
event.message.chat.id,
TelegramStickers ID_404 // todo: is here better erro notify?
).replyToMessageId(event.message.messageId)
return
case "md5" => genResult_hash(input, MD5)
case "sha1" => genResult_hash(input, SHA1)
case "sha256" => genResult_hash(input, SHA256)
case "sha512" => genResult_hash(input, SHA512)
case _ =>
MornyCoeur.extra exec SendSticker(
event.message.chat.id,
TelegramStickers ID_404
).replyToMessageId(event.message.messageId)
return;
// END BLOCK: encrypt
// output
result match
case _file: EXFile =>
MornyCoeur.extra exec SendDocument(
event.message.chat.id,
_file.result
).fileName(_file.resultName).replyToMessageId(event.message.messageId)
case _text: EXTextLike =>
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
MornyCoeur.extra exec SendMessage(
event.message.chat.id,
s"<pre><code>${h(_text.text)}</code></pre>"
).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId)
}
/** echo help to a specific message in a specific chat.
*
* === the help message ===
* The first paragraph lists available encrypt algorithms and its alias,
* each line have one algorithm where the first name highlighted is the
* main name and following is aliases separated with `,`.
* with the separator `---`, the second paragraph lists available mods
* for algorithms, displays with the same rule of algorithms, with an extra
* italic text following describes its usage environment.
*
* when output to telegram just like:
* <blockquote>
* '''__base64__''', b64<br>
* '''__base64url__''', base64u, b64u<br>
* '''__base64decode__''', base64d, b64d<br>
* '''__base64url-decode__''', base64ud, b64ud<br>
* '''__sha1__'''<br>
* '''__sha256__'''<br>
* '''__sha512__'''<br>
* '''__md5__'''<br>
* ---<br>
* '''__uppercase__''', upper, u ''(sha1/sha256/sha512/md5 only)''
* </blockquote>
*/
private def echoHelp(chat: Long, replyTo: Int): Unit =
MornyCoeur.extra exec SendMessage(
chat,
s"""<b><u>base64</u></b>, b64
|<b><u>base64url</u></b>, base64u, b64u
|<b><u>base64decode</u></b>, base64d, b64d
|<b><u>base64url-decode</u></b>, base64ud, b64ud
|<b><u>sha1</u></b>
|<b><u>sha256</u></b>
|<b><u>sha512</u></b>
|<b><u>md5</u></b>
|---
|<b><i>uppercase</i></b>, upper, u <i>(sha1/sha256/sha512/md5 only)</i>"""
.stripMargin
).replyToMessageId(replyTo).parseMode(ParseMode HTML)
}

View File

@ -1,56 +0,0 @@
package cc.sukazyo.cono.morny.bot.command
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle
import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle.{registerHack, HackType}
import cc.sukazyo.cono.morny.data.TelegramStickers
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.request.SendSticker
import scala.language.postfixOps
object EventHack extends ITelegramCommand {
override val name: String = "event_hack"
override val aliases: Array[ICommandAlias] | Null = null
override val paramRule: String = "[(user|group|any)]"
override val description: String = "输出 bot 下一个获取到的事件序列化数据"
override def execute (using command: InputCommand, event: Update): Unit = {
val x_mode = if (command.args nonEmpty) command.args(0) else ""
def done_ok =
MornyCoeur.extra exec SendSticker(
event.message.chat.id,
TelegramStickers ID_WAITING
).replyToMessageId(event.message.messageId)
def done_forbiddenForAny =
MornyCoeur.extra exec SendSticker(
event.message.chat.id,
TelegramStickers ID_403
).replyToMessageId(event.message.messageId)
def doRegister (t: HackType): Unit =
registerHack(
event.message.messageId longValue,
event.message.from.id,
event.message.chat.id,
t
)
x_mode match
case "any" =>
if (MornyCoeur.trusted isTrusted event.message.from.id)
doRegister(HackType ANY)
done_ok
else done_forbiddenForAny
case "group" =>
doRegister(HackType GROUP)
done_ok
case _ =>
doRegister(HackType USER)
done_ok
}
}

View File

@ -1,67 +0,0 @@
package cc.sukazyo.cono.morny.bot.command
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.util.tgapi.{InputCommand, Standardize}
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.model.request.ParseMode
import com.pengrad.telegrambot.request.{GetChatMember, SendMessage}
import scala.language.postfixOps
object GetUsernameAndId extends ITelegramCommand {
override val name: String = "user"
override val aliases: Array[ICommandAlias] | Null = null
override val paramRule: String = "[userid]"
override val description: String = "获取指定或回复的用户相关信息"
override def execute (using command: InputCommand, event: Update): Unit = {
val args = command.args
if (args.length > 1)
MornyCoeur.extra exec SendMessage(
event.message.chat.id,
"[Unavailable] Too much arguments."
).replyToMessageId(event.message.messageId)
return
val userId: Long =
if (args nonEmpty) {
try args(0) toLong
catch case e: NumberFormatException =>
MornyCoeur.extra exec SendMessage(
event.message.chat.id,
s"[Unavailable] ${e.getMessage}"
).replyToMessageId(event.message.messageId)
return
} else if (event.message.replyToMessage eq null) event.message.from.id
else event.message.replyToMessage.from.id
val response = MornyCoeur.account execute GetChatMember(event.message.chat.id, userId)
if (response.chatMember eq null)
MornyCoeur.extra exec SendMessage(
event.message.chat.id,
"[Unavailable] user not found."
).replyToMessageId(event.message.messageId)
return
val user = response.chatMember.user
if (user.id == Standardize.CHANNEL_SPEAKER_MAGIC_ID)
MornyCoeur.extra exec SendMessage(
event.message.chat.id,
"<code>$__channel_identify</code>"
).replyToMessageId(event.message.messageId)
return;
MornyCoeur.extra exec SendMessage(
event.message.chat.id,
TelegramUserInformation getFormattedInformation user
).replyToMessageId(event.message.messageId()).parseMode(ParseMode HTML)
}
}

View File

@ -1,18 +0,0 @@
package cc.sukazyo.cono.morny.bot.command
trait ICommandAlias {
val name: String
val listed: Boolean
}
object ICommandAlias {
case class ListedAlias (name: String) extends ICommandAlias:
override val listed = true
case class HiddenAlias (name: String) extends ICommandAlias:
override val listed = false
}

View File

@ -1,77 +0,0 @@
package cc.sukazyo.cono.morny.bot.command
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.data.ip186.IP186QueryHandler
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.model.request.ParseMode
import com.pengrad.telegrambot.request.SendMessage
import scala.language.postfixOps
object IP186Query {
private enum Subs (val cmd: String):
case IP extends Subs("ip")
case WHOIS extends Subs("whois")
object IP extends ITelegramCommand:
override val name: String = "ip"
override val aliases: Array[ICommandAlias]|Null = null
override val paramRule: String = "[ip]"
override val description: String = "通过 https://ip.186526.xyz 查询 ip 资料"
override def execute (using command: InputCommand, event: Update): Unit = query
object Whois extends ITelegramCommand:
override val name: String = "whois"
override val aliases: Array[ICommandAlias]|Null = null
override val paramRule: String = "[domain]"
override val description: String = "通过 https://ip.186526.xyz 查询域名资料"
override def execute (using command: InputCommand, event: Update): Unit = query
private def query (using event: Update, command: InputCommand): Unit = {
val target: String|Null =
if (command.args isEmpty)
if event.message.replyToMessage eq null then null else event.message.replyToMessage.text
else if (command.args.length > 1)
MornyCoeur.extra exec SendMessage(
event.message.chat.id,
"[Unavailable] Too much arguments."
).replyToMessageId(event.message.messageId)
return
else command.args(0)
if (target eq null)
MornyCoeur.extra exec new SendMessage(
event.message.chat.id,
"[Unavailable] No ip defined."
).replyToMessageId(event.message.messageId)
return;
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
try {
val response = command.command match
case Subs.IP.cmd => IP186QueryHandler.query_ip(target)
case Subs.WHOIS.cmd => IP186QueryHandler.query_whoisPretty(target)
case _ => throw IllegalArgumentException(s"Unknown 186-IP query method ${command.command}")
MornyCoeur.extra exec SendMessage(
event.message.chat.id,
s"""${h(response.url)}
|<code>${h(response.body)}</code>"""
.stripMargin
).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId)
} catch case e: Exception =>
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())
}
}

Some files were not shown because too many files have changed in this diff Show More