Compare commits

..

No commits in common. "49bbc03ec06034854eb14adcb2a38b0c7e7b210c" and "3661cb126481ec8e05c8603a641314f3c5fd7299" have entirely different histories.

181 changed files with 4989 additions and 5996 deletions

View File

@ -1,17 +0,0 @@
# IDE
.idea/
.vscode/
.gradle/
.settings/
#build
/build/
/bin/
.metals/
.bloop/
.project
lcoal.properties
# debug dir
/run/

File diff suppressed because it is too large Load Diff

6
.gitignore vendored
View File

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

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "_book"]
path = _book
url = https://storage.sukazyo.cc/Eyre_S/morny-book.git

View File

@ -1,21 +0,0 @@
FROM eclipse-temurin:20-jdk as build
LABEL authors="A.C.Sukazyo Eyre"
COPY . /app/source/
WORKDIR /app
RUN cd ./source \
&& ./gradlew shadowJar -PdockerBuild \
&& cd .. \
&& cp ./source/build/libs/morny-coeur-docker-build.jar ./morny-coeur.jar
#&& rm -r ./source \
#&& rm -r /root/.gradle \
FROM eclipse-temurin:20-jre
COPY --from=build /app/morny-coeur.jar /app/morny-coeur.jar
WORKDIR /app
ENTRYPOINT ["java", "-jar", "morny-coeur.jar"]
CMD ["-q", "-v"]

View File

@ -4,22 +4,19 @@
[todo]: https://github.com/users/Eyre-S/projects/1
[artifact]: https://mvn.sukazyo.cc/#/releases/cc/sukazyo/morny-coeur
[scala]: https://www.scala-lang.org/
[spotbugs]: https://spotbugs.github.io/
[tg4j]: https://github.com/pengrad/java-telegram-bot-api
[okhttp]: https://square.github.io/okhttp/
[gson]: https://github.com/google/gson
[scalatest]: https://scalatest.org/
[spotbugs]: https://spotbugs.github.io/
[junit5]: https://junit.org/junit5/
<div align=center>
# ~~给所有喜欢morny的大家的~~ Morny Coeur 源代码
~~"and nobody cares."~~
~~"你们又有意见又不发issue这样子我很为难的啊"~~
![social preview card](morny-github-social-preview-card@0.75x.png)
一个 telegram 上的服侍 A.C.Sukazyo Eyre 和它的花宫成员的 bot 内核
一个 telegram 上的服侍 A.C.Sukazyo Eyre 和它的花宫成员的 bot 内核
[Task Listing][todo] | [~~BBS~~][issues] | [Published][artifact]
@ -35,9 +32,6 @@
[Java Telegram Bot API][tg4j]
[okhttp] | [Gson][gson]
[Scala][scala] | [SpotBugs Annotations][spotbugs] | [ScalaTest][scalatest]
[SpotBugs Annotations][spotbugs] | [JUnit 5][junit5]
</div>

1
_book Submodule

@ -0,0 +1 @@
Subproject commit 3072bcee8e498e87ecdd36958185ad423e80bcf3

View File

@ -1,72 +1,16 @@
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'
id 'application'
id 'com.github.johnrengelman.shadow' version '7.1.0'
}
import org.ajoberstar.grgit.Status
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
final boolean proj_git = grgit!=null
final String proj_store = MORNY_CODE_STORE
final String proj_commit = proj_git ? grgit.head().id : null
final String proj_commit_path = MORNY_COMMIT_PATH
final boolean proj_clean = isCleanBuild()
if (!proj_git)
println "[MornyBuild] git repository not available for current working space! git version tag will be disabled."
else if (isCleanBuild()) {
println "git: clean build at ${grgit.head().id}"
} else {
final Status status = grgit.status()
println "git: non-clean-build"
if (!status.unstaged.allChanges.empty) {
println "git: unstaged changes"
listChanges(status.unstaged)
}
if (!status.staged.allChanges.empty) {
println "git: staged changes"
listChanges(status.staged)
}
}
final String proj_group = 'cc.sukazyo'
final String proj_package = "${proj_group}.cono.morny"
final String proj_archive_name = MORNY_ARCHIVE_NAME
final String proj_application_main = "${proj_package}.ServerMain"
final String proj_version_base = VERSION
final String proj_version_delta = VERSION_DELTA
final boolean proj_version_use_delta = Boolean.parseBoolean(USE_DELTA)
final String proj_version = proj_version_base + (proj_version_use_delta ? "-δ${proj_version_delta}" : "")
final String proj_version_full = proj_version + (proj_git ? "+git.${proj_commit.substring(0, 8)}" + (proj_clean?"":".δ") : "")
final String proj_version_codename = CODENAME
final long proj_code_time = proj_clean ? grgit.head().dateTime.toInstant().toEpochMilli() : System.currentTimeMillis()
final JavaVersion proj_java = JavaVersion.VERSION_17
final Charset proj_file_encoding = StandardCharsets.UTF_8
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
String publish_remote_password = null
if (project.hasProperty("publishLocalArchiveRepoUrl")) publish_local_url = publishLocalArchiveRepoUrl
if (project.hasProperty("publishMvnRepoUrl")) {
publish_remote_url = publishMvnRepoUrl
publish_remote_username = publishMvnRepoUsername
publish_remote_password = publishMvnRepoPassword
}
group proj_group
version proj_version_full
group 'cc.sukazyo'
version VERSION
project.ext.archiveBaseName = 'Coeur_Morny_Cono'
project.ext.artifactId = 'morny-coeur'
mainClassName = 'cc.sukazyo.cono.morny.ServerMain'
repositories {
mavenCentral()
@ -75,142 +19,86 @@ repositories {
dependencies {
api "org.scala-lang:scala3-library_3:${proj_scala_lib}"
compileOnlyApi "com.github.spotbugs:spotbugs-annotations:${lib_spotbugs_v}"
compileOnlyApi "com.github.spotbugs:spotbugs-annotations:${libSpotbugsVersion}"
implementation "cc.sukazyo:messiva:${lib_messiva_v}"
implementation "cc.sukazyo:resource-tools:${lib_resourcetools_v}"
api "cc.sukazyo:messiva:${libMessivaVersion}"
implementation "com.github.pengrad:java-telegram-bot-api:${lib_javatelegramapi_v}"
implementation "com.squareup.okhttp3:okhttp:${lib_okhttp_v}"
implementation "com.google.code.gson:gson:${lib_gson_v}"
implementation "com.github.pengrad:java-telegram-bot-api:${libJavaTelegramBotApiVersion}"
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 "org.junit.jupiter:junit-jupiter-api:${libJunitVersion}"
testImplementation "org.junit.jupiter:junit-jupiter-params:${libJunitVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${libJunitVersion}"
}
task updateVersionCode {
ant.replaceregexp(match:'VERSION = ["a-zA-Z0-9.\\-_+@]+;', replace:"VERSION = \"$project.version\";", flags:'g', byline:true) {
fileset(dir: 'src/main/java/cc/sukazyo/cono/morny', includes: 'GradleProjectConfigures.java')
}
ant.replaceregexp(match:'CODENAME = ["a-zA-Z0-9]+;', replace:"CODENAME = \"${CODENAME}\";", flags:'g', byline:true) {
fileset(dir: 'src/main/java/cc/sukazyo/cono/morny', includes: 'GradleProjectConfigures.java')
}
ant.replaceregexp(match:'COMPILE_TIMESTAMP = [0-9]+L;', replace:"COMPILE_TIMESTAMP = ${System.currentTimeMillis()}L;", flags:'g', byline:true) {
fileset(dir: 'src/main/java/cc/sukazyo/cono/morny', includes: 'GradleProjectConfigures.java')
}
}
compileJava.dependsOn updateVersionCode
test {
useJUnitPlatform()
}
java {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
withSourcesJar()
}
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"
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
tasks.withType(Javadoc).configureEach {
options.encoding = proj_file_encoding.name()
tasks.withType(Javadoc) {
options.encoding = 'UTF-8'
options.docEncoding = 'UTF-8'
options.charSet = 'UTF-8'
}
//tasks.withType(ScalaDoc).configureEach {
//}
test {
}
application {
mainClass = proj_application_main
}
buildConfig {
packageName(proj_package)
buildConfigField('String', 'VERSION', "\"${proj_version}\"")
buildConfigField('String', 'VERSION_FULL', "\"${proj_version_full}\"")
buildConfigField('String', 'VERSION_BASE', "\"${proj_version_base}\"")
buildConfigField('String', 'VERSION_DELTA', proj_version_use_delta ? "\"${proj_version_delta}\"" : "null")
buildConfigField('String', 'CODENAME', "\"${proj_version_codename}\"")
buildConfigField('long', 'CODE_TIMESTAMP', "${proj_code_time}L")
buildConfigField('String', 'COMMIT', proj_git ? "\"${proj_commit}\"" : "null")
buildConfigField('boolean', 'CLEAN_BUILD', "${proj_clean}")
buildConfigField('String', 'CODE_STORE', proj_store==""?"null":"\"${proj_store}\"")
buildConfigField('String', 'COMMIT_PATH', proj_commit_path==""?"null":"\"${proj_commit_path}\"")
}
tasks.withType(Jar).configureEach {
archiveBaseName.set proj_archive_name
tasks.test {
useJUnitPlatform()
}
shadowJar {
archiveClassifier.set "fat"
if (project.hasProperty("dockerBuild")) {
println "shadow-jar: using docker build name"
archiveVersion.set ""
archiveClassifier.set "docker-build"
}
}
@SuppressWarnings('GrMethodMayBeStatic')
boolean isCleanBuild () {
if (grgit == null) return false
Set<String> changes = grgit.status().unstaged.allChanges + grgit.status().staged.allChanges
for (String file in changes) {
if (file.startsWith("src/")) return false
if (file == "build.gradle") return false
if (file == "gradle.properties") return false
}
return true
}
void listChanges (Status.Changes listing) {
for (String file in listing.added)
println " add: ${file}"
for (String file in listing.modified)
println " mod: ${file}"
for (String file in listing.removed)
println " del: ${file}"
archiveBaseName.set("${project.ext.archiveBaseName}")
archiveVersion.set("${project.version}")
archiveClassifier.set("fat")
}
publishing {
repositories{
if (publish_local_url != null) maven {
name 'archives'
url publish_local_url
maven {
name 'builds'
url publishLocalArchiveRepoUrl
}
if (publish_remote_url != null) maven {
maven {
name '-ws-'
url publish_remote_url
url publishMvnRepoUrl
credentials {
username publish_remote_username
password publish_remote_password
username publishMvnRepoUsername
password publishMvnRepoPassword
}
}
}
publications {
//noinspection GroovyAssignabilityCheck
main (MavenPublication) {
//noinspection GroovyAssignabilityCheck
from components.java
//noinspection GroovyAssignabilityCheck
groupId = proj_group
//noinspection GroovyAssignabilityCheck
artifactId = proj_archive_name
//noinspection GroovyAssignabilityCheck
version = proj_version
groupId = project.group
artifactId = project.ext.artifactId
version = project.version
}
}
}

View File

@ -1,4 +0,0 @@
services:
coeur-app:
build: .
command: -v

View File

@ -1,28 +1,15 @@
## Core
MORNY_ARCHIVE_NAME = morny-coeur
VERSION = 0.8.0.11
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
USE_DELTA = false
VERSION_DELTA =
CODENAME = beiping
CODENAME = putian
# dependencies
lib_spotbugs_v = 4.7.3
lib_scalamodule_xml_v = 2.2.0
libSpotbugsVersion = 4.7.2
lib_messiva_v = 0.1.1
lib_resourcetools_v = 0.2.2
libMessivaVersion = 0.1.0.1
lib_javatelegramapi_v = 6.2.0
libJavaTelegramBotApiVersion = 5.6.0
lib_okhttp_v = 4.11.0
lib_gson_v = 2.10.1
lib_scalatest_v = 3.2.17
libJunitVersion = 5.9.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-7.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

0
gradlew vendored Executable file → Normal file
View File

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

View File

@ -0,0 +1,10 @@
package cc.sukazyo.cono.morny;
/**
* the final field that will be updated by gradle automatically.
*/
public class GradleProjectConfigures {
public static final String VERSION = "0.8.0.11";
public static final String CODENAME = "putian";
public static final long COMPILE_TIMESTAMP = 1667376095614L;
}

View File

@ -0,0 +1,18 @@
package cc.sukazyo.cono.morny;
import cc.sukazyo.messiva.Logger;
import cc.sukazyo.messiva.appender.ConsoleAppender;
/**
* Morny log 管理器
*/
public class Log {
/**
* Morny Logger 实例
* messiva 更新
* @since 0.4.1.1
*/
public static final Logger logger = new Logger(new ConsoleAppender());
}

View File

@ -0,0 +1,320 @@
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 java.util.Set;
import static cc.sukazyo.cono.morny.Log.logger;
/**
* Morny Cono 核心<br>
* - 的程序化入口类保管着 morny 的核心属性<br>
*/
public class MornyCoeur {
/** 当前程序的 Morny Coeur 实例 */
private static MornyCoeur INSTANCE;
/** 当前 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;
private final boolean isRemoveCommandListWhenExit;
/**
* 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>
* <br>
* {@link cc.sukazyo.cono.morny.bot.event.OnUpdateTimestampOffsetLock}
* 会根据这里定义的时间戳取消掉比此时间更早的事件链
*/
public final long latestEventTimestamp;
/**
* morny 主程序启动时间<br>
* 用于统计数据
*/
public static final long coeurStartTimestamp = System.currentTimeMillis();
public static final long DINNER_CHAT_ID = -1001707106392L;
private record LogInResult(TelegramBot account, String username, long userid) { }
/**
* 执行 bot 初始化
*
* @param botKey bot telegram bot api token
* @param botUsername bot username 限定如果为 null 则表示不限定
* 如果指定则登录时会检查所登陆的 bot 的用户名是否与此相等
* @param master morny 实例所信任的主人的 id用于初始化 {@link #trusted}
* @param trustedChat morny 实例所信任的群组的 id用于初始化 {@link #trusted}
* @param latestEventTimestamp 事件处理器会处理事件的最早时间戳
* 只有限定的 message 事件会受此影响
* 单位为毫秒
*/
private MornyCoeur (
@Nullable String botApi, @Nullable String botApi4File,
@Nonnull String botKey, @Nullable String botUsername,
long master, long trustedChat, Set<Long> trustedRDinner,
long latestEventTimestamp,
boolean isRemoveCommandListWhenExit
) {
this.latestEventTimestamp = latestEventTimestamp;
this.isRemoveCommandListWhenExit = isRemoveCommandListWhenExit;
configureSafeExit();
logger.info("args key:\n " + botKey);
if (botUsername != null) {
logger.info("login as:\n " + botUsername);
}
try {
final LogInResult loginResult = login(botApi, botApi4File, botKey, botUsername);
this.account = loginResult.account;
this.username = loginResult.username;
this.userid = loginResult.userid;
this.trusted = new MornyTrusted(master, trustedChat, trustedRDinner);
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""",
master, 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 程序初始化方法
*/
public static void main (
@Nullable String botApi, @Nullable String botApi4File,
@Nonnull String botKey, @Nullable String botUsername,
long master, long trustedChat, Set<Long> trustedRDinner, long latestEventTimestamp,
boolean isAutomaticResetCommandList, boolean isRemoveCommandListWhenExit
) {
if (INSTANCE == null) {
logger.info("Coeur Starting");
INSTANCE = new MornyCoeur(
botApi, botApi4File,
botKey, botUsername,
master, trustedChat, trustedRDinner,
latestEventTimestamp,
isRemoveCommandListWhenExit
);
MornyDaemons.start();
logger.info("start telegram events listening");
EventListeners.registerAllListeners();
INSTANCE.account.setUpdatesListener(OnUpdate::onNormalUpdate);
if (isAutomaticResetCommandList) {
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 () {
logger.info("clean:save tracker data.");
MornyDaemons.stop();
if (isRemoveCommandListWhenExit) {
commandManager.automaticRemoveList();
}
}
/**
* 为程序在虚拟机上添加退出钩子
*/
private void configureSafeExit () {
Runtime.getRuntime().addShutdownHook(new Thread(this::exitCleanup, "exit-cleaning"));
}
/**
* 登录 bot<br>
* <br>
* 会反复尝试三次进行登录如果登录失败则会直接抛出 RuntimeException 结束处理
* 会通过 GetMe 动作验证是否连接上了 telegram api 服务器
* 同时也要求登录获得的 username {@link #username} 声明值相等
*
* @param api bot client 将会连接到的 telegram bot api 位置
* @param api4File bot client 将会连接到的 telegram file api 位置如果不指定则会跟随 {@code api} 选项的设定
* @param key bot api-token
* @param requireName 要求登录到的需要的 username如果登陆后的 username 与此不同则会报错退出
* @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) {
e.printStackTrace(System.out);
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.");
}
/**
* 获取登录成功后的 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;
}
/**
*
* 获取忽略时间点
*
* @return {@link #latestEventTimestamp MornyCoeur.latestEventTimestamp}
*/
public static long getLatestEventTimestamp () {
return INSTANCE.latestEventTimestamp;
}
/**
* 获取 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; }
}

View File

@ -0,0 +1,70 @@
package cc.sukazyo.cono.morny;
/**
* {@link #MORNY_PREVIEW_IMAGE_ASCII} 静态数据存放类
*/
@SuppressWarnings("all")
public class MornyHello {
/**
* 系统的开屏欢迎语 ASCII 字符画字段
*/
public static final String MORNY_PREVIEW_IMAGE_ASCII = """
ttt///t/////fucj(\\tvnxtf{< .' .. .:i` . . ^!`l|-^i+,!_[:1/|{i?//\\//jf\\\\\\///\\\\\\\\//\\\\\\//////\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\//\\\\\\\\/\\\\\\\\/\\\\//\\\\\\///\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\fnncvvU0O00QCx!!". .. ` \s
tt//////////\\jzjrucnjt/?{j,,"' . .' .. .":. .;{: ' "`.,1(<."i?)\\(-}\\\\\\(((\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\///\\//////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|\\\\\\\\\\\\///\\\\///\\\\\\\\\\\\\\\\|\\\\\\\\\\\\|\\\\\\\\\\\\\\\\tvXvuXcxn/[<!l~<` `I`. \s
tt//////t////\\//|rvx//\\(((-;,''" ",.,II..' `. . ^"' . .` .. .. .:: ```!],";";;^ "!?)/_ :li~)1[;<li<(\\1(1;+||\\||\\\\\\\\\\\\\\\\\\||\\\\\\||\\\\\\\\\\///\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\||\\\\\\\\\\\\\\\\//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\/\\\\||\\\\\\\\\\\\\\\\\\\\\\\\||||\\\\//t{}[!>Il)({_:.. ."` .,\s
//////////////////\\////|)/([}-_<+[]>.^^""[<'`^` .''""`'.`'`"i! ^!>l:' :<" !!.IiI`+l^^`i>_<`??)1;^{\\\\\\\\\\{|({({|/\\]I)\\\\()\\(]}|\\\\||\\|||\\/\\\\\\\\\\\\|||\\\\\\\\\\\\\\\\//\\\\\\\\/\\\\\\\\||\\\\\\\\\\\\\\\\//\\\\\\\\\\\\\\\\\\\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\//\\\\\\\\\\\\\\\\\\\\\\\\////\\\\\\\\\\\\\\\\\\//|{{?{|)[[-;
ttt/tt//////////////////{)(\\t(/tt/1~I}{-1\\_^])1_+[{|(?"<1~>>+!+[}11)}[(1}]};^1\\|~_1}{I:-1(I+)(|))|\\\\/////////\\\\////\\\\\\/////\\\\\\\\\\\\\\\\\\\\\\\\\\\\/\\//\\\\///\\//||\\////|)(//\\\\///){\\/\\(11|///({)//({[1\\\\\\\\\\\\\\\\\\\\|\\/\\\\\\/\\//////////\\\\\\\\\\\\\\\\\\//\\\\\\\\\\///////\\|\\\\\\\\//////\\\\///\\
tttt/////////////\\///////\\||///////t//|(|)|}|\\/(\\\\(//(l_{{<i!l}}})(()\\/\\///{{|||\\\\|\\\\((\\\\\\\\\\\\\\\\\\\\\\////\\\\\\\\\\//\\\\/\\////\\///\\\\\\\\\\\\\\\\\\//\\\\\\\\/\\|}[{|1?{|[i:,,;i<i,,,I~}}<<l^":I,`.':-<l!~l~i:!;,l)\\\\/\\//\\/\\\\\\/////\\\\\\//\\\\////\\\\\\\\\\\\\\\\//\\\\\\/\\\\///\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\//
//t//////////////\\///////////////////////\\//////////////////\\/\\/////\\\\\\\\\\\\\\//\\\\|||\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\/\\\\\\\\\\\\\\\\\\\\//\\\\/////////\\\\\\\\|))([+)}!<~"^,^.`' .^."~, :` '" `_>. ... ">+<^'I!: ^<(\\\\1}1//\\\\\\//////////\\\\///\\/\\///\\\\\\\\\\\\//\\\\//\\\\\\\\\\\\\\\\\\\\\\\\\\\\///\\(/\\{
t////////////////////////////////////////////////////////\\/\\\\///////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\/\\\\\\\\\\\\||\\|\\\\\\\\\\|\\\\\\\\/\\\\|\\\\\\\\\\\\////((|///}!:,":,^`. .;' ' '^..':. ^!;. .^^ '^^`. '' ...I[{!>:^;_i:'~\\ttt/////tt//\\\\////////////\\\\\\\\/\\\\\\\\\\\\/\\\\\\\\/\\\\\\\\\\)}-+[+I??i
ttt////////////////////////////////////////////////////////\\\\//\\//\\\\/\\\\\\\\\\\\\\\\\\\\//\\\\\\\\\\\\||\\\\\\/\\//\\\\\\\\\\\\\\\\\\/\\|\\\\\\////\\1;``^;<>+!">__+I `' .. "'. .;" ;;. .:^ ``,,;'` .;]I ,-_-|\\////t////t///////\\/\\\\\\\\//\\\\\\\\\\\\//\\\\\\///////-II<!l_; I
/////////////////////////////////////////////////\\\\/\\\\\\\\\\////\\\\//\\\\/////\\///////////tt//\\\\/\\////\\/////t/\\//\\\\///\\]{\\l,`'+< ,i ^i" . .`"l" . .`;?-' .` `>1ttt///tttt/////////\\/\\/////\\\\\\\\////t|+<}?!-]l<{[[1-+]
t//////////////////////////////////////\\/////////\\////////////////\\//////\\//tttttttttt//////////////////////////)_)t)|}1f/{<.^,^:~: . .. '''^:-|/> '-/}-_?\\/)-{?(//\\(\\tt////\\///\\\\\\\\\\\\\\///t1.;); .l~` '"
///////////////////////////////////////////tt/t(|tt//]+{t\\{][|////\\//////////ttttt///t//t/////////////\\//////|//{[|f}!l<!!I, `' ,!!i+- .`:l;IIll>>~++~<<<!:`. '' '^-l `.,l:.`{{[_:]/1l;>\\//]l~?])tt//\\\\\\\\\\/\\\\///\\\\|?<_}["^!;I^;]:. .
////////////////////////////////tttt/|{[1)]~!!+>!<_(/|[-<"i!l,]tt//ttt/t////ttt//t///ttttttt////////t//t//ttt){+. :?^ '. l_-!+l;;;|!!>~~il!lllllllllll!!lI:`'. .' :I;]_}>,?tf:.+fft)l+1//\\~`'I-(//\\/t/|/(-1[)/?>>II:' '.`';-'` \s
/////////////////////////t//()\\1_<>il^'''' ,!>;.,.'{tti `~tf(`'-(|fffftttttttttt/tttttttttttt///tttft//(t|]?-+!^ ."`. `. ;!I,. .?{il-\\_!~<>>!lII;IllIIIIIllllllllllI;;:,:,' '"^`(f{+{>' .<{t(I!}/||t> ^(//}>;:1\\]: "[:"` ^<: . II.'.. \s
///////////////ttt//tt((-!+}"'^. I, ,?<:' ,:;!>~',!_~{}-1]`^!}_+\\ttttttt/tttttttfff/tt\\(||]-?+;,:"l" '..'.. ?]l:" -(lI;,~?~!IIIlllI:IIlI;IIIIlllllllIIIIIllII!; . . '^^;~), "~!}\\/t//\\\\/_. '</||1?-)/\\\\)+_1>". '_i !i''' \s
tt//t///ttt///(]<>l>][l"'.`,. ^.^. ii ;; ~>>>. .i~I'^^<}), .;|tfftttttttttttf\\]}t-!,,I` .^ '. !: . .",I;. ^,I<)/-l:;llllllI;lIll;;IIIIlllIllIlIIlIllI;><. ' .;}". '.:+](ft\\}(t/t{;<\\{l^>}!^l\\/{>1/t(lI:I!+<<". ':" \s
t//tttt|?+!I!:' '` .`. ...... `^ "<^.;`^"'`,!".,^^^.,?)!. [f/+>(/tttft\\tff|+^,!' '^: >[,++:`' .I^ . _?!:^. ;~{/<II:IllIlllI;l;IlI;;IIIIIIlIIIIIIllIlllI+- "+;,...<\\;^_(/~}t/(+ ^(/?.:|)il)\\>?//)! __::. ':. '. \s
t/\\}[{]",il'`!-<-]:`'^` .. '' .^+:'. .^'"i:`^. ';`:<_|>'.?/t/!"<)ffftf)]]!'II.,l ^' ''. '";" .' `Il, ;]>]j_;lI;ll;!!llIII>~IIIIII;:<iIlIIlII;llllll;(> ;,~.',.<:`, 'I_|\\; .i|/]^ ?(}\\/////\\i' '' ....'^ \s
tf1<}i `^. `I` .I?"'. . . ^' .^' .'` .". >}_.I|t{_(tf({~,~(); ')t};.><,. .. . .. . . .]}^{j1IlllIlI!1IlllII?{IIIIIlI;[1!I;IIllIIlIIllI<x" '.!+~!,``...:. 'l]?l"i{/\\><]_;+/t\\(|\\/1,' "` ... \s
)+::((:^' ll .,` . . ..'. ' :+'`{tj{,l: ^;"..;!"^.I?' '~; .` .'. . `1+ [x?-:lIlll!]r-IllI~~{~I>lllll[i\\--+;;I~IIIII!l;x] "I"-<<_> >i.' l{}:itf/}[/\\)(\\}))|(:^^..'. `". \s
>.._f|i.:l,;^^''__. .^' `' "+<!~)?,,[>,`]1i`!1_. ^l: .". .` '1I +JIt!IIlll;]\\<vlIlI(;I\\i+<iII>) >1}c(_i(!IllI_l;(f. ,_";~~+^ .. .;-i '+([i+: !//1](||/\\(?:^..^^ \s
i'"}_,.` ''^... '. `. ^<`_> .. +x??_~]:[|!,.ll` . {+ ;Y[^|,>~IlIIf\\ {/;I!\\ [[-'<+l-{ _??]f\\n]lllI[!;1v` `+"]-}]~" ..'`l, ''i-` l+?\\\\\\/t{!)t[:' .^^ \s
;,:: :,^..;:. . i+..;^ `_<!,!~II,`. '' . "t..\\n^!]!i]<II!n> ]]<-?l``-]' I>]?+<l~<!._n_IllI1+i}J: ...l!,'`' `. . .~>. ^-|\\\\_I?]{t/?` .... \s
^(\\]I^~?;."!" . .. . ^<^ :( >t) _[il>|+:(U<1nYQ0Xx\\> . .~xcXXYzx(n?IllI}">xCI .:1]_-" . .^. `}>!}((1-^,+?" .. \s
1+,~I.<! .`' .. `x"(+1 >(l<?_1}Cj!f\\?l"' '"I~]|_;Ill{ vQ" ':,~_' .. .. .`^[(`,?\\||\\+ '," . \s
}<. `^]!.;lI>' . ... ~|r:;`.+I?\\};+t) "".-?;lI>(;]xn. '_>]!+. '^'`l:11l[|((+?: . \s
[-`.':;..""' lv|. .:_(;I!u> ^,",^. .;?I]?IlI}n[</( "+'.. .I]; ',;!\\/\\|~I}" '";'. \s
.,><I>^ ')(I' `tlII>x1" <}1{)l "~+ |[II;\\[:~zl .i;. .... `"i\\\\}]..!' ^.... \s
-" ` ' . ":` .|+<<;!\\U)>^ '^`' ^"I?)c-;<if]>j/ <1I~;` .!}\\(: .;"`' \s
' 'l, .' ~[><+;!f()nn|]!:' ..^:!+1fcjx}}v!_)})>|n` ~^ ^;"'` .<+I<)/||\\i'"<"'^ `. \s
II ^}_'+_!fI?_/-jJjUr\\\\ucJJz\\|J>}?-j{]^ni<z; : `' .-+~->" .;](),.;-<`' .^ .^' \s
+[" +]{.`i;I</-jmvxjj)<^ !0~l-?+` : '" '` .' l\\)-++{f\\" ',^^^ .^ ,!` '' \s
.>; ::."!??l.^Ywj}<, (n, ,~_:` .,, ` `` ' '... ^+-l,]}]}\\j/!. . ` 'I<~`'{tl..^` \s
'' .,<{[>" i/i" `-[; ,<_[>^i_l,:^_! ',+l.. ^,:,,. ;~>l;^ l> ',;I^???~,'l".. .. \s
;{?l. !+ .. .<1i '^' "}|{:-+-;?\\[)-] ^:l1-:. '' '`, . ';.`~^ '. ..^`. \s
'i+;]}!,. <))\\!<|ji >((_}}?t)}\\\\v|]?jI!), "lf!l. ... .^ . ". \s
.+{>` l/z\\!,>""I+~_){]<t~-(! .!nt_]]]])1)]]\\( ?_' :( . . `:. `; ." ... .. \s
Ir. ..>vQjut_~~>>>_-<]<-)f":l_v){\\/1}}}{t/\\0?z~. ^' `-l . . .. . \s
l\\ :_>>i:^+\\)_-]!:>-+<?\\Uzfjj-/mxt|[??/f)??}/mc1;. . . . ^' .
I1I`l?-}l>l'...`^;1!^ 'l>})l\\n\\Qt?]?]})1{][[(XC>^ ...
^-+^.i-((?!"`:>l<[<!tc_:;<+>~<]nQY+?????][{\\cmO||l . ''..
'|: '^[{~)\\_+++))1{uxnvt(t){{[[u0\\1|({1()){-?|xfc: .. \s
.<-, ]-]]]})11)){{{{}{{)|{}}{1{111{11{{}}]_!"x\\]Xf \s
,]<\\}][[[[[[[[[[[[[[[]][[[[[[[[][[[[[[]?-+!YC{z} .`
"_[}?]][[[[[[[[[[[[[[[[[[][[[[[[[[[[[[[[[]vn\\?. ^
^. ;{_(_??][[[[[[[[[[[[[[[[[|[[[[[[[[[[[[[[[[v_(]^ \s
.' '. :t>/?[[[[[[[[[[[[[[[[[[[]t\\][[[[[[[[[[][]?u;()_ .. .'. . .
`. .` :)!j_]][[[]]]]][[[[[[[[[[}j(/{[[[[[][[}1{~n!)ft . .. .. . .. .
". ..' .` . "' .^":;~ti{\\1]][]]]??]]]]][]]??[){[}[[[[?+}]<I"]({~c' .,",. .. ', .'`I' ^l. \s
. .. .'. ..'.... '.;,.'.":`,_: '` . '"^. '<:,'. 'l:`'. . ';"'. .i~I:;,,`.^!{[?[[[[[[[[[[[[[[]?]]]])t11])I;:. '^c^ .. '. . .' .^ .."?, '` \s
' '^. `. . I;. . `^ '... ...``'. ,">!^':``^``,`;I.!<>?>:??i:;-;,<_..^,Ii: 'l,i+```' ..'!; ''. ?~.'<+ .li!:,1?}[[[[[[[[[[[[[[[[[[[[)1}]t[:, "O" . .' . ^!' . ". i+' . \s
^. . .':`'`~/1,-<-~^'^'^^,`.."i_>^. `1t]!,^I]l^;`,I::_?]?[!:;`.`"'`l!l<1f{~>;]\\1(]I>~l!l[<,,;`lI,~},^>!>l<l[-' I-+:i_!ll::l"`>'...'": 'x' ]l>i .:I1[~]]]]}}}}}[[}}[}}}}}[[]]]??1}}[}~;:>vx. :;..:??,.' ^` I;>.";:"^' .. ^'.^"" ' .!},' .
~!;:!".":i"^_/|]^li(\\1;;it{' .[\\fft+<}(/{}/)|f||<tj\\/1//jj[<<~])11?''.`;\\)l!(ff~!\\j1-|\\t\\f\\//||>'.^{/[)?!:(?+-,I+fjtil"'"+fj{i:',!!;!!^:.`r. r; !l'"i1?!i>~+_?][[[?-???]][[]?-+-]??+~<_{[_l?> ''^l;l`-}<`^.i>+l ``;I":+?!~-l ,>>l.'.;. ':!!(/!":,I
/t<I^^!{{-;[[-~|1\\jj\\?(/)f{~~+}}//t(?(ft}{tf]j1>1ffft+{jff/ttff)];)?1(/tt\\/t/tfttttfftf/1\\|t\\|/?<_]_]{<_]/f({fffjttf/[i>1//|tft|" :<~:+}, ]>if" .:-~ >) ^`^l)f(_<??)\\}}(t|}>{/\\}-+1\\()t-{j/]!:^'l<]\\)+ ."_?I_{
ffft)|)(t[_-{tjjrjrj/{(||}(rjj\\1)I<\\((ffj/rjffttjffftrjfffrtfff/f[1jjffffftt//)}tttff/ttt[<{rj}tf1?<:~{/j)>)fttf|?)tfffftt1_;+tf1-1|~i1, >;:} '1_ ;( .. .. `:"_1{}tjtvj)vjr/|jfff/<(tf)+1/)1j)~~-[j[l|[(/\\j{:-]]([}\\t
""";
}

View File

@ -0,0 +1,54 @@
package cc.sukazyo.cono.morny;
import cc.sukazyo.cono.morny.util.FileUtils;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
/**
* Morny Cono Coeur 的程序属性存放类
*/
public class MornySystem {
/**
* 程序的语义化版本号<br>
* 会由 gradle 任务 {@code updateVersionCode} 更新
*/
public static final String VERSION = GradleProjectConfigures.VERSION;
/**
* Morny Coeur 当前的版本代号.<br>
* 一个单个单词一般作为一个大版本的名称只在重大更新改变<br>
* 格式保持为仅由小写字母和数字组成<br>
* 有时也可能是复合词或特殊的词句<br>
* <br>
* 会由 gradle 任务 {@code updateVersionCode} 更新
*/
public static final String CODENAME = GradleProjectConfigures.CODENAME;
/**
* 获取程序 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) {
e.printStackTrace(System.out);
return "<non-jar-runtime>";
} catch (NoSuchAlgorithmException e) {
e.printStackTrace(System.out);
return "<calculation-error>";
}
}
}

View File

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

View File

@ -0,0 +1,249 @@
package cc.sukazyo.cono.morny;
import cc.sukazyo.cono.morny.util.CommonFormat;
import javax.annotation.Nonnull;
import java.util.HashSet;
import java.util.Set;
import static cc.sukazyo.cono.morny.Log.logger;
/**
* 程序启动入口<br>
* <br>
* 会处理程序传入的参数和选项等数据并执行对应的启动方式<br>
*
* @since 0.4.0.0
*/
public class ServerMain {
public static final String PROP_TOKEN_KEY = "TELEGRAM_BOT_API_TOKEN";
public static final String PROP_TOKEN_MORNY_KEY = "MORNY_TG_TOKEN";
private static final String THREAD_MORNY_INIT = "morny-init";
/**
* 程序入口也是参数处理器<br>
* <br>
* {@code -} 开头的参数会被解析为选项<br>
* <br>
* 支持以下选项
* <ul>
* <li>
* {@code --version} 只输出版本信息不运行主程序此参数会导致其它所有参数失效优先级最高
* </li>
* <li>
* {@code --only-hello} 只输出欢迎字符画({@link MornyHello})不运行主程序
* 不要同时使用 {@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 --no-hello} 不在主程序启动时输出用于欢迎消息的字符画
* {@code --only-hello} 参数不兼容 会导致程序完全没有任何输出
* </li>
* <li>
* {@code --outdated-block} 会使得 {@link MornyCoeur#latestEventTimestamp}
* 赋值为程序启动的时间从而造成阻挡程序启动之前的消息事件处理效果
* </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#main
* @since 0.4.0.0
* @param args 参数组
*/
public static void main (@Nonnull String[] args) {
//#
//# 启动参数设置区块
//#
boolean versionEchoMode = false;
boolean welcomeEchoMode = false;
boolean showWelcome = true;
String key = null;
String username = null;
boolean outdatedBlock = false;
long master = 793274677L;
Set<Long> trustedReadersOfDinner = new HashSet<>();
long trustedChat = -1001541451710L;
boolean autoCmdList = false;
boolean autoCmdRemove = false;
String api = null;
String api4File = null;
for (int i = 0; i < args.length; i++) {
if (args[i].startsWith("-")) {
switch (args[i]) {
case "--outdated-block", "-ob" -> {
outdatedBlock = 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++;
key = args[i];
continue;
}
case "--username", "-u" -> {
i++;
username = args[i];
continue;
}
case "--master", "-mm" -> {
i++;
master = Long.parseLong(args[i]);
continue;
}
case "--trusted-chat", "-trs" -> {
i++;
trustedChat = Long.parseLong(args[i]);
continue;
}
//noinspection SpellCheckingInspection
case "--trusted-reader-dinner", "-trsd" -> {
i++;
trustedReadersOfDinner.add(Long.parseLong(args[i]));
continue;
}
case "--auto-cmd", "-cmd", "-c" -> {
autoCmdList = true;
autoCmdRemove = true;
continue;
}
case "--auto-cmd-list", "-ca" -> {
autoCmdList = true;
continue;
}
case "--auto-cmd-remove", "-cr" -> {
autoCmdRemove = true;
continue;
}
case "--api", "-a" -> {
i++;
api = args[i];
continue;
}
case "--api-files", "files-api", "-af" -> {
i++;
api4File = args[i];
continue;
}
}
}
logger.warn("Can't understand arg to some meaning :\n " + args[i]);
}
String propToken = null;
String propTokenKey = null;
for (String iKey : new String[]{PROP_TOKEN_KEY, PROP_TOKEN_MORNY_KEY}) {
if (System.getenv(iKey) != null) {
propToken = System.getenv(iKey);
propTokenKey = iKey;
}
}
//#
//# 启动相关参数的检查和处理
//#
if (versionEchoMode) {
logger.info(String.format("""
Morny Cono Version
- version :
%s %s
- md5hash :
%s
- co.time :
%d
%s [UTC]""",
MornySystem.VERSION, MornySystem.CODENAME.toUpperCase(),
MornySystem.getJarMd5(),
GradleProjectConfigures.COMPILE_TIMESTAMP,
CommonFormat.formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0)
));
return;
}
if (showWelcome) logger.info(MornyHello.MORNY_PREVIEW_IMAGE_ASCII);
if (welcomeEchoMode) return;
logger.info(String.format("""
ServerMain.java Loaded >>>
- version %s (%s)(%d)
- Morny %s""",
MornySystem.VERSION,
MornySystem.getJarMd5(), GradleProjectConfigures.COMPILE_TIMESTAMP,
MornySystem.CODENAME.toUpperCase()
));
//#
//# Coeur 参数检查和正式启动主程序
//#
if (propToken != null) {
key = propToken;
logger.info("Parameter <token> set by EnvVar $"+propTokenKey);
}
if (key == null) {
logger.info("Parameter required has no value:\n --token.");
return;
}
Thread.currentThread().setName(THREAD_MORNY_INIT);
MornyCoeur.main(
api, api4File,
key, username,
master, trustedChat, trustedReadersOfDinner,
outdatedBlock?System.currentTimeMillis():0,
autoCmdList, autoCmdRemove
);
}
}

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,123 @@
package cc.sukazyo.cono.morny.bot.api;
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 (EventRuntimeException e) {
final StringBuilder errorMessage = new StringBuilder();
errorMessage.append("Event runtime breaks: " + e.getMessage()).append('\n');
errorMessage.append("at " + e.getStackTrace()[0].toString()).append('\n');
errorMessage.append("at " + e.getStackTrace()[1].toString()).append('\n');
errorMessage.append("at " + e.getStackTrace()[2].toString()).append('\n');
errorMessage.append("at " + e.getStackTrace()[3].toString()).append('\n');
if (e instanceof EventRuntimeException.ActionFailed) {
errorMessage.append((
"\"telegram request track\": " +
new GsonBuilder().setPrettyPrinting().create().toJson(((EventRuntimeException.ActionFailed)e).getResponse())
).indent(4)).append('\n');
}
logger.error(errorMessage.toString());
} catch (Exception e) {
logger.error("Event Error!");
e.printStackTrace(System.out);
}
}
}
}
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,205 @@
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.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());
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());
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,92 @@
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) {
boolean isOk = false;
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
);isOk = true;
}
break;
case "group":
OnEventHackHandle.registerHack(
event.message().messageId(),
event.message().from().id(),
event.message().chat().id(),
OnEventHackHandle.HackType.GROUP
);isOk = true;
break;
default:
OnEventHackHandle.registerHack(
event.message().messageId(),
event.message().from().id(),
event.message().chat().id(),
OnEventHackHandle.HackType.USER
);isOk = true;
break;
}
if (isOk) {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_WAITING
).replyToMessageId(event.message().messageId())
);
} else {
MornyCoeur.extra().exec(new SendSticker(
event.message().chat().id(),
TelegramStickers.ID_403
).replyToMessageId(event.message().messageId())
);
}
}
}

View File

@ -0,0 +1,79 @@
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; }
long userId = event.message().from().id();
if (event.message().replyToMessage()!= null) {
userId = event.message().replyToMessage().from().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,86 @@
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 org.jetbrains.annotations.NotNull;
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 (@NotNull InputCommand command, @NotNull 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 (@NotNull InputCommand command, @NotNull 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,373 @@
package cc.sukazyo.cono.morny.bot.command;
import cc.sukazyo.cono.morny.GradleProjectConfigures;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.MornySystem;
import cc.sukazyo.cono.morny.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 org.jetbrains.annotations.NotNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static cc.sukazyo.cono.morny.Log.logger;
import static cc.sukazyo.cono.morny.util.CommonFormat.formatDate;
import static cc.sukazyo.cono.morny.util.CommonFormat.formatDuration;
import static cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml;
public class MornyCommands {
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 HelloOnStart(),
new GetUsernameAndId(),
new EventHack(),
new Nbnhhsh(),
new Ip186Query.Ip(),
new Ip186Query.Whois(),
new Encryptor(),
new SaveData(),
new MornyInformations(),
new Version(),
new MornyRuntime(),
new Jrrp(),
new Exit(), new ExitAlias()
);
// 特殊的命令
register(
new Testing(),
new DirectMsgClear()
);
// 统一注册这些奇怪的东西&.&
register(
new 喵呜.抱抱(),
new 喵呜.揉揉(),
new 喵呜.蹭蹭(),
new 喵呜.贴贴(),
new 私わね(),
new 喵呜.Progynova()
);
}
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, "".equals(paramRule) ? (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); }
}
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 (@NotNull InputCommand command, @NotNull 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());
System.exit(0);
} 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());
}
}
private static class Version implements ITelegramCommand {
@Nonnull @Override public String getName () { return "version"; }
@Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "检查 Bot 版本信息"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandVersionExec(event); }
}
private static void onCommandVersionExec (@Nonnull Update event) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
String.format(
"""
version:
- Morny <code>%s</code>
- <code>%s</code>
core md5_hash:
- <code>%s</code>
compile timestamp:
- <code>%d</code>
- <code>%s [UTC]</code>""",
escapeHtml(MornySystem.CODENAME.toUpperCase()),
escapeHtml(MornySystem.VERSION),
escapeHtml(MornySystem.getJarMd5()),
GradleProjectConfigures.COMPILE_TIMESTAMP,
escapeHtml(formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0))
)
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
}
private static class MornyRuntime implements ITelegramCommand {
@Nonnull @Override public String getName () { return "runtime"; }
@Nullable @Override public String[] getAliases () { return null; }
@Nonnull @Override public String getParamRule () { return ""; }
@Nonnull @Override public String getDescription () { return "获取 Bot 运行时信息(包括版本号)"; }
@Override public void execute (@Nonnull InputCommand command, @Nonnull Update event) { onCommandRuntimeExec(event); }
}
/**
* @since 0.4.1.2
*/
private static void onCommandRuntimeExec (@Nonnull Update event) {
String hostname;
try {
hostname = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
hostname = "<unknown>";
}
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
String.format("""
system:
- <code>%s</code>
- <code>%s</code>
- <code>%s</code>
java runtime:
- <code>%s</code>
- <code>%s</code>
vm memory:
- <code>%d</code> / <code>%d</code> MB
- <code>%d</code> cores
coeur version:
- <code>%s</code> (<code>%s</code>)
- <code>%s</code>
- <code>%s [UTC]</code>
- [<code>%d</code>]
continuous:
- <code>%s</code>
- [<code>%d</code>]
- <code>%s [UTC]</code>
- [<code>%d</code>]""",
// system
escapeHtml(hostname),
escapeHtml(String.format("%s (%s)", System.getProperty("os.name"), System.getProperty("os.arch"))),
escapeHtml(System.getProperty("os.version")),
// java
escapeHtml(System.getProperty("java.vm.vendor")+"."+System.getProperty("java.vm.name")),
escapeHtml(System.getProperty("java.vm.version")),
// memory
Runtime.getRuntime().totalMemory() / 1024 / 1024,
Runtime.getRuntime().maxMemory() / 1024 / 1024,
Runtime.getRuntime().availableProcessors(),
// version
escapeHtml(MornySystem.VERSION),
escapeHtml(MornySystem.CODENAME),
escapeHtml(MornySystem.getJarMd5()),
escapeHtml(formatDate(GradleProjectConfigures.COMPILE_TIMESTAMP, 0)),
GradleProjectConfigures.COMPILE_TIMESTAMP,
// continuous
escapeHtml(formatDuration(System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp)),
System.currentTimeMillis() - MornyCoeur.coeurStartTimestamp,
escapeHtml(formatDate(MornyCoeur.coeurStartTimestamp, 0)),
MornyCoeur.coeurStartTimestamp
)
).replyToMessageId(event.message().messageId()).parseMode(ParseMode.HTML));
}
private static class Jrrp implements ITelegramCommand {
@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());
}
}
}

View File

@ -0,0 +1,46 @@
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.Update;
import com.pengrad.telegrambot.request.SendSticker;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class MornyInformations implements ITelegramCommand {
private static final String ACT_STICKER = "stickers";
@Nonnull @Override public String getName () { return "info"; }
@Nullable @Override public String[] getAliases () { return new String[0]; }
@Nonnull @Override public String getParamRule () { return "[(stickers)|(stickers.)sticker_id]"; }
@Nonnull @Override public String getDescription () { return "输出 Morny 当前版本的一些预定义信息"; }
@Override
public void execute (@Nonnull InputCommand command, @Nonnull Update event) {
if (!command.hasArgs() || command.getArgs().length > 1) {
MornyCoeur.extra().exec(new SendSticker(event.message().chat().id(), TelegramStickers.ID_404).replyToMessageId(event.message().messageId()));
}
final String action = command.getArgs()[0];
if (action.startsWith("stickers")) {
if (action.equals("stickers"))
TelegramStickers.echoAllStickers(MornyCoeur.extra(), event.message().chat().id(), event.message().messageId());
else {
TelegramStickers.echoStickerByID(
action.substring((ACT_STICKER+".").length()),
MornyCoeur.extra(), event.message().chat().id(), event.message().messageId()
);
}
return;
}
MornyCoeur.extra().exec(new SendSticker(event.message().chat().id(), TelegramStickers.ID_404).replyToMessageId(event.message().messageId()));
}
}

View File

@ -0,0 +1,70 @@
package cc.sukazyo.cono.morny.bot.command;
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 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) {
try {
String queryTarget = "";
if (event.message().replyToMessage() != null && event.message().replyToMessage().text() != null)
queryTarget = event.message().replyToMessage().text();
if (command.hasArgs())
queryTarget = stringsConnecting(command.getArgs(), " ", 0, command.getArgs().length-1);
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 (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,74 @@
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.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;
@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) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"抱抱——"
));
}
}
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) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"蹭蹭w"
));
}
}
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) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"喵呜~-"
));
}
}
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) {
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
"<tg-spoiler>(贴贴喵呜&amp;.&amp;)</tg-spoiler>"
).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,38 @@
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 java.util.concurrent.ThreadLocalRandom;
@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 (ThreadLocalRandom.current().nextInt(521) == 0) {
// 可以接入未来的心情系统如果有的话
final String text = switch (ThreadLocalRandom.current().nextInt(11)) {
case 0,7,8,9,10 -> "才不是";
case 1,2,3,6 -> "才不是!";
case 4,5 -> "才不是..";
default -> throw new IllegalStateException("Unexpected random value in 私わね command.");
};
MornyCoeur.extra().exec(new SendMessage(
event.message().chat().id(),
text
).replyToMessageId(event.message().messageId()));
}
}
}

View File

@ -0,0 +1,38 @@
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();
@SuppressWarnings("unused") 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();
@SuppressWarnings("unused") 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 void registerAllListeners () {
EventListenerManager.addListener(
// ACTIVITY_RECORDER,
UPDATE_TIMESTAMP_OFFSET_LOCK,
/* write functional event behind here */
// KUOHUANHUAN_NEED_SLEEP,
COMMANDS_LISTENER,
RANDOMLY_TRIGGERED,
USER_RANDOMS,
USER_SLASH_ACTION,
INLINE_QUERY,
CALL_ME,
CALL_MSG_SEND,
MEDICATION_NOTIFY_APPLY,
EVENT_HACK_HANDLE
);
}
}

View File

@ -0,0 +1,27 @@
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;
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,182 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.MornyCoeur;
import cc.sukazyo.cono.morny.MornyTrusted;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.data.TelegramStickers;
import cc.sukazyo.cono.morny.util.CommonFormat;
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 MornyTrusted#MASTER} 的值
* @since 0.4.2.1
*/
private static final long ME = MornyCoeur.trustedInstance().MASTER;
/**
* 监听私聊 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.DINNER_CHAT_ID)).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,221 @@
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 (
String message,
MessageEntity[] entities,
ParseMode parseMode,
long targetId
) { }
@Override
public boolean onMessage(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) 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));
}
// 发送文本测试
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,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.DeleteMessage;
import javax.annotation.Nonnull;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
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,28 @@
package cc.sukazyo.cono.morny.bot.event;
import cc.sukazyo.cono.morny.bot.api.EventListener;
import cc.sukazyo.cono.morny.daemon.MedicationTimer;
import cc.sukazyo.cono.morny.daemon.MornyDaemons;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update;
import org.jetbrains.annotations.NotNull;
public class OnMedicationNotifyApply extends EventListener {
@Override
public boolean onEditedChannelPost (@NotNull Update update) {
return editedMessageProcess(update.editedChannelPost());
}
@Override
public boolean onEditedMessage (@NotNull Update update) {
return editedMessageProcess(update.editedMessage());
}
private boolean editedMessageProcess (Message edited) {
if (edited.chat().id() != MedicationTimer.NOTIFY_CHAT) return false;
MornyDaemons.medicationTimerInstance.refreshNotificationWrite(edited);
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,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 MornyCoeur#latestEventTimestamp 指定时间} 之前的事件处理.
* <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.getLatestEventTimestamp()/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,52 @@
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 com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.SendMessage;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class OnUserRandoms extends EventListener {
private static final Pattern USER_OR_CN_QUERY = Pattern.compile("(.+)还是(.+)");
private static final Pattern USER_OR_EN_QUERY = Pattern.compile("(.+)or(.+)");
@Override
public boolean onMessage (@NotNull Update update) {
if (update.message().text() == null) return false;
if (!update.message().text().startsWith("/")) return false;
final String[] preProcess = UniversalCommand.format(update.message().text());
if (preProcess.length > 1) return false;
final String query = preProcess[0];
// ----- START CODE BLOCK COMMENT -----
// 这里实现思路和代码优化有至少一半是 copilot IDEA 提供的
// 实现思路都可以从人类手里抢一半贡献太恐怖了aba
String result = null;
final Matcher matcher;
if (query.contains("还是")) {
matcher = USER_OR_CN_QUERY.matcher(query);
} else {
matcher = USER_OR_EN_QUERY.matcher(query);
}
if (matcher.find()) {
result = ThreadLocalRandom.current().nextBoolean() ? matcher.group(1) : matcher.group(2);
}
// ----- STOP CODE BLOCK COMMENT -----
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+)|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]+))/?(\\?(?:p=(\\d+))?.*)?|(?:av|AV)(\\d+)|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]+))$");
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(
// "====== 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,78 @@
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.util.ArrayList;
import java.util.List;
import static cc.sukazyo.cono.morny.Log.logger;
public class MedicationTimer extends Thread {
public static final long NOTIFY_CHAT = -1001729016815L;
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;
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 (Exception e) {
logger.error("Unexpected error occurred");
e.printStackTrace(System.out);
}
}
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;
}
private static long calcNextRoutineTimestamp () {
return ((System.currentTimeMillis()+8*60*60*1000) / (12*60*60*1000) + 1) * 12*60*60*1000 - 8*60*60*1000;
}
private void waitToNextRoutine () throws InterruptedException {
sleep(calcNextRoutineTimestamp() - System.currentTimeMillis());
}
}

View File

@ -0,0 +1,30 @@
package cc.sukazyo.cono.morny.daemon;
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();
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); }
logger.info("ALL Morny Daemons STOPPED.");
}
}

View File

@ -0,0 +1,133 @@
package cc.sukazyo.cono.morny.daemon;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantLock;
import static cc.sukazyo.cono.morny.Log.logger;
public class TrackerDataManager {
public static final ReentrantLock trackingLock = new ReentrantLock();
private static final ReentrantLock recordLock = new ReentrantLock();
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.");
}
}
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();
}
public static void init () {
DAEMON.start();
}
public static void save () {
logger.info("start writing tracker data.");
save(reset());
logger.info("done writing tracker data.");
}
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;
}
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;
channelCurrent.write(ByteBuffer.wrap(
String.format("%d\n", timestamp).getBytes(StandardCharsets.UTF_8)
));
} catch (Exception e) {
logger.error(String.format("exception in write tracker data: %d/%d/%d", chat, user, timestamp));
e.printStackTrace(System.out);
}
}
}));
}
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 new FileOutputStream(file, true).getChannel();
}
}

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,50 @@
package cc.sukazyo.cono.morny.data;
import java.io.IOException;
import com.google.gson.Gson;
import okhttp3.MediaType;
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/";
public static final String API_GUESS_DATA_TEMPLATE = "{ \"text\": \"%s\" }";
private static final OkHttpClient httpClient = new OkHttpClient();
public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
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(JSON, reqJsonText))
.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

@ -1,9 +1,11 @@
package cc.sukazyo.cono.morny.data;
import javax.annotation.Nonnull;
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 java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 存放 bot 使用到的贴纸
@ -21,30 +23,42 @@ public class TelegramStickers {
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";
public static final String ID_501 = "CAACAgEAAxkBAAIHbGUhJ8zm2Sb_c0YU-DYQ6xb-ZDtaAAKdJwACePzGBTOftDZL6X7vMAQ";
@Nonnull
public static Map<String, String> map () {
final LinkedHashMap<String, String> mapping = new LinkedHashMap<>();
public static void echoAllStickers (ExtraAction actionObject, long sentChat, int replyToMessageId) {
for (Field object : TelegramStickers.class.getFields()) {
if (object.getType()==String.class && object.getName().startsWith("ID_")) {
try {
mapping.put(object.getName(), (String)object.get(""));
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);
}
}
}
return mapping;
}
@Nonnull
public static Map.Entry<String, String> getById (@Nonnull String stickerFieldID)
throws NoSuchFieldException {
public static void echoStickerByID (String stickerFieldID, ExtraAction actionObject, long sentChat, int replyToMessageId) {
try {
// normally get the sticker and echo
Field field = TelegramStickers.class.getField(stickerFieldID);
return Map.entry(field.getName(), (String)field.get(""));
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,73 @@
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};
/**
* 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.
*
* @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.
*/
@Nonnegative
public static long toAv (@Nonnull String bv) {
long av = 0;
for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) {
av += BV_TABLE_REVERSED.get(bv.charAt(BV_TEMPLATE_FILTER[i])) * 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,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,47 @@
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 (coma[i] == '"') {
break;
} else if (coma[i] == '\\' && (coma[i+1] == '"' || coma[i+1] == '\\')) {
i++;
tmp.append(coma[i]);
} else {
tmp.append(coma[i]);
}
}
} else if (coma[i] == '\\' && (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

@ -0,0 +1,88 @@
package cc.sukazyo.cono.morny.util.tgapi;
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException;
import com.pengrad.telegrambot.TelegramBot;
import com.pengrad.telegrambot.model.Chat;
import com.pengrad.telegrambot.model.ChatMember;
import com.pengrad.telegrambot.model.User;
import com.pengrad.telegrambot.request.BaseRequest;
import com.pengrad.telegrambot.request.GetChatMember;
import com.pengrad.telegrambot.response.BaseResponse;
public class ExtraAction {
private final TelegramBot bot;
public ExtraAction (TelegramBot bot) {
this.bot = bot;
}
public static ExtraAction as (TelegramBot bot) {
return new ExtraAction(bot);
}
public boolean isUserInGroup (User user, Chat chat) {
return isUserInGroup(user.id(), chat.id());
}
public <T extends BaseRequest<T, R>, R extends BaseResponse> R exec (T req) {
return exec(req, "");
}
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.equals("") ? String.valueOf(resp.errorCode()) : errorMessage),
resp
);
return resp;
}
public boolean isUserInGroup (User user, Chat chat, ChatMember.Status permissionLevel) {
return isUserInGroup(user.id(), chat.id(), permissionLevel);
}
public boolean isUserInGroup (long userId, long chatId) {
return isUserInGroup(userId, chatId, ChatMember.Status.restricted);
}
public boolean isUserInGroup (long userId, long chatId, ChatMember.Status permissionLevel) {
final ChatMember chatMember = exec(new GetChatMember(chatId, userId)).chatMember();
return
chatMember != null &&
UserPermissionLevel.as(chatMember.status()).hasPermission(UserPermissionLevel.as(permissionLevel));
}
}
enum UserPermissionLevel {
CREATOR(3),
ADMINISTRATOR(2),
MEMBER(1),
RESTRICTED(0),
LEFT(-1),
KICKED(-2);
final int permissionLevel;
UserPermissionLevel (int permissionLevel) {
this.permissionLevel = permissionLevel;
}
static UserPermissionLevel as (ChatMember.Status status) {
return switch (status) {
case creator -> CREATOR;
case administrator -> ADMINISTRATOR;
case member -> MEMBER;
case restricted -> RESTRICTED;
case left -> LEFT;
case kicked -> KICKED;
};
}
boolean hasPermission (UserPermissionLevel required) {
return this.permissionLevel >= required.permissionLevel;
}
}

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

View File

@ -1,55 +0,0 @@
ttt///t/////fucj(\tvnxtf{< .' .. .:i` . . ^!`l|-^i+,!_[:1/|{i?//\//jf\\\///\\\\//\\\//////\\/\\\\\\\\\\\\\\//\\\\/\\\\/\\//\\\///\\\\\\\\\\\\\\\\\\\\fnncvvU0O00QCx!!". .. `
tt//////////\jzjrucnjt/?{j,,"' . .' .. .":. .;{: ' "`.,1(<."i?)\(-}\\\(((\\/\\\\\\\\\\\\\\\\///\//////\\\\\\\\\\\\\\\\\\\\\\\|\\\\\\///\\///\\\\\\\\|\\\\\\|\\\\\\\\tvXvuXcxn/[<!l~<` `I`.
tt//////t////\//|rvx//\(((-;,''" ",.,II..' `. . ^"' . .` .. .. .:: ```!],";";;^ "!?)/_ :li~)1[;<li<(\1(1;+||\||\\\\\\\\\||\\\||\\\\\///\\\\\\\\\\\\\\\\\\\\\\\\\\\||\\\\\\\\//\\\\\\\\\\\\\\\\\/\\||\\\\\\\\\\\\||||\\//t{}[!>Il)({_:.. ."` .,
//////////////////\////|)/([}-_<+[]>.^^""[<'`^` .''""`'.`'`"i! ^!>l:' :<" !!.IiI`+l^^`i>_<`??)1;^{\\\\\{|({({|/\]I)\\()\(]}|\\||\|||\/\\\\\\|||\\\\\\\\//\\\\/\\\\||\\\\\\\\//\\\\\\\\\\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\//\\\\\\\\\\\\////\\\\\\\\\//|{{?{|)[[-;
ttt/tt//////////////////{)(\t(/tt/1~I}{-1\_^])1_+[{|(?"<1~>>+!+[}11)}[(1}]};^1\|~_1}{I:-1(I+)(|))|\\/////////\\////\\\/////\\\\\\\\\\\\\\/\//\\///\//||\////|)(//\\///){\/\(11|///({)//({[1\\\\\\\\\\|\/\\\/\//////////\\\\\\\\\//\\\\\///////\|\\\\//////\\///\
tttt/////////////\///////\||///////t//|(|)|}|\/(\\(//(l_{{<i!l}}})(()\/\///{{|||\\|\\((\\\\\\\\\\\////\\\\\//\\/\////\///\\\\\\\\\//\\\\/\|}[{|1?{|[i:,,;i<i,,,I~}}<<l^":I,`.':-<l!~l~i:!;,l)\\/\//\/\\\/////\\\//\\////\\\\\\\\//\\\/\\///\\/\\\\\\\\\\\\\\\\//
//t//////////////\///////////////////////\//////////////////\/\/////\\\\\\\//\\|||\\/\\\\\\\\\\\\\/\\\\\\\\\\//\\/////////\\\\|))([+)}!<~"^,^.`' .^."~, :` '" `_>. ... ">+<^'I!: ^<(\\1}1//\\\//////////\\///\/\///\\\\\\//\\//\\\\\\\\\\\\\\///\(/\{
t////////////////////////////////////////////////////////\/\\///////\\\\\\\\\\\\\\\/\\\\\\||\|\\\\\|\\\\/\\|\\\\\\////((|///}!:,":,^`. .;' ' '^..':. ^!;. .^^ '^^`. '' ...I[{!>:^;_i:'~\ttt/////tt//\\////////////\\\\/\\\\\\/\\\\/\\\\\)}-+[+I??i
ttt////////////////////////////////////////////////////////\\//\//\\/\\\\\\\\\\//\\\\\\||\\\/\//\\\\\\\\\/\|\\\////\1;``^;<>+!">__+I `' .. "'. .;" ;;. .:^ ``,,;'` .;]I ,-_-|\////t////t///////\/\\\\//\\\\\\//\\\///////-II<!l_; I
/////////////////////////////////////////////////\\/\\\\\////\\//\\/////\///////////tt//\\/\////\/////t/\//\\///\]{\l,`'+< ,i ^i" . .`"l" . .`;?-' .` `>1ttt///tttt/////////\/\/////\\\\////t|+<}?!-]l<{[[1-+]
t//////////////////////////////////////\/////////\////////////////\//////\//tttttttttt//////////////////////////)_)t)|}1f/{<.^,^:~: . .. '''^:-|/> '-/}-_?\/)-{?(//\(\tt////\///\\\\\\\///t1.;); .l~` '"
///////////////////////////////////////////tt/t(|tt//]+{t\{][|////\//////////ttttt///t//t/////////////\//////|//{[|f}!l<!!I, `' ,!!i+- .`:l;IIll>>~++~<<<!:`. '' '^-l `.,l:.`{{[_:]/1l;>\//]l~?])tt//\\\\\/\\///\\|?<_}["^!;I^;]:. .
////////////////////////////////tttt/|{[1)]~!!+>!<_(/|[-<"i!l,]tt//ttt/t////ttt//t///ttttttt////////t//t//ttt){+. :?^ '. l_-!+l;;;|!!>~~il!lllllllllll!!lI:`'. .' :I;]_}>,?tf:.+fft)l+1//\~`'I-(//\/t/|/(-1[)/?>>II:' '.`';-'`
/////////////////////////t//()\1_<>il^'''' ,!>;.,.'{tti `~tf(`'-(|fffftttttttttt/tttttttttttt///tttft//(t|]?-+!^ ."`. `. ;!I,. .?{il-\_!~<>>!lII;IllIIIIIllllllllllI;;:,:,' '"^`(f{+{>' .<{t(I!}/||t> ^(//}>;:1\]: "[:"` ^<: . II.'..
///////////////ttt//tt((-!+}"'^. I, ,?<:' ,:;!>~',!_~{}-1]`^!}_+\ttttttt/tttttttfff/tt\(||]-?+;,:"l" '..'.. ?]l:" -(lI;,~?~!IIIlllI:IIlI;IIIIlllllllIIIIIllII!; . . '^^;~), "~!}\/t//\\/_. '</||1?-)/\\)+_1>". '_i !i'''
tt//t///ttt///(]<>l>][l"'.`,. ^.^. ii ;; ~>>>. .i~I'^^<}), .;|tfftttttttttttf\]}t-!,,I` .^ '. !: . .",I;. ^,I<)/-l:;llllllI;lIll;;IIIIlllIllIlIIlIllI;><. ' .;}". '.:+](ft\}(t/t{;<\{l^>}!^l\/{>1/t(lI:I!+<<". ':"
t//tttt|?+!I!:' '` .`. ...... `^ "<^.;`^"'`,!".,^^^.,?)!. [f/+>(/tttft\tff|+^,!' '^: >[,++:`' .I^ . _?!:^. ;~{/<II:IllIlllI;l;IlI;;IIIIIIlIIIIIIllIlllI+- "+;,...<\;^_(/~}t/(+ ^(/?.:|)il)\>?//)! __::. ':. '.
t/\}[{]",il'`!-<-]:`'^` .. '' .^+:'. .^'"i:`^. ';`:<_|>'.?/t/!"<)ffftf)]]!'II.,l ^' ''. '";" .' `Il, ;]>]j_;lI;ll;!!llIII>~IIIIII;:<iIlIIlII;llllll;(> ;,~.',.<:`, 'I_|\; .i|/]^ ?(}\/////\i' '' ....'^
tf1<}i `^. `I` .I?"'. . . ^' .^' .'` .". >}_.I|t{_(tf({~,~(); ')t};.><,. .. . .. . . .]}^{j1IlllIlI!1IlllII?{IIIIIlI;[1!I;IIllIIlIIllI<x" '.!+~!,``...:. 'l]?l"i{/\><]_;+/t\(|\/1,' "` ...
)+::((:^' ll .,` . . ..'. ' :+'`{tj{,l: ^;"..;!"^.I?' '~; .` .'. . `1+ [x?-:lIlll!]r-IllI~~{~I>lllll[i\--+;;I~IIIII!l;x] "I"-<<_> >i.' l{}:itf/}[/\)(\}))|(:^^..'. `".
>.._f|i.:l,;^^''__. .^' `' "+<!~)?,,[>,`]1i`!1_. ^l: .". .` '1I +JIt!IIlll;]\<vlIlI(;I\i+<iII>) >1}c(_i(!IllI_l;(f. ,_";~~+^ .. .;-i '+([i+: !//1](||/\(?:^..^^
i'"}_,.` ''^... '. `. ^<`_> .. +x??_~]:[|!,.ll` . {+ ;Y[^|,>~IlIIf\ {/;I!\ [[-'<+l-{ _??]f\n]lllI[!;1v` `+"]-}]~" ..'`l, ''i-` l+?\\\/t{!)t[:' .^^
;,:: :,^..;:. . i+..;^ `_<!,!~II,`. '' . "t..\n^!]!i]<II!n> ]]<-?l``-]' I>]?+<l~<!._n_IllI1+i}J: ...l!,'`' `. . .~>. ^-|\\_I?]{t/?` ....
^(\]I^~?;."!" . .. . ^<^ :( >t) _[il>|+:(U<1nYQ0Xx\> . .~xcXXYzx(n?IllI}">xCI .:1]_-" . .^. `}>!}((1-^,+?" ..
1+,~I.<! .`' .. `x"(+1 >(l<?_1}Cj!f\?l"' '"I~]|_;Ill{ vQ" ':,~_' .. .. .`^[(`,?\||\+ '," .
}<. `^]!.;lI>' . ... ~|r:;`.+I?\};+t) "".-?;lI>(;]xn. '_>]!+. '^'`l:11l[|((+?: .
[-`.':;..""' lv|. .:_(;I!u> ^,",^. .;?I]?IlI}n[</( "+'.. .I]; ',;!\/\|~I}" '";'.
.,><I>^ ')(I' `tlII>x1" <}1{)l "~+ |[II;\[:~zl .i;. .... `"i\\}]..!' ^....
-" ` ' . ":` .|+<<;!\U)>^ '^`' ^"I?)c-;<if]>j/ <1I~;` .!}\(: .;"`'
' 'l, .' ~[><+;!f()nn|]!:' ..^:!+1fcjx}}v!_)})>|n` ~^ ^;"'` .<+I<)/||\i'"<"'^ `.
II ^}_'+_!fI?_/-jJjUr\\ucJJz\|J>}?-j{]^ni<z; : `' .-+~->" .;](),.;-<`' .^ .^'
+[" +]{.`i;I</-jmvxjj)<^ !0~l-?+` : '" '` .' l\)-++{f\" ',^^^ .^ ,!` ''
.>; ::."!??l.^Ywj}<, (n, ,~_:` .,, ` `` ' '... ^+-l,]}]}\j/!. . ` 'I<~`'{tl..^`
'' .,<{[>" i/i" `-[; ,<_[>^i_l,:^_! ',+l.. ^,:,,. ;~>l;^ l> ',;I^???~,'l".. ..
;{?l. !+ .. .<1i '^' "}|{:-+-;?\[)-] ^:l1-:. '' '`, . ';.`~^ '. ..^`.
'i+;]}!,. <))\!<|ji >((_}}?t)}\\v|]?jI!), "lf!l. ... .^ . ".
.+{>` l/z\!,>""I+~_){]<t~-(! .!nt_]]]])1)]]\( ?_' :( . . `:. `; ." ... ..
Ir. ..>vQjut_~~>>>_-<]<-)f":l_v){\/1}}}{t/\0?z~. ^' `-l . . .. .
l\ :_>>i:^+\)_-]!:>-+<?\Uzfjj-/mxt|[??/f)??}/mc1;. . . . ^' .
I1I`l?-}l>l'...`^;1!^ 'l>})l\n\Qt?]?]})1{][[(XC>^ ...
^-+^.i-((?!"`:>l<[<!tc_:;<+>~<]nQY+?????][{\cmO||l . ''..
'|: '^[{~)\_+++))1{uxnvt(t){{[[u0\1|({1()){-?|xfc: ..
.<-, ]-]]]})11)){{{{}{{)|{}}{1{111{11{{}}]_!"x\]Xf
,]<\}][[[[[[[[[[[[[[[]][[[[[[[[][[[[[[]?-+!YC{z} .`
"_[}?]][[[[[[[[[[[[[[[[[[][[[[[[[[[[[[[[[]vn\?. ^
^. ;{_(_??][[[[[[[[[[[[[[[[[|[[[[[[[[[[[[[[[[v_(]^
.' '. :t>/?[[[[[[[[[[[[[[[[[[[]t\][[[[[[[[[[][]?u;()_ .. .'. . .
`. .` :)!j_]][[[]]]]][[[[[[[[[[}j(/{[[[[[][[}1{~n!)ft . .. .. . .. .
". ..' .` . "' .^":;~ti{\1]][]]]??]]]]][]]??[){[}[[[[?+}]<I"]({~c' .,",. .. ', .'`I' ^l.
. .. .'. ..'.... '.;,.'.":`,_: '` . '"^. '<:,'. 'l:`'. . ';"'. .i~I:;,,`.^!{[?[[[[[[[[[[[[[[]?]]]])t11])I;:. '^c^ .. '. . .' .^ .."?, '`
' '^. `. . I;. . `^ '... ...``'. ,">!^':``^``,`;I.!<>?>:??i:;-;,<_..^,Ii: 'l,i+```' ..'!; ''. ?~.'<+ .li!:,1?}[[[[[[[[[[[[[[[[[[[[)1}]t[:, "O" . .' . ^!' . ". i+' .
^. . .':`'`~/1,-<-~^'^'^^,`.."i_>^. `1t]!,^I]l^;`,I::_?]?[!:;`.`"'`l!l<1f{~>;]\1(]I>~l!l[<,,;`lI,~},^>!>l<l[-' I-+:i_!ll::l"`>'...'": 'x' ]l>i .:I1[~]]]]}}}}}[[}}[}}}}}[[]]]??1}}[}~;:>vx. :;..:??,.' ^` I;>.";:"^' .. ^'.^"" ' .!},' .
~!;:!".":i"^_/|]^li(\1;;it{' .[\fft+<}(/{}/)|f||<tj\/1//jj[<<~])11?''.`;\)l!(ff~!\j1-|\t\f\//||>'.^{/[)?!:(?+-,I+fjtil"'"+fj{i:',!!;!!^:.`r. r; !l'"i1?!i>~+_?][[[?-???]][[]?-+-]??+~<_{[_l?> ''^l;l`-}<`^.i>+l ``;I":+?!~-l ,>>l.'.;. ':!!(/!":,I
/t<I^^!{{-;[[-~|1\jj\?(/)f{~~+}}//t(?(ft}{tf]j1>1ffft+{jff/ttff)];)?1(/tt\/t/tfttttfftf/1\|t\|/?<_]_]{<_]/f({fffjttf/[i>1//|tft|" :<~:+}, ]>if" .:-~ >) ^`^l)f(_<??)\}}(t|}>{/\}-+1\()t-{j/]!:^'l<]\)+ ."_?I_{
ffft)|)(t[_-{tjjrjrj/{(||}(rjj\1)I<\((ffj/rjffttjffftrjfffrtfff/f[1jjffffftt//)}tttff/ttt[<{rj}tf1?<:~{/j)>)fttf|?)tfffftt1_;+tf1-1|~i1, >;:} '1_ ;( .. .. `:"_1{}tjtvj)vjr/|jfff/<(tf)+1/)1j)~~-[j[l|[(/\j{:-]]([}\t

View File

@ -1,29 +0,0 @@
package cc.sukazyo.cono.morny
import cc.sukazyo.messiva.appender.ConsoleAppender
import cc.sukazyo.messiva.formatter.SimpleFormatter
import cc.sukazyo.messiva.log.LogLevel
import cc.sukazyo.messiva.logger.Logger
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,11 +0,0 @@
package cc.sukazyo.cono.morny
import cc.sukazyo.restools.ResourcesPackage
object MornyAssets {
class AssetsException (caused: Throwable) extends Exception("Cannot read assets file.", caused)
val pack: ResourcesPackage = ResourcesPackage(MornyAssets.getClass, "assets_morny")
}

View File

@ -1,156 +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_SERVER_EXIT
import cc.sukazyo.cono.morny.bot.api.EventListenerManager
import cc.sukazyo.cono.morny.bot.event.{MornyEventListeners, MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock}
import cc.sukazyo.cono.morny.bot.query.MornyQueries
import com.pengrad.telegrambot.TelegramBot
import com.pengrad.telegrambot.request.GetMe
import scala.util.boundary
import scala.util.boundary.break
object MornyCoeur {
val THREAD_SERVER_EXIT = "system-exit"
}
class MornyCoeur (using val config: MornyConfig) {
given MornyCoeur = this
///>>> BLOCK START instance configure & startup stage 1
logger info "Coeur starting..."
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: LoginResult = login() match
case some: Some[LoginResult] => some.get
case None =>
logger error "Login to bot failed."
System exit -1
throw RuntimeException()
configure_exitCleanup()
///<<< BLOCK END instance configure & startup stage 1
/** [[TelegramBot]] account of this Morny */
val account: TelegramBot = __loginResult.account
/** [[account]]'s telegram username */
val username: String = __loginResult.username
/** [[account]]'s telegram user id */
val userid: Long = __loginResult.userid
/** current Morny's [[MornyTrusted]] instance */
val trusted: MornyTrusted = MornyTrusted()
val daemons: MornyDaemons = MornyDaemons()
//noinspection ScalaWeakerAccess
val eventManager: EventListenerManager = EventListenerManager()
eventManager register MornyOnUpdateTimestampOffsetLock()
val commands: MornyCommands = MornyCommands()
//noinspection ScalaWeakerAccess
val queries: MornyQueries = MornyQueries()
eventManager register MornyOnTelegramCommand(using commands)
eventManager register MornyOnInlineQuery(using queries)
//noinspection ScalaUnusedSymbol
val events: MornyEventListeners = MornyEventListeners(using eventManager)
/** inner value: about why morny exit, used in [[daemon.MornyReport]]. */
private var whileExit_reason: Option[AnyRef] = None
def exitReason: Option[AnyRef] = whileExit_reason
val coeurStartTimestamp: Long = ServerMain.systemStartupTime
///>>> BLOCK START instance configure & startup stage 2
daemons.start()
logger info "start telegram event listening"
account setUpdatesListener eventManager
if config.commandLoginRefresh then
logger info "resetting telegram command list"
commands.automaticTGListUpdate()
daemons.reporter.reportCoeurMornyLogin()
logger info "Coeur start complete."
///<<< BLOCK END instance configure & startup stage 2
def saveDataAll(): Unit = {
// nothing to do
logger info "done all save action."
}
private def exitCleanup (): Unit = {
daemons.reporter.reportCoeurExit()
account.shutdown()
logger info "stopped bot account"
daemons.stop()
if config.commandLogoutClear then
commands.automaticTGListRemove()
logger info "done exit cleanup"
}
private def configure_exitCleanup (): Unit = {
Runtime.getRuntime.addShutdownHook(new Thread(() => exitCleanup(), THREAD_SERVER_EXIT))
}
def exit (status: Int, reason: AnyRef): Unit =
whileExit_reason = Some(reason)
System exit status
private case class LoginResult(account: TelegramBot, username: String, userid: Long)
private def login (): Option[LoginResult] = {
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[Option[LoginResult]] {
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(Some(LoginResult(account, remote.username, remote.id)))
} catch
case r: boundary.Break[Option[LoginResult]] => throw r
case e =>
logger error
s"""${exceptionLog(e)}
|login failed"""
.stripMargin
}
None
}
}
}

View File

@ -1,185 +0,0 @@
package cc.sukazyo.cono.morny;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.annotation.*;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.Set;
public class MornyConfig {
/**
* 表示一个字段的值属于敏感数据不应该被执行打印等操作
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Sensitive {}
/* ======================================= *
* Config props Names Definition *
* ======================================= */
public static final String PROP_TOKEN_KEY_DEFAULT = "TELEGRAM_BOT_API_TOKEN";
public static final String PROP_TOKEN_MORNY_KEY = "MORNY_TG_TOKEN";
public static final String[] PROP_TOKEN_KEY = {PROP_TOKEN_KEY_DEFAULT, PROP_TOKEN_MORNY_KEY};
/* ======================================= *
* telegram bot login config *
* ======================================= */
/**
* Morny Telegram 使用的 API 服务器.
* <p>
* 不设定的话默认将会使用 {@code https://api.telegram.org/bot}
*/
@Nullable public final String telegramBotApiServer;
/**
* Morny Telegram 使用的 API 服务器的 file 服务路径.
* <p>
* 不设定的话默认将会使用 {@value com.pengrad.telegrambot.impl.FileApi#FILE_API}
*/
@Nullable public final String telegramBotApiServer4File;
/**
* morny 使用的 telegram bot bot api token.
* <p>
* 这个值必须设定
*/
@Nonnull @Sensitive public final String telegramBotKey;
/**
* morny 所使用的 bot username.
* <p>
* 如果设定了这个值则在 morny 登录 bot 时将会检查所登录的 bot username 是否和这里设定的 username 匹配
* 如果不匹配则会拒绝登录然后报错
* <p>
* 如果没有设定这个值则不会对登录 bot username 进行限制
*/
@Nullable public final String telegramBotUsername;
/* ======================================= *
* morny trusted config *
* ======================================= */
/**
* morny 的主人.
* <p>
* 这项值的对象总是会被{@link MornyTrusted 信任管理器}认为是可信任的
*/
public final long trustedMaster;
/**
* morny 可信群聊的 id.
* <p>
* {@link MornyTrusted 信任管理器}将会认为这个群聊中的所有拥有
* {@link com.pengrad.telegrambot.model.ChatMember.Status#administrator administrator} 权限的成员是可信任的
* <p>
* id 需要符合 bot api 标准
*/
public final long trustedChat;
/* ======================================= *
* system: event ignore *
* ======================================= */
public final boolean eventIgnoreOutdated;
/**
* morny 的事件忽略前缀时间<br>
* <br>
* {@link cc.sukazyo.cono.morny.bot.event.MornyOnUpdateTimestampOffsetLock}
* 会根据这里定义的时间戳取消掉比此时间更早的事件链
*/
public final long eventOutdatedTimestamp;
/* ======================================= *
* system: command list automation *
* ======================================= */
public final boolean commandLoginRefresh;
public final boolean commandLogoutClear;
/* ======================================= *
* system: morny report *
* ======================================= */
/**
* 控制 Morny Coeur 系统的报告的报告对象.
* @since 1.0.0-alpha5
*/
public final long reportToChat;
/* ======================================= *
* function: dinner query tool *
* ======================================= */
@Nonnull public final Set<Long> dinnerTrustedReaders;
public final long dinnerChatId;
/* ======================================= *
* function: medication timer *
* ======================================= */
public final long medicationNotifyToChat;
@Nonnull public final ZoneOffset medicationTimerUseTimezone;
@Nonnull public final Set<Integer> medicationNotifyAt;
/* ======================================= *
* End Configs | ConfigBuilder *
* ======================================= */
private MornyConfig (@Nonnull Prototype prototype) throws CheckFailure {
this.telegramBotApiServer = prototype.telegramBotApiServer;
this.telegramBotApiServer4File = prototype.telegramBotApiServer4File;
if (prototype.telegramBotKey == null) throw new CheckFailure.NullTelegramBotKey();
this.telegramBotKey = prototype.telegramBotKey;
this.telegramBotUsername = prototype.telegramBotUsername;
this.trustedMaster = prototype.trustedMaster;
this.trustedChat = prototype.trustedChat;
this.eventIgnoreOutdated = prototype.eventIgnoreOutdated;
if (prototype.eventOutdatedTimestamp < 1) throw new CheckFailure.UnsetEventOutdatedTimestamp();
this.eventOutdatedTimestamp = prototype.eventOutdatedTimestamp;
this.commandLoginRefresh = prototype.commandLoginRefresh;
this.commandLogoutClear = prototype.commandLogoutClear;
this.dinnerTrustedReaders = prototype.dinnerTrustedReaders;
this.dinnerChatId = prototype.dinnerChatId;
this.reportToChat = prototype.reportToChat;
this.medicationNotifyToChat = prototype.medicationNotifyToChat;
this.medicationTimerUseTimezone = prototype.medicationTimerUseTimezone;
prototype.medicationNotifyAt.forEach(i -> { if (i < 0 || i > 23) throw new CheckFailure.UnavailableTimeInMedicationNotifyAt(); });
this.medicationNotifyAt = prototype.medicationNotifyAt;
}
public static class CheckFailure extends RuntimeException {
public static class NullTelegramBotKey extends CheckFailure {}
public static class UnsetEventOutdatedTimestamp extends CheckFailure {}
public static class UnavailableTimeInMedicationNotifyAt extends CheckFailure {}
}
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;
@Nullable public String telegramBotUsername = null;
public long trustedMaster = -1L;
public long trustedChat = -1L;
public boolean eventIgnoreOutdated = false;
public long eventOutdatedTimestamp = -1;
public boolean commandLoginRefresh = false;
public boolean commandLogoutClear = false;
@Nonnull public final Set<Long> dinnerTrustedReaders = new HashSet<>();
public long dinnerChatId = -1L;
public long reportToChat = -1L;
public long medicationNotifyToChat = -1L;
@Nonnull public ZoneOffset medicationTimerUseTimezone = ZoneOffset.UTC;
@Nonnull public final Set<Integer> medicationNotifyAt = new HashSet<>();
}
}

View File

@ -1,48 +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.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>") // todo: will not implemented
"<calculation-error>"
}
}

View File

@ -1,24 +0,0 @@
package cc.sukazyo.cono.morny
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.{LimboChat, LimboUser}
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Chat.*
import cc.sukazyo.cono.morny.Log.logger
import com.pengrad.telegrambot.model.ChatMember.Status
import com.pengrad.telegrambot.TelegramBot
class MornyTrusted (using coeur: MornyCoeur)(using config: MornyConfig) {
if config.trustedMaster == -1 then
logger warn "You have not set your Morny's master.\n it may have some issues on controlling your bot."
def isTrusted (userId: Long): Boolean =
given TelegramBot = coeur.account
if userId == config.trustedMaster then true
else if config.trustedChat == -1 then false
else LimboChat(config.trustedChat) memberHasPermission(LimboUser(userId), 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(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,105 +0,0 @@
package cc.sukazyo.cono.morny.bot.api
import cc.sukazyo.cono.morny.{Log, MornyCoeur}
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException
import com.google.gson.GsonBuilder
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.UpdatesListener
import scala.collection.mutable
import scala.language.postfixOps
/** Contains a [[mutable.Queue]] of [[EventListener]], and delivery telegram [[Update]].
*
* Implemented [[process]] in [[UpdatesListener]] so it can directly used in [[com.pengrad.telegrambot.TelegramBot.setupListener]].
*
* @param coeur the [[MornyCoeur]] context.
*/
class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
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
coeur.daemons.reporter.exception(e, "on event running")
}
if (status isOk) return
}
}
}
import java.util
import scala.jdk.CollectionConverters.*
/** Delivery the telegram [[Update]]s.
*
* The implementation of [[UpdatesListener]].
*
* For each [[Update]], create an [[EventRunner]] for it, and
* start the it.
*
* @return [[UpdatesListener.CONFIRMED_UPDATES_ALL]], for all Updates
* should be processed in [[EventRunner]] created for it.
*/
override def process (updates: util.List[Update]): Int = {
for (update <- updates.asScala)
EventRunner(using update).start()
UpdatesListener.CONFIRMED_UPDATES_ALL
}
}

View File

@ -1,59 +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 cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import com.pengrad.telegrambot.model.{Chat, Update}
import com.pengrad.telegrambot.request.{DeleteMessage, GetChatMember, SendSticker}
import scala.language.postfixOps
class DirectMsgClear (using coeur: MornyCoeur) 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 != coeur.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 = coeur.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) {
coeur.account exec DeleteMessage(
event.message.chat.id, event.message.replyToMessage.messageId
)
def _isPrivate: Boolean = event.message.chat.`type` == Chat.Type.Private
def _isPermission: Boolean =
(coeur.account exec GetChatMember(event.message.chat.id, event.message.from.id))
.chatMember.canDeleteMessages
if (_isPrivate || _isPermission) {
coeur.account exec DeleteMessage(event.message.chat.id, event.message.messageId)
}
} else coeur.account exec SendSticker(
event.message.chat.id,
TelegramStickers ID_403
).replyToMessageId(event.message.messageId)
}
}

View File

@ -1,230 +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 cc.sukazyo.cono.morny.util.CommonEncrypt
import cc.sukazyo.cono.morny.util.CommonEncrypt.*
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
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`__. */
class Encryptor (using coeur: MornyCoeur) 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
coeur.account 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(
coeur.account getFileContent (coeur.account exec GetFile(_r.document.fileId)).file,
_r.document.fileName
)} catch case e: IOException =>
logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}"
coeur.daemons.reporter.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(
coeur.account getFileContent (coeur.account exec GetFile(_photo_origin.fileId)).file,
s"photo$rand_id.png"
)
} catch
case e: IOException =>
//noinspection DuplicatedCode
logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}"
coeur.daemons.reporter.exception(e, "NetworkRequest error: TelegramFileAPI")
return
case e: IllegalArgumentException =>
logger warn s"FileProcess error: PhotoSize:\n\t${e.getMessage}"
coeur.daemons.reporter.exception(e, "FileProcess error: PhotoSize")
return
} else if ((_r ne null) && (_r.text ne null)) {
XText(_r.text)
} else {
coeur.account 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 =>
coeur.account 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 _ =>
coeur.account exec SendSticker(
event.message.chat.id,
TelegramStickers ID_404
).replyToMessageId(event.message.messageId)
return;
// END BLOCK: encrypt
// output
result match
case _file: EXFile =>
coeur.account 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
coeur.account 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 =
coeur.account 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,57 +0,0 @@
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 cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.request.SendSticker
import scala.language.postfixOps
class EventHack (using coeur: MornyCoeur) 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 = {
import coeur.daemons.eventHack.{registerHack, HackType}
val x_mode = if (command.args nonEmpty) command.args(0) else ""
def done_ok =
coeur.account exec SendSticker(
event.message.chat.id,
TelegramStickers ID_WAITING
).replyToMessageId(event.message.messageId)
def done_forbiddenForAny =
coeur.account 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 (coeur.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,68 +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 cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.model.request.ParseMode
import com.pengrad.telegrambot.request.{GetChatMember, SendMessage}
import scala.language.postfixOps
class GetUsernameAndId (using coeur: MornyCoeur) 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)
coeur.account 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 =>
coeur.account 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 = coeur.account execute GetChatMember(event.message.chat.id, userId)
if (response.chatMember eq null)
coeur.account 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)
coeur.account exec SendMessage(
event.message.chat.id,
"<code>$__channel_identify</code>"
).replyToMessageId(event.message.messageId)
return;
coeur.account exec SendMessage(
event.message.chat.id,
TelegramUserInformation getFormattedInformation user
).replyToMessageId(event.message.messageId()).parseMode(ParseMode HTML)
}
}

View File

@ -1,44 +0,0 @@
package cc.sukazyo.cono.morny.bot.command
/** One alias definition, contains the necessary message of how
* to process the alias.
*/
trait ICommandAlias {
/** The alias name.
*
* same with the command name, it is the unique identifier of this alias.
*/
val name: String
/** If the alias should be listed while list commands to end-user.
*
* The alias can only be listed when the parent command can be listed
* (meanwhile the parent command implemented [[ITelegramCommand]]). If the
* parent command cannot be listed, it will always cannot be listed.
*/
val listed: Boolean
}
/** Default implementations of [[ICommandAlias]]. */
object ICommandAlias {
/** Alias which can be listed to end-user.
*
* the [[ICommandAlias.listed]] value is always true.
*
* @param name The alias name, see more in [[ICommandAlias.name]]
*/
case class ListedAlias (name: String) extends ICommandAlias:
override val listed = true
/** Alias which cannot be listed to end-user.
*
* the [[ICommandAlias.listed]] value is always false.
*
* @param name The alias name, see more in [[ICommandAlias.name]]
*/
case class HiddenAlias (name: String) extends ICommandAlias:
override val listed = false
}

View File

@ -1,78 +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 cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.model.request.ParseMode
import com.pengrad.telegrambot.request.SendMessage
import scala.language.postfixOps
class IP186Query (using coeur: MornyCoeur) {
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)
coeur.account exec SendMessage(
event.message.chat.id,
"[Unavailable] Too much arguments."
).replyToMessageId(event.message.messageId)
return
else command.args(0)
if (target eq null)
coeur.account 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}")
coeur.account 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 =>
coeur.account exec new SendMessage(
event.message().chat().id(),
s"""[Exception] in query:
|<code>${h(e.getMessage)}</code>"""
.stripMargin
).parseMode(ParseMode.HTML).replyToMessageId(event.message().messageId())
}
}

View File

@ -1,39 +0,0 @@
package cc.sukazyo.cono.morny.bot.command
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import com.pengrad.telegrambot.model.Update
/** A simple command.
*
* Contains only [[name]] and [[aliases]].
*
* Won't be listed to end-user. if you want the command listed,
* see [[ITelegramCommand]].
*
*/
trait ISimpleCommand {
/** the main name of the command.
*
* must have a value as the unique identifier of this command.
*/
val name: String
/** aliases of the command.
*
* Alias means it is the same to call [[name main name]] when call this.
* There can be multiple aliases. But notice that, although alias is not
* the unique identifier, it uses the same namespace with [[name]], means
* it also cannot be duplicate with other [[name]] or [[aliases]].
*
* It can be [[Null]], means no aliases.
*/
val aliases: Array[ICommandAlias]|Null
/** The work code of this command.
*
* @param command The parsed input command which called this command.
* @param event The raw event which called this command.
*/
def execute (using command: InputCommand, event: Update): Unit
}

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