mirror of
https://github.com/Eyre-S/Coeur-Morny-Cono.git
synced 2024-11-23 03:27:39 +08:00
Compare commits
2 Commits
ddfe77350e
...
6e82227447
Author | SHA1 | Date | |
---|---|---|---|
6e82227447 | |||
a8b7562b51 |
@ -4,8 +4,6 @@
|
||||
.vscode/
|
||||
.gradle/
|
||||
.settings/
|
||||
/src/test/java/test/*
|
||||
/src/test/resources/test/*
|
||||
|
||||
#build
|
||||
/build/
|
||||
|
928
.editorconfig
928
.editorconfig
File diff suppressed because it is too large
Load Diff
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,12 +4,11 @@
|
||||
.vscode/
|
||||
.gradle/
|
||||
.settings/
|
||||
/src/test/java/test/*
|
||||
/src/test/resources/test/*
|
||||
|
||||
#build
|
||||
/build/
|
||||
/bin/
|
||||
/out/
|
||||
.metals/
|
||||
.bloop/
|
||||
.project
|
||||
|
21
build.gradle
21
build.gradle
@ -3,6 +3,7 @@ plugins {
|
||||
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'
|
||||
@ -84,8 +85,10 @@ dependencies {
|
||||
implementation "com.squareup.okhttp3:okhttp:${lib_okhttp_v}"
|
||||
implementation "com.google.code.gson:gson:${lib_gson_v}"
|
||||
|
||||
testImplementation platform("org.junit:junit-bom:${lib_junit_v}")
|
||||
testImplementation "org.junit.jupiter:junit-jupiter"
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
@ -108,14 +111,18 @@ tasks.withType(ScalaCompile).configureEach {
|
||||
|
||||
scalaCompileOptions.additionalParameters.add "-language:postfixOps"
|
||||
// scalaCompileOptions.additionalParameters.add("-Yexplicit-nulls")
|
||||
// scalaCompileOptions.additionalParameters.add "-language:experimental.saferExceptions"
|
||||
|
||||
}
|
||||
|
||||
tasks.withType(Javadoc).configureEach {
|
||||
options.encoding = proj_file_encoding.name()
|
||||
}
|
||||
|
||||
//tasks.withType(ScalaDoc).configureEach {
|
||||
//}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed"
|
||||
}
|
||||
}
|
||||
|
||||
application {
|
||||
@ -152,7 +159,7 @@ shadowJar {
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
@SuppressWarnings('GrMethodMayBeStatic')
|
||||
boolean isCleanBuild () {
|
||||
if (grgit == null) return false
|
||||
Set<String> changes = grgit.status().unstaged.allChanges + grgit.status().staged.allChanges
|
||||
|
@ -5,16 +5,17 @@ MORNY_ARCHIVE_NAME = morny-coeur
|
||||
MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono
|
||||
MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s
|
||||
|
||||
VERSION = 1.0.0-RC4
|
||||
VERSION = 1.0.0-RC5
|
||||
|
||||
USE_DELTA = true
|
||||
VERSION_DELTA = scalaport3
|
||||
VERSION_DELTA = scalaport4
|
||||
|
||||
CODENAME = beiping
|
||||
|
||||
# dependencies
|
||||
|
||||
lib_spotbugs_v = 4.7.3
|
||||
lib_scalamodule_xml_v = 2.2.0
|
||||
|
||||
lib_messiva_v = 0.1.1
|
||||
lib_resourcetools_v = 0.2.2
|
||||
@ -24,4 +25,4 @@ lib_javatelegramapi_v = 6.2.0
|
||||
lib_okhttp_v = 4.11.0
|
||||
lib_gson_v = 2.10.1
|
||||
|
||||
lib_junit_v = 5.10.0
|
||||
lib_scalatest_v = 3.2.17
|
||||
|
@ -1,98 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util;
|
||||
|
||||
import javax.annotation.Nonnegative;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class BiliTool {
|
||||
|
||||
private static final long V_CONV_XOR = 177451812L;
|
||||
private static final long V_CONV_ADD = 8728348608L;
|
||||
private static final char[] BV_TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF".toCharArray();
|
||||
private static final int TABLE_INT = BV_TABLE.length;
|
||||
private static final Map<Character, Integer> BV_TABLE_REVERSED = new HashMap<>();
|
||||
static { for (int i = 0; i < BV_TABLE.length; i++) BV_TABLE_REVERSED.put(BV_TABLE[i], i); }
|
||||
private static final char[] BV_TEMPLATE = "1 4 1 7 ".toCharArray();
|
||||
private static final int[] BV_TEMPLATE_FILTER = new int[]{9, 8, 1, 6, 2, 4};
|
||||
|
||||
public static class IllegalFormatException extends RuntimeException {
|
||||
|
||||
private IllegalFormatException (String bv, String reason) {
|
||||
super("`%s` is not a valid 10 digits base58 BV id: %s".formatted(bv, reason));
|
||||
}
|
||||
|
||||
private IllegalFormatException (String bv, int length) {
|
||||
this(bv, "length is %d.".formatted(length));
|
||||
}
|
||||
|
||||
private IllegalFormatException (String bv, char c, int location) {
|
||||
this(bv, "char `%s` is not in base58 char table (in position %d)".formatted(c, location));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a <a href="https://www.bilibili.com/">Bilibili</a> AV video id format to BV id format.
|
||||
* <p>
|
||||
* the AV id is a number; the BV id is a special base58 number, it shows as String in programming.<br>
|
||||
* eg:<br>
|
||||
* while the link <i>{@code https://www.bilibili.com/video/BV17x411w7KC/}</i>
|
||||
* shows the same with <i>{@code https://www.bilibili.com/video/av170001/}</i>,
|
||||
* the AV id is <u>{@code 170001}</u>, the BV id is <u>{@code 17x411w7KC}</u>
|
||||
* <p>
|
||||
* for now , the BV id has 10 digits.
|
||||
* the method <b>available while the <u>av-id < 2^27</u></b>, while it theoretically available when the av-id < 2^30.
|
||||
* <p>
|
||||
* this method allows input only 10 digits base58 BV id, if the input is not formatted by this method, it will throw
|
||||
* an Exception.
|
||||
*
|
||||
* @see <a href="https://www.zhihu.com/question/381784377/answer/1099438784">mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」?</a>
|
||||
*
|
||||
* @param bv the BV id, a string in (a special) base58 number format, <b>without "BV" prefix</b>.
|
||||
* @return the AV id corresponding to this bv id in <a href="https://www.bilibili.com/">Bilibili</a>, formatted as a number.
|
||||
* @throws IllegalFormatException if the input BV id is not the 10 digits base58 String.
|
||||
*/
|
||||
@Nonnegative
|
||||
public static long toAv (@Nonnull String bv) throws IllegalFormatException {
|
||||
long av = 0;
|
||||
if (bv.length() != 10)
|
||||
throw new IllegalFormatException(bv, bv.length());
|
||||
for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) {
|
||||
final Integer tableToken = BV_TABLE_REVERSED.get(bv.charAt(BV_TEMPLATE_FILTER[i]));
|
||||
if (tableToken == null)
|
||||
throw new IllegalFormatException(bv, bv.charAt(BV_TEMPLATE_FILTER[i]), BV_TEMPLATE_FILTER[i]);
|
||||
av += tableToken * Math.pow(TABLE_INT,i);
|
||||
}
|
||||
return (av-V_CONV_ADD)^V_CONV_XOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a <a href="https://www.bilibili.com/">Bilibili</a> BV video id format to AV id format.
|
||||
* <p>
|
||||
* the AV id is a number; the BV id is a special base58 number, it shows as String in programming.<br>
|
||||
* eg:<br>
|
||||
* while the link <i>{@code https://www.bilibili.com/video/BV17x411w7KC/}</i>
|
||||
* shows the same with <i>{@code https://www.bilibili.com/video/av170001/}</i>,
|
||||
* the AV id is <u>{@code 170001}</u>, the BV id is <u>{@code 17x411w7KC}</u>
|
||||
* <p>
|
||||
* for now , the BV id has 10 digits.
|
||||
* the method <b>available while the <u>av-id < 2^27</u></b>, while it theoretically available when the av-id < 2^30.
|
||||
*
|
||||
* @see <a href="https://www.zhihu.com/question/381784377/answer/1099438784">mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」?</a>
|
||||
*
|
||||
* @param av the (base10) AV id.
|
||||
* @return the AV id corresponding to this bv id in <a href="https://www.bilibili.com/">Bilibili</a>,
|
||||
* as a (special) base 58 number format <b>without "BV" prefix</b>.
|
||||
*/
|
||||
@Nonnull
|
||||
public static String toBv (@Nonnegative long av) {
|
||||
av = (av^V_CONV_XOR)+V_CONV_ADD;
|
||||
final char[] bv = BV_TEMPLATE.clone();
|
||||
for (int i = 0; i < BV_TEMPLATE_FILTER.length; i++) {
|
||||
bv[BV_TEMPLATE_FILTER[i]] = BV_TABLE[(int)(Math.floor(av/(Math.pow(TABLE_INT, i)))%TABLE_INT)];
|
||||
}
|
||||
return String.copyValueOf(bv);
|
||||
}
|
||||
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util;
|
||||
|
||||
import javax.annotation.Nonnegative;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public class CommonRandom {
|
||||
|
||||
/**
|
||||
* 通过 {@link ThreadLocalRandom} 以指定的一定几率返回 true.
|
||||
* @param probability 一个正整数,决定在样本空间中有多大的可能性为 true。
|
||||
* @param base 一个正整数,决定样本空间有多大。
|
||||
* @return 有 {@code base} 分之 {@code probability} 的几率,返回值为 {@link true}.
|
||||
* 如果 {@code probability} 大于 {@code base},也就是为 true 的可能性大于 100%,则会永远为 true。
|
||||
* @throws IllegalArgumentException
|
||||
* 当参数 base 或是 probability 不为正整数时
|
||||
* @since 1.0.0-RC3.2
|
||||
*/
|
||||
public static boolean probabilityTrue (@Nonnegative int probability, @Nonnegative int base) {
|
||||
if (probability < 1) throw new IllegalArgumentException("the probability must be a positive value!");
|
||||
if (base < 1) throw new IllegalArgumentException("the probability base must be a positive value!");
|
||||
return probability > ThreadLocalRandom.current().nextInt(base);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以一定几率返回 true.
|
||||
* @return {@code probabilityIn} 分之 {@link 1} 的几率为 {@link true}.
|
||||
* @see #probabilityTrue(int, int)
|
||||
* @since 1.0.0-RC3.2
|
||||
*/
|
||||
public static boolean probabilityTrue (@Nonnegative int probabilityIn) {
|
||||
return (probabilityTrue(1, probabilityIn));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 {@link ThreadLocalRandom} 实现的随机 boolean 取值.
|
||||
* @return 随机的 {@link true} 或 {@link false},各占(近似)一半可能性.
|
||||
* @see ThreadLocalRandom#nextBoolean()
|
||||
* @since 1.0.0-RC3.2
|
||||
*/
|
||||
public static boolean iif () {
|
||||
return ThreadLocalRandom.current().nextBoolean();
|
||||
}
|
||||
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
|
||||
public class OkHttpPublic {
|
||||
|
||||
public static class MediaTypes {
|
||||
|
||||
public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class UniversalCommand {
|
||||
|
||||
@Nonnull
|
||||
public static String[] format (@Nonnull String com) {
|
||||
|
||||
final ArrayList<String> arr = new ArrayList<>();
|
||||
|
||||
final StringBuilder tmp = new StringBuilder();
|
||||
final char[] coma = com.toCharArray();
|
||||
for (int i = 0; i < coma.length; i++) {
|
||||
if (coma[i] == ' ') {
|
||||
if (!tmp.toString().equals("")) { arr.add(tmp.toString()); }
|
||||
tmp.setLength(0);
|
||||
} else if (coma[i] == '"') {
|
||||
while (true) {
|
||||
i++;
|
||||
if (i >= coma.length) {
|
||||
break;
|
||||
} else if (coma[i] == '"') {
|
||||
break;
|
||||
} else if (coma[i] == '\\' && i+1 < coma.length && (coma[i+1] == '"' || coma[i+1] == '\\')) {
|
||||
i++;
|
||||
tmp.append(coma[i]);
|
||||
} else {
|
||||
tmp.append(coma[i]);
|
||||
}
|
||||
}
|
||||
} else if (coma[i] == '\\' && i+1 < coma.length && (coma[i+1] == ' ' || coma[i+1] == '"' || coma[i+1] == '\\')) {
|
||||
i++;
|
||||
tmp.append(coma[i]);
|
||||
} else {
|
||||
tmp.append(coma[i]);
|
||||
}
|
||||
}
|
||||
if (!tmp.toString().equals("")) { arr.add(tmp.toString()); }
|
||||
tmp.setLength(0);
|
||||
|
||||
final String[] out = new String[arr.size()];
|
||||
arr.toArray(out);
|
||||
return out;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi;
|
||||
|
||||
public class Standardize {
|
||||
|
||||
public static final int CHANNEL_SPEAKER_MAGIC_ID = 136817688;
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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("&", "&");
|
||||
raw = raw.replaceAll("<", "<");
|
||||
raw = raw.replaceAll(">", ">");
|
||||
return raw;
|
||||
}
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi.formatting;
|
||||
|
||||
import com.pengrad.telegrambot.model.Chat;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class TGToStringFromChat {
|
||||
|
||||
|
||||
public static final long MASK_BOTAPI_ID = -1000000000000L;
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getSafeName () {
|
||||
if (data.type() == Chat.Type.Private)
|
||||
return data.firstName() + (data.lastName()==null ? "" : " "+data.lastName());
|
||||
else return data.title();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSafeLinkHTML () {
|
||||
if (data.username() == null) {
|
||||
if (data.type() == Chat.Type.Private)
|
||||
// language=html
|
||||
return String.format("<a href='tg://user?id=%d'>@[u:%d]</a>", data.id(), data.id());
|
||||
// language=html
|
||||
else return String.format("<a href='https://t.me/c/%d'>@[c/%d]</a>", id_tdLib(), id_tdLib());
|
||||
} else return "@"+data.username();
|
||||
}
|
||||
|
||||
public long id_tdLib () {
|
||||
return data.id() < 0 ? Math.abs(data.id() - MASK_BOTAPI_ID) : data.id();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getTypeTag () {
|
||||
return switch (data.type()) {
|
||||
case Private -> "🔒";
|
||||
case group -> "💭";
|
||||
case supergroup -> "💬";
|
||||
case channel -> "📢";
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
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() + "]";
|
||||
}
|
||||
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -2,11 +2,11 @@ package cc.sukazyo.cono.morny
|
||||
|
||||
import cc.sukazyo.cono.morny.bot.command.MornyCommands
|
||||
import cc.sukazyo.cono.morny.daemon.MornyDaemons
|
||||
import cc.sukazyo.cono.morny.util.tgapi.ExtraAction
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.MornyCoeur.THREAD_MORNY_EXIT
|
||||
import cc.sukazyo.cono.morny.bot.api.TelegramUpdatesListener
|
||||
import cc.sukazyo.cono.morny.bot.event.MornyEventListeners
|
||||
import cc.sukazyo.cono.morny.util.tgapi.ExtraAction
|
||||
import com.pengrad.telegrambot.TelegramBot
|
||||
import com.pengrad.telegrambot.request.GetMe
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
package cc.sukazyo.cono.morny
|
||||
|
||||
import cc.sukazyo.cono.morny.internal.BuildConfigField
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport
|
||||
import cc.sukazyo.cono.morny.util.FileUtils
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.URISyntaxException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport
|
||||
|
||||
object MornySystem {
|
||||
|
||||
@ -17,6 +17,7 @@ object MornySystem {
|
||||
@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
|
||||
|
@ -1,9 +1,9 @@
|
||||
package cc.sukazyo.cono.morny.bot.api
|
||||
|
||||
import cc.sukazyo.cono.morny.Log
|
||||
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport
|
||||
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
|
||||
@ -65,7 +65,7 @@ object EventListenerManager {
|
||||
case actionFailed: EventRuntimeException.ActionFailed =>
|
||||
errorMessage ++= "\ntg-api action: response track: "
|
||||
errorMessage ++= (GsonBuilder().setPrettyPrinting().create().toJson(
|
||||
actionFailed.getResponse
|
||||
actionFailed.response
|
||||
) indent 4) ++= "\n"
|
||||
case _ =>
|
||||
logger error errorMessage.toString
|
||||
|
@ -25,6 +25,10 @@ object DirectMsgClear extends ISimpleCommand {
|
||||
logger trace "message is not outdated(48 hrs ago)"
|
||||
|
||||
val isTrusted = MornyCoeur.trusted isTrusted event.message.from.id
|
||||
// todo:
|
||||
// it does not work. due to the Telegram Bot API doesn't provide
|
||||
// nested replyToMessage, so currently the trusted check by
|
||||
// replyToMessage.replyToMessage will not work!
|
||||
def _isReplyTrusted: Boolean =
|
||||
if (event.message.replyToMessage.replyToMessage == null) false
|
||||
else if (event.message.replyToMessage.replyToMessage.from.id == event.message.from.id) true
|
||||
|
@ -4,10 +4,10 @@ import cc.sukazyo.cono.morny.Log.logger
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport
|
||||
import cc.sukazyo.cono.morny.data.TelegramStickers
|
||||
import cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex
|
||||
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.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
|
||||
import com.pengrad.telegrambot.model.{PhotoSize, Update}
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.request.{GetFile, SendDocument, SendMessage, SendSticker}
|
||||
@ -16,6 +16,7 @@ import java.io.IOException
|
||||
import java.util.Base64
|
||||
import scala.language.postfixOps
|
||||
|
||||
/** Provides Telegram Command __`/encrypt`__. */
|
||||
object Encryptor extends ITelegramCommand {
|
||||
|
||||
override val name: String = "encrypt"
|
||||
@ -25,12 +26,20 @@ object Encryptor extends ITelegramCommand {
|
||||
|
||||
override def execute (using command: InputCommand, event: Update): Unit = {
|
||||
|
||||
val args = command.getArgs
|
||||
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
|
||||
@ -46,13 +55,21 @@ object Encryptor extends ITelegramCommand {
|
||||
return
|
||||
} else false
|
||||
|
||||
trait XEncryptable { val asByteArray: Array[Byte] }
|
||||
case class XFile (data: Array[Byte], name: String) extends XEncryptable {
|
||||
// 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
|
||||
}
|
||||
case class XText (data: String) extends XEncryptable {
|
||||
/** 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)) {
|
||||
@ -73,9 +90,10 @@ object Encryptor extends ITelegramCommand {
|
||||
_photo_origin = size
|
||||
_photo_size = _size
|
||||
if (_photo_origin eq null) throw IllegalArgumentException("no photo from api.")
|
||||
import cc.sukazyo.cono.morny.util.UseRandom.rand_id
|
||||
XFile(
|
||||
MornyCoeur.account getFileContent (MornyCoeur.extra exec GetFile(_photo_origin.fileId)).file,
|
||||
s"photo${byteArrayToHex(hashMd5(System.currentTimeMillis toString)) substring 32-12 toUpperCase}.png"
|
||||
s"photo$rand_id.png"
|
||||
)
|
||||
} catch
|
||||
case e: IOException =>
|
||||
@ -95,19 +113,26 @@ object Encryptor extends ITelegramCommand {
|
||||
).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)
|
||||
case class EXText (result: String) extends EXTextLike { override val text:String = result }
|
||||
case class EXHash (result: String) extends EXTextLike { override val text:String = result }
|
||||
/** 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), ENCRYPT_STANDARD_CHARSET))
|
||||
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 = byteArrayToHex(processor(source asByteArray))
|
||||
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" =>
|
||||
@ -126,24 +151,26 @@ object Encryptor extends ITelegramCommand {
|
||||
try { genResult_encrypt(
|
||||
input,
|
||||
_tool_b64d.decode,
|
||||
CommonEncrypt.base64FilenameLint
|
||||
CommonEncrypt.lint_base64FileName
|
||||
) } catch case _: IllegalArgumentException =>
|
||||
MornyCoeur.extra exec SendSticker(
|
||||
event.message.chat.id,
|
||||
TelegramStickers ID_404 // todo: is here better erro notify?
|
||||
).replyToMessageId(event.message.messageId)
|
||||
return
|
||||
case "md5" => genResult_hash(input, hashMd5)
|
||||
case "sha1" => genResult_hash(input, hashSha1)
|
||||
case "sha256" => genResult_hash(input, hashSha256)
|
||||
case "sha512" => genResult_hash(input, hashSha512)
|
||||
case "md5" => genResult_hash(input, MD5)
|
||||
case "sha1" => genResult_hash(input, SHA1)
|
||||
case "sha256" => genResult_hash(input, SHA256)
|
||||
case "sha512" => genResult_hash(input, SHA512)
|
||||
case _ =>
|
||||
MornyCoeur.extra exec SendSticker(
|
||||
event.message.chat.id,
|
||||
TelegramStickers ID_404
|
||||
).replyToMessageId(event.message.messageId)
|
||||
return;
|
||||
// END BLOCK: encrypt
|
||||
|
||||
// output
|
||||
result match
|
||||
case _file: EXFile =>
|
||||
MornyCoeur.extra exec SendDocument(
|
||||
@ -151,7 +178,7 @@ object Encryptor extends ITelegramCommand {
|
||||
_file.result
|
||||
).fileName(_file.resultName).replyToMessageId(event.message.messageId)
|
||||
case _text: EXTextLike =>
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
event.message.chat.id,
|
||||
s"<pre><code>${h(_text.text)}</code></pre>"
|
||||
@ -159,6 +186,30 @@ object Encryptor extends ITelegramCommand {
|
||||
|
||||
}
|
||||
|
||||
/** echo help to a specific message in a specific chat.
|
||||
*
|
||||
* === the help message ===
|
||||
* The first paragraph lists available encrypt algorithms and its alias,
|
||||
* each line have one algorithm where the first name highlighted is the
|
||||
* main name and following is aliases separated with `,`.
|
||||
* with the separator `---`, the second paragraph lists available mods
|
||||
* for algorithms, displays with the same rule of algorithms, with an extra
|
||||
* italic text following describes its usage environment.
|
||||
*
|
||||
* when output to telegram just like:
|
||||
* <blockquote>
|
||||
* '''__base64__''', b64<br>
|
||||
* '''__base64url__''', base64u, b64u<br>
|
||||
* '''__base64decode__''', base64d, b64d<br>
|
||||
* '''__base64url-decode__''', base64ud, b64ud<br>
|
||||
* '''__sha1__'''<br>
|
||||
* '''__sha256__'''<br>
|
||||
* '''__sha512__'''<br>
|
||||
* '''__md5__'''<br>
|
||||
* ---<br>
|
||||
* '''__uppercase__''', upper, u ''(sha1/sha256/sha512/md5 only)''
|
||||
* </blockquote>
|
||||
*/
|
||||
private def echoHelp(chat: Long, replyTo: Int): Unit =
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
chat,
|
||||
|
@ -1,10 +1,10 @@
|
||||
package cc.sukazyo.cono.morny.bot.command
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle
|
||||
import cc.sukazyo.cono.morny.bot.event.OnEventHackHandle.{registerHack, HackType}
|
||||
import cc.sukazyo.cono.morny.data.TelegramStickers
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import OnEventHackHandle.{HackType, registerHack}
|
||||
import cc.sukazyo.cono.morny.data.TelegramStickers
|
||||
import com.pengrad.telegrambot.request.SendSticker
|
||||
|
||||
import scala.language.postfixOps
|
||||
@ -18,7 +18,7 @@ object EventHack extends ITelegramCommand {
|
||||
|
||||
override def execute (using command: InputCommand, event: Update): Unit = {
|
||||
|
||||
val x_mode = if (command.hasArgs) command.getArgs()(0) else ""
|
||||
val x_mode = if (command.args nonEmpty) command.args(0) else ""
|
||||
|
||||
def done_ok =
|
||||
MornyCoeur.extra exec SendSticker(
|
||||
|
@ -18,7 +18,7 @@ object GetUsernameAndId extends ITelegramCommand {
|
||||
|
||||
override def execute (using command: InputCommand, event: Update): Unit = {
|
||||
|
||||
val args = command.getArgs
|
||||
val args = command.args
|
||||
|
||||
if (args.length > 1)
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
@ -59,7 +59,7 @@ object GetUsernameAndId extends ITelegramCommand {
|
||||
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
event.message.chat.id,
|
||||
TelegramUserInformation informationOutputHTML user
|
||||
TelegramUserInformation getFormattedInformation user
|
||||
).replyToMessageId(event.message.messageId()).parseMode(ParseMode HTML)
|
||||
|
||||
}
|
||||
|
@ -31,15 +31,15 @@ object IP186Query {
|
||||
private def query (using event: Update, command: InputCommand): Unit = {
|
||||
|
||||
val target: String|Null =
|
||||
if (command.getArgs isEmpty)
|
||||
if (command.args isEmpty)
|
||||
if event.message.replyToMessage eq null then null else event.message.replyToMessage.text
|
||||
else if (command.getArgs.length > 1)
|
||||
else if (command.args.length > 1)
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
event.message.chat.id,
|
||||
"[Unavailable] Too much arguments."
|
||||
).replyToMessageId(event.message.messageId)
|
||||
return
|
||||
else command.getArgs()(0)
|
||||
else command.args(0)
|
||||
|
||||
if (target eq null)
|
||||
MornyCoeur.extra exec new SendMessage(
|
||||
@ -48,14 +48,15 @@ object IP186Query {
|
||||
).replyToMessageId(event.message.messageId)
|
||||
return;
|
||||
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
try {
|
||||
|
||||
val response = command.getCommand match
|
||||
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.getCommand}")
|
||||
case _ => throw IllegalArgumentException(s"Unknown 186-IP query method ${command.command}")
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
event.message.chat.id,
|
||||
s"""${h(response.url)}
|
||||
@ -64,7 +65,6 @@ object IP186Query {
|
||||
).parseMode(ParseMode HTML).replyToMessageId(event.message.messageId)
|
||||
|
||||
} catch case e: Exception =>
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
MornyCoeur.extra exec new SendMessage(
|
||||
event.message().chat().id(),
|
||||
s"""[Exception] in query:
|
||||
|
@ -1,9 +1,9 @@
|
||||
package cc.sukazyo.cono.morny.bot.command
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.data.TelegramStickers
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import com.pengrad.telegrambot.model.{BotCommand, DeleteMyCommands, Update}
|
||||
import com.pengrad.telegrambot.request.{SendSticker, SetMyCommands}
|
||||
|
||||
@ -60,14 +60,14 @@ object MornyCommands {
|
||||
)
|
||||
|
||||
def execute (using command: InputCommand, event: Update): Boolean = {
|
||||
if (commands contains command.getCommand)
|
||||
commands(command.getCommand) execute;
|
||||
if (commands contains command.command)
|
||||
commands(command.command) execute;
|
||||
true
|
||||
else nonCommandExecutable
|
||||
}
|
||||
|
||||
private def nonCommandExecutable (using command: InputCommand, event: Update): Boolean = {
|
||||
if command.getTarget eq null then false
|
||||
if command.target eq null then false
|
||||
else
|
||||
MornyCoeur.extra exec SendSticker(
|
||||
event.message.chat.id,
|
||||
|
@ -1,8 +1,8 @@
|
||||
package cc.sukazyo.cono.morny.bot.command
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.bot.command.ICommandAlias.ListedAlias
|
||||
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
|
||||
|
||||
|
@ -3,8 +3,8 @@ package cc.sukazyo.cono.morny.bot.command
|
||||
import cc.sukazyo.cono.morny.{BuildConfig, MornyAbout, MornyCoeur, MornySystem}
|
||||
import cc.sukazyo.cono.morny.data.{TelegramImages, TelegramStickers}
|
||||
import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration}
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.request.{SendMessage, SendPhoto, SendSticker}
|
||||
@ -14,6 +14,7 @@ import java.net.InetAddress
|
||||
import java.rmi.UnknownHostException
|
||||
import scala.language.postfixOps
|
||||
|
||||
// todo: maybe move some utils method outside
|
||||
object MornyInformation extends ITelegramCommand {
|
||||
|
||||
private case object Subs {
|
||||
@ -30,12 +31,12 @@ object MornyInformation extends ITelegramCommand {
|
||||
|
||||
override def execute (using command: InputCommand, event: Update): Unit = {
|
||||
|
||||
if (!command.hasArgs) {
|
||||
if (command.args isEmpty) {
|
||||
echoInfo(event.message.chat.id, event.message.messageId)
|
||||
return
|
||||
}
|
||||
|
||||
val action: String = command.getArgs()(0)
|
||||
val action: String = command.args(0)
|
||||
|
||||
action match {
|
||||
case s if s startsWith Subs.STICKERS => echoStickers
|
||||
@ -46,6 +47,7 @@ object MornyInformation extends ITelegramCommand {
|
||||
|
||||
}
|
||||
|
||||
//noinspection ScalaWeakerAccess
|
||||
def getVersionGitTagHTML: String = {
|
||||
if (!MornySystem.isGitBuild) return ""
|
||||
val g = StringBuilder()
|
||||
@ -61,11 +63,12 @@ object MornyInformation extends ITelegramCommand {
|
||||
val v = StringBuilder()
|
||||
v ++= s"<code>${MornySystem VERSION_BASE}</code>"
|
||||
if (MornySystem isUseDelta) v++=s"-δ<code>${MornySystem VERSION_DELTA}</code>"
|
||||
if (MornySystem isGitBuild) v++="+"++=getVersionGitTagHTML
|
||||
if (MornySystem isGitBuild) v++="+git."++=getVersionGitTagHTML
|
||||
v ++= s"*<code>${MornySystem.CODENAME toUpperCase}</code>"
|
||||
v toString
|
||||
}
|
||||
|
||||
//noinspection ScalaWeakerAccess
|
||||
def getRuntimeHostname: String|Null = {
|
||||
try InetAddress.getLocalHost.getHostName
|
||||
catch case _:UnknownHostException => null
|
||||
@ -94,13 +97,13 @@ object MornyInformation extends ITelegramCommand {
|
||||
|
||||
private def echoStickers (using command: InputCommand, event: Update): Unit = {
|
||||
val mid: String|Null =
|
||||
if (command.getArgs()(0) == Subs.STICKERS) {
|
||||
if (command.getArgs.length == 1) ""
|
||||
else if (command.getArgs.length == 2) command.getArgs()(1)
|
||||
if (command.args(0) == Subs.STICKERS) {
|
||||
if (command.args.length == 1) ""
|
||||
else if (command.args.length == 2) command.args(1)
|
||||
else null
|
||||
} else if (command.getArgs.length == 1) {
|
||||
if ((command.getArgs()(0) startsWith s"${Subs.STICKERS}.") || (command.getArgs()(0) startsWith s"${Subs.STICKERS}#")) {
|
||||
command.getArgs()(0) substring Subs.STICKERS.length+1
|
||||
} else if (command.args.length == 1) {
|
||||
if ((command.args(0) startsWith s"${Subs.STICKERS}.") || (command.args(0) startsWith s"${Subs.STICKERS}#")) {
|
||||
command.args(0) substring Subs.STICKERS.length+1
|
||||
} else null
|
||||
} else null
|
||||
if (mid == null) echo404
|
||||
@ -154,13 +157,13 @@ object MornyInformation extends ITelegramCommand {
|
||||
event.message.chat.id,
|
||||
/* language=html */
|
||||
s"""system:
|
||||
|- Morny <code>${h(if (getRuntimeHostname == null) "<unknown-host>" else getRuntimeHostname)}</code>
|
||||
|- <code>${h(if (getRuntimeHostname == null) "<unknown-host>" else getRuntimeHostname)}</code>
|
||||
|- <code>${h(sysprop("os.name"))}</code> <code>${h(sysprop("os.arch"))}</code> <code>${h(sysprop("os.version"))}</code>
|
||||
|java runtime:
|
||||
|- <code>${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))}</code>
|
||||
|- <code>${h(sysprop("java.vm.version"))}</code>
|
||||
|vm memory:
|
||||
|- <code>${Runtime.getRuntime.totalMemory/1024/1024}</code> / <code>${Runtime.getRuntime.maxMemory/1024/1024}</code>
|
||||
|- <code>${Runtime.getRuntime.totalMemory/1024/1024}</code> / <code>${Runtime.getRuntime.maxMemory/1024/1024}</code> MB
|
||||
|- <code>${Runtime.getRuntime.availableProcessors}</code> cores
|
||||
|coeur version:
|
||||
|- $getVersionAllFullTagHTML
|
||||
|
@ -1,4 +1,5 @@
|
||||
package cc.sukazyo.cono.morny.bot.command
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
package cc.sukazyo.cono.morny.bot.command
|
||||
import cc.sukazyo.cono.morny.bot.command.ICommandAlias.HiddenAlias
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.data.TelegramStickers
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.request.SendSticker
|
||||
|
||||
import scala.language.postfixOps
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
import cc.sukazyo.cono.morny.daemon.MornyReport
|
||||
|
||||
object MornyManagers {
|
||||
|
||||
@ -30,7 +30,7 @@ object MornyManagers {
|
||||
event.message.chat.id,
|
||||
TelegramStickers ID_EXIT
|
||||
).replyToMessageId(event.message.messageId)
|
||||
logger info s"Morny exited by user ${(TGToString as user) toStringLogTag}"
|
||||
logger info s"Morny exited by user ${user toLogTag}"
|
||||
MornyCoeur.exit(0, user)
|
||||
|
||||
} else {
|
||||
@ -39,7 +39,7 @@ object MornyManagers {
|
||||
event.message.chat.id,
|
||||
TelegramStickers ID_403
|
||||
).replyToMessageId(event.message.messageId)
|
||||
logger info s"403 exit caught from user ${(TGToString as user) toStringLogTag}"
|
||||
logger info s"403 exit caught from user ${user toLogTag}"
|
||||
MornyReport.unauthenticatedAction("/exit", user)
|
||||
|
||||
}
|
||||
@ -61,7 +61,7 @@ object MornyManagers {
|
||||
|
||||
if (MornyCoeur.trusted isTrusted user.id) {
|
||||
|
||||
logger info s"call save from command by ${(TGToString as user) toStringLogTag}"
|
||||
logger info s"call save from command by ${user toLogTag}"
|
||||
MornyCoeur.callSaveData()
|
||||
MornyCoeur.extra exec SendSticker(
|
||||
event.message.chat.id,
|
||||
@ -74,7 +74,7 @@ object MornyManagers {
|
||||
event.message.chat.id,
|
||||
TelegramStickers ID_403
|
||||
).replyToMessageId(event.message.messageId)
|
||||
logger info s"403 save caught from user ${(TGToString as user) toStringLogTag}"
|
||||
logger info s"403 save caught from user ${user toLogTag}"
|
||||
MornyReport.unauthenticatedAction("/save", user)
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
package cc.sukazyo.cono.morny.bot.command
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import cc.sukazyo.cono.morny.data.MornyJrrp
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
|
||||
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
|
||||
|
||||
@ -24,11 +24,11 @@ object MornyOldJrrp extends ITelegramCommand {
|
||||
case a if a > 30 => ";"
|
||||
case _ => "..."
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
event.message.chat.id,
|
||||
// language=html
|
||||
f"${(TGToString as user) fullnameRefHtml} 在(utc的)今天的运气指数是———— <code>$jrrp%.2f%%</code>${h(ending)}"
|
||||
f"${user.fullnameRefHTML} 在(utc的)今天的运气指数是———— <code>$jrrp%.2f%%</code> ${h(ending)}"
|
||||
).replyToMessageId(event.message.messageId).parseMode(ParseMode HTML)
|
||||
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ package cc.sukazyo.cono.morny.bot.command
|
||||
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.data.{NbnhhshQuery, TelegramStickers}
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.request.{SendMessage, SendSticker}
|
||||
@ -25,11 +25,10 @@ object Nbnhhsh extends ITelegramCommand {
|
||||
override def execute (using command: InputCommand, event: Update): Unit = {
|
||||
|
||||
val queryTarget: String|Null =
|
||||
import cc.sukazyo.cono.morny.util.CommonConvert.stringsConnecting
|
||||
if (event.message.replyToMessage != null && event.message.replyToMessage.text != null)
|
||||
if command.args nonEmpty then
|
||||
command.args mkString " "
|
||||
else if (event.message.replyToMessage != null && event.message.replyToMessage.text != null)
|
||||
event.message.replyToMessage.text
|
||||
else if command hasArgs then
|
||||
stringsConnecting(command.getArgs, " ", 0, command.getArgs.length-1)
|
||||
else null
|
||||
|
||||
if (queryTarget == null)
|
||||
|
@ -3,15 +3,15 @@ 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.request.ParseMode
|
||||
import com.pengrad.telegrambot.model.{Message, Update}
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.request.{SendMessage, SendSticker}
|
||||
|
||||
import javax.swing.text.html.HTML
|
||||
import scala.annotation.unused
|
||||
import scala.language.postfixOps
|
||||
|
||||
@SuppressWarnings(Array("NonAsciiCharacters"))
|
||||
//noinspection NonAsciiCharacters
|
||||
object 喵呜 {
|
||||
|
||||
object 抱抱 extends ISimpleCommand {
|
||||
@ -56,7 +56,7 @@ object 喵呜 {
|
||||
}
|
||||
|
||||
private def replyingSet (whileRec: String, whileNew: String)(using event: Update): Unit = {
|
||||
val isNew = event.message.replyToMessage == null;
|
||||
val isNew = event.message.replyToMessage == null
|
||||
val target = if (isNew) event.message else event.message.replyToMessage
|
||||
MornyCoeur.extra exec new SendMessage(
|
||||
event.message.chat.id,
|
||||
|
@ -2,10 +2,12 @@ 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.CommonRandom.probabilityTrue
|
||||
import cc.sukazyo.cono.morny.util.UseMath.over
|
||||
import cc.sukazyo.cono.morny.util.UseRandom.*
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.request.SendMessage
|
||||
|
||||
//noinspection NonAsciiCharacters
|
||||
object 私わね extends ISimpleCommand {
|
||||
|
||||
override val name: String = "me"
|
||||
@ -13,7 +15,7 @@ object 私わね extends ISimpleCommand {
|
||||
|
||||
override def execute (using command: InputCommand, event: Update): Unit = {
|
||||
|
||||
if (probabilityTrue(521)) {
|
||||
if ((1 over 521) chance_is true) {
|
||||
val text = "/打假"
|
||||
MornyCoeur.extra exec new SendMessage(
|
||||
event.message.chat.id,
|
||||
|
@ -3,9 +3,9 @@ package cc.sukazyo.cono.morny.bot.event
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener
|
||||
import cc.sukazyo.cono.morny.data.TelegramStickers
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
|
||||
import com.pengrad.telegrambot.model.{Chat, Message, Update, User}
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.request.{ForwardMessage, GetChat, SendMessage, SendSticker}
|
||||
|
||||
import scala.language.postfixOps
|
||||
@ -44,7 +44,7 @@ object OnCallMe extends EventListener {
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
me,
|
||||
s"""request $itemHTML
|
||||
|from ${(TGToString as user) fullnameRefHtml}${if extra == null then "" else "\n"+extra}"""
|
||||
|from ${user.fullnameRefHTML}${if extra == null then "" else "\n"+extra}"""
|
||||
.stripMargin
|
||||
).parseMode(ParseMode HTML)
|
||||
|
||||
@ -52,6 +52,8 @@ object OnCallMe extends EventListener {
|
||||
var isAllowed = false
|
||||
var lastDinnerData: Message|Null = null
|
||||
if (MornyCoeur.trusted isTrusted_dinnerReader req.from.id) {
|
||||
// todo: have issues
|
||||
// i dont want to test it anymore... it might be deprecated soon
|
||||
lastDinnerData = (MornyCoeur.extra exec GetChat(MornyCoeur.config.dinnerChatId)).chat.pinnedMessage
|
||||
val sendResp = MornyCoeur.extra exec ForwardMessage(
|
||||
req.from.id,
|
||||
@ -59,7 +61,7 @@ object OnCallMe extends EventListener {
|
||||
lastDinnerData.forwardFromMessageId
|
||||
)
|
||||
import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration}
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
def lastDinner_dateMillis: Long = lastDinnerData.forwardDate longValue;
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
req.from.id,
|
||||
|
@ -3,7 +3,6 @@ package cc.sukazyo.cono.morny.bot.event
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.data.TelegramStickers
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString
|
||||
import com.pengrad.telegrambot.model.{Chat, Message, MessageEntity, Update}
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.request.{GetChat, SendMessage, SendSticker}
|
||||
@ -108,11 +107,11 @@ object OnCallMsgSend extends EventListener {
|
||||
val targetChatResponse = MornyCoeur.account execute GetChat(messageToSend.targetId)
|
||||
if (targetChatResponse isOk) {
|
||||
def getChatDescriptionHTML (chat: Chat): String =
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
val _c = TGToString as chat
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
// language=html
|
||||
s"""<i><u>${h(chat.id toString)}</u>@${h(chat.`type`.name)}</i>${if (chat.`type` != Chat.Type.Private) ":::" else ""}
|
||||
|${_c getTypeTag} <b>${h(_c getSafeName)}</b> ${_c getSafeLinkHTML}"""
|
||||
|${chat.typeTag} <b>${h(chat.safe_name)}</b> ${chat.safe_linkHTML}"""
|
||||
.stripMargin
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
update.message.chat.id,
|
||||
|
@ -1,8 +1,6 @@
|
||||
package cc.sukazyo.cono.morny.bot.event
|
||||
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener
|
||||
|
||||
import scala.collection.mutable
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import com.google.gson.GsonBuilder
|
||||
@ -10,6 +8,7 @@ import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.request.SendMessage
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.language.postfixOps
|
||||
|
||||
object OnEventHackHandle extends EventListener {
|
||||
@ -39,7 +38,7 @@ object OnEventHackHandle extends EventListener {
|
||||
else if hackers contains "[[]]" then (hackers remove "[[]]")get
|
||||
else return false
|
||||
logger debug s"hacked event by $x"
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
x.from_chat,
|
||||
// language=html
|
||||
@ -53,9 +52,9 @@ object OnEventHackHandle extends EventListener {
|
||||
override def onEditedMessage (using update: Update): Boolean =
|
||||
onEventHacked(update.editedMessage.chat.id, update.editedMessage.from.id)
|
||||
override def onChannelPost (using update: Update): Boolean =
|
||||
onEventHacked(update.channelPost.chat.id, update.channelPost.from.id)
|
||||
onEventHacked(update.channelPost.chat.id, 0)
|
||||
override def onEditedChannelPost (using update: Update): Boolean =
|
||||
onEventHacked(update.editedChannelPost.chat.id, update.editedChannelPost.from.id)
|
||||
onEventHacked(update.editedChannelPost.chat.id, 0)
|
||||
override def onInlineQuery (using update: Update): Boolean =
|
||||
onEventHacked(0, update.inlineQuery.from.id)
|
||||
override def onChosenInlineResult (using update: Update): Boolean =
|
||||
|
@ -9,16 +9,23 @@ import scala.language.postfixOps
|
||||
|
||||
object OnQuestionMarkReply extends EventListener {
|
||||
|
||||
private def QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓')
|
||||
private val QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓')
|
||||
|
||||
def isAllMessageMark (using text: String): Boolean = {
|
||||
var isAll = true
|
||||
for (c <- text)
|
||||
if !(QUESTION_MARKS contains c) then isAll = false
|
||||
isAll
|
||||
}
|
||||
|
||||
override def onMessage (using event: Update): Boolean = {
|
||||
|
||||
if event.message.text eq null then return false
|
||||
|
||||
import cc.sukazyo.cono.morny.util.CommonRandom.probabilityTrue
|
||||
if !probabilityTrue(8) then return false
|
||||
for (c <- event.message.text toCharArray)
|
||||
if !(QUESTION_MARKS contains c) then return false
|
||||
import cc.sukazyo.cono.morny.util.UseMath.over
|
||||
import cc.sukazyo.cono.morny.util.UseRandom.chance_is
|
||||
if (1 over 8) chance_is false then return false
|
||||
if !isAllMessageMark(using event.message.text) then return false
|
||||
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
event.message.chat.id, event.message.text
|
||||
|
@ -1,11 +1,11 @@
|
||||
package cc.sukazyo.cono.morny.bot.event
|
||||
|
||||
import cc.sukazyo.cono.morny.bot.api.EventListener
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import com.pengrad.telegrambot.model.{Message, Update}
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.bot.command.MornyCommands
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import com.pengrad.telegrambot.model.{Message, Update}
|
||||
|
||||
object OnTelegramCommand extends EventListener {
|
||||
|
||||
@ -19,10 +19,10 @@ object OnTelegramCommand extends EventListener {
|
||||
|
||||
if !_isCommandMessage(update.message) then return false
|
||||
val inputCommand = InputCommand(update.message.text drop 1)
|
||||
if (!(inputCommand.getCommand matches "^\\w+$"))
|
||||
if (!(inputCommand.command matches "^\\w+$"))
|
||||
logger debug "not command"
|
||||
false
|
||||
else if ((inputCommand.getTarget ne null) && (inputCommand.getTarget ne MornyCoeur.username))
|
||||
else if ((inputCommand.target ne null) && (inputCommand.target != MornyCoeur.username))
|
||||
logger debug "not morny command"
|
||||
false
|
||||
else
|
||||
|
@ -9,21 +9,23 @@ import scala.language.postfixOps
|
||||
|
||||
object OnUserRandom extends EventListener {
|
||||
|
||||
private val USER_OR_QUERY = "(.+)(?:还是|or)(.+)"r
|
||||
private val USER_IF_QUERY = "(.+)[吗?|?]+$"r
|
||||
private val USER_OR_QUERY = "^(.+)(?:还是|or)(.+)$"r
|
||||
private val USER_IF_QUERY = "^(.+)(?:吗\\?|?|\\?|吗?)$"r
|
||||
|
||||
override def onMessage(using update: Update): Boolean = {
|
||||
|
||||
if update.message.text == null then return false
|
||||
if update.message.text startsWith "/" then return false
|
||||
if !(update.message.text startsWith "/") then return false
|
||||
|
||||
import cc.sukazyo.cono.morny.util.CommonRandom.iif
|
||||
import cc.sukazyo.cono.morny.util.UseRandom.rand_half
|
||||
val query = update.message.text substring 1
|
||||
val result: String|Null = query match
|
||||
case USER_OR_QUERY(_con1, _con2) =>
|
||||
if iif then _con1 else _con2
|
||||
if rand_half then _con1 else _con2
|
||||
case USER_IF_QUERY(_con) =>
|
||||
(if iif then "不" else "") + _con
|
||||
// for capability with [[OnQuestionMarkReply]]
|
||||
if OnQuestionMarkReply.isAllMessageMark(using _con) then return false
|
||||
(if rand_half then "不" else "") + _con
|
||||
case _ => null
|
||||
|
||||
if result == null then return false
|
||||
|
@ -2,9 +2,9 @@ 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.TelegramFormatter.*
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.UniversalCommand
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.request.SendMessage
|
||||
@ -22,14 +22,24 @@ object OnUserSlashAction extends EventListener {
|
||||
|
||||
if (text startsWith "/") {
|
||||
|
||||
val actions = UniversalCommand format text
|
||||
// there has to be some special conditions for DP7
|
||||
// due to I have left DP7, I closed those special
|
||||
// conditions.
|
||||
// that is 2022, May 28th
|
||||
// when one year goes, These code have rewrite with
|
||||
// scala, those commented code is removed permanently.
|
||||
// these message, here to remember the old DP7.
|
||||
|
||||
val actions = UniversalCommand(text)
|
||||
actions(0) = actions(0) substring 1
|
||||
|
||||
actions(0)
|
||||
|
||||
actions(0) match
|
||||
// ignore Telegram command like
|
||||
case TG_FORMAT(_) =>
|
||||
return false
|
||||
// ignore Path link
|
||||
case x if x contains "/" => return false
|
||||
case _ =>
|
||||
|
||||
@ -51,11 +61,11 @@ object OnUserSlashAction extends EventListener {
|
||||
MornyCoeur.extra exec SendMessage(
|
||||
update.message.chat.id,
|
||||
"%s %s%s %s %s!".format(
|
||||
(TGToString as origin) getSenderFirstNameRefHtml,
|
||||
origin.sender_firstnameRefHTML,
|
||||
h(v_verb), if hasObject then "" else "了",
|
||||
if (origin == target)
|
||||
s"<a href='tg://user?id=${(TGToString as target) getSenderId}'>自己</a>"
|
||||
else (TGToString as target) getSenderFirstNameRefHtml,
|
||||
s"<a href='tg://user?id=${origin.sender_id}'>自己</a>"
|
||||
else target.sender_firstnameRefHTML,
|
||||
if hasObject then h(v_object+" ") else ""
|
||||
)
|
||||
).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId)
|
||||
|
@ -1,6 +1,6 @@
|
||||
package cc.sukazyo.cono.morny.bot.query
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode}
|
||||
@ -14,13 +14,13 @@ object MyInformation extends ITelegramQuery {
|
||||
|
||||
override def query (event: Update): List[InlineQueryUnit[_]] | Null = {
|
||||
|
||||
if ((event.inlineQuery.query ne null) || (event.inlineQuery.query nonEmpty)) return null
|
||||
if !((event.inlineQuery.query eq null) || (event.inlineQuery.query isEmpty)) then return null
|
||||
|
||||
List(
|
||||
InlineQueryUnit(InlineQueryResultArticle(
|
||||
inlineIds(ID_PREFIX), TITLE,
|
||||
inlineQueryId(ID_PREFIX), TITLE,
|
||||
new InputTextMessageContent(
|
||||
TelegramUserInformation informationOutputHTML event.inlineQuery.from
|
||||
TelegramUserInformation getFormattedInformation event.inlineQuery.from
|
||||
).parseMode(ParseMode HTML)
|
||||
)).isPersonal(true).cacheTime(10)
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
package cc.sukazyo.cono.morny.bot.query
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent}
|
||||
|
||||
@ -16,7 +16,7 @@ object RawText extends ITelegramQuery {
|
||||
|
||||
List(
|
||||
InlineQueryUnit(InlineQueryResultArticle(
|
||||
inlineIds(ID_PREFIX, event.inlineQuery.query), TITLE,
|
||||
inlineQueryId(ID_PREFIX, event.inlineQuery.query), TITLE,
|
||||
InputTextMessageContent(event.inlineQuery.query)
|
||||
))
|
||||
)
|
||||
|
@ -1,8 +1,8 @@
|
||||
package cc.sukazyo.cono.morny.bot.query
|
||||
|
||||
import cc.sukazyo.cono.morny.Log.logger
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId
|
||||
import cc.sukazyo.cono.morny.util.BiliTool
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode}
|
||||
|
||||
@ -60,11 +60,11 @@ object ShareToolBilibili extends ITelegramQuery {
|
||||
|
||||
List(
|
||||
InlineQueryUnit(InlineQueryResultArticle(
|
||||
inlineIds(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av,
|
||||
inlineQueryId(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av,
|
||||
InputTextMessageContent(SHARE_FORMAT_HTML.format(link_av, id_av)).parseMode(ParseMode HTML)
|
||||
)),
|
||||
InlineQueryUnit(InlineQueryResultArticle(
|
||||
inlineIds(ID_PREFIX_BILI_BV + bv), TITLE_BILI_BV + bv,
|
||||
inlineQueryId(ID_PREFIX_BILI_BV + bv), TITLE_BILI_BV + bv,
|
||||
InputTextMessageContent(SHARE_FORMAT_HTML.format(link_bv, id_bv)).parseMode(ParseMode HTML)
|
||||
))
|
||||
)
|
||||
|
@ -1,20 +1,19 @@
|
||||
package cc.sukazyo.cono.morny.bot.query
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId
|
||||
import com.pengrad.telegrambot.model.Update
|
||||
import com.pengrad.telegrambot.model.request.InlineQueryResultArticle
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.NamedUtils.inlineIds
|
||||
|
||||
import scala.language.postfixOps
|
||||
import scala.util.matching.Regex
|
||||
|
||||
object ShareToolTwitter extends ITelegramQuery {
|
||||
|
||||
val TITLE_VX = "[tweet] Share as VxTwitter"
|
||||
val TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)"
|
||||
val ID_PREFIX_VX = "[morny/share/twitter/vxtwi]"
|
||||
val ID_PREFIX_VX_COMBINED = "[morny/share/twitter/vxtwi_combine]"
|
||||
val REGEX_TWEET_LINK: Regex = "^(?:https?://)?((?:(?:c\\.)?vx|fx|www\\.)?twitter\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(\\?[\\w&=-]+)?$"r
|
||||
private val TITLE_VX = "[tweet] Share as VxTwitter"
|
||||
private val TITLE_VX_COMBINED = "[tweet] Share as VxTwitter(combination)"
|
||||
private val ID_PREFIX_VX = "[morny/share/twitter/vxtwi]"
|
||||
private val ID_PREFIX_VX_COMBINED = "[morny/share/twitter/vxtwi_combine]"
|
||||
private val REGEX_TWEET_LINK: Regex = "^(?:https?://)?((?:(?:c\\.)?vx|fx|www\\.)?twitter\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(\\?[\\w&=-]+)?$"r
|
||||
|
||||
override def query (event: Update): List[InlineQueryUnit[_]] | Null = {
|
||||
|
||||
@ -22,14 +21,14 @@ object ShareToolTwitter extends ITelegramQuery {
|
||||
|
||||
event.inlineQuery.query match
|
||||
|
||||
case REGEX_TWEET_LINK(_1, _2, _) =>
|
||||
case REGEX_TWEET_LINK(_, _2, _, _, _, _) =>
|
||||
List(
|
||||
InlineQueryUnit(InlineQueryResultArticle(
|
||||
inlineIds(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX,
|
||||
inlineQueryId(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX,
|
||||
s"https://vxtwitter.com/$_2"
|
||||
)),
|
||||
InlineQueryUnit(InlineQueryResultArticle(
|
||||
inlineIds(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED,
|
||||
inlineQueryId(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED,
|
||||
s"https://c.vxtwitter.com/$_2"
|
||||
))
|
||||
)
|
||||
|
@ -1,16 +1,16 @@
|
||||
package cc.sukazyo.cono.morny.daemon
|
||||
|
||||
import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig}
|
||||
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException
|
||||
import com.pengrad.telegrambot.request.{BaseRequest, SendMessage}
|
||||
import com.pengrad.telegrambot.response.BaseResponse
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.bot.command.MornyInformation
|
||||
import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.pengrad.telegrambot.model.request.ParseMode
|
||||
import com.pengrad.telegrambot.model.User
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.MsgEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TGToString
|
||||
import com.pengrad.telegrambot.request.{BaseRequest, SendMessage}
|
||||
import com.pengrad.telegrambot.response.BaseResponse
|
||||
|
||||
object MornyReport {
|
||||
|
||||
@ -25,7 +25,7 @@ object MornyReport {
|
||||
s"""cannot execute report to telegram:
|
||||
|${exceptionLog(e) indent 4}
|
||||
| tg-api response:
|
||||
|${(e.getResponse toString) indent 4}"""
|
||||
|${(e.response toString) indent 4}"""
|
||||
.stripMargin
|
||||
}
|
||||
}
|
||||
@ -36,7 +36,7 @@ object MornyReport {
|
||||
case api: EventRuntimeException.ActionFailed =>
|
||||
// language=html
|
||||
"\n\ntg-api error:\n<pre><code>%s</code></pre>"
|
||||
.formatted(GsonBuilder().setPrettyPrinting().create.toJson(api.getResponse))
|
||||
.formatted(GsonBuilder().setPrettyPrinting().create.toJson(api.response))
|
||||
case _ => ""
|
||||
executeReport(SendMessage(
|
||||
MornyCoeur.config.reportToChat,
|
||||
@ -55,7 +55,7 @@ object MornyReport {
|
||||
// language=html
|
||||
s"""<b>▌User unauthenticated action</b>
|
||||
|action: ${h(action)}
|
||||
|by user ${(TGToString as user) fullnameRefHtml}"""
|
||||
|by user ${user.fullnameRefHTML}"""
|
||||
.stripMargin
|
||||
).parseMode(ParseMode HTML))
|
||||
}
|
||||
@ -74,6 +74,7 @@ object MornyReport {
|
||||
).parseMode(ParseMode HTML))
|
||||
}
|
||||
|
||||
//noinspection ScalaWeakerAccess
|
||||
def sectionConfigFields (config: MornyConfig): String = {
|
||||
val echo = StringBuilder()
|
||||
for (field <- config.getClass.getFields) {
|
||||
@ -105,7 +106,7 @@ object MornyReport {
|
||||
def onMornyExit (causedBy: AnyRef|Null): Unit = {
|
||||
if unsupported then return
|
||||
val causedTag = causedBy match
|
||||
case u: User => (TGToString as u) fullnameRefHtml
|
||||
case u: User => u.fullnameRefHTML
|
||||
case n if n == null => "UNKNOWN reason"
|
||||
case a: AnyRef => /*language=html*/ s"<code>${h(a.toString)}</code>"
|
||||
executeReport(SendMessage(
|
||||
|
@ -10,8 +10,8 @@ object MornyJrrp {
|
||||
jrrp_v_xmomi(user.id, timestamp/(1000*60*60*24)) * 100.0
|
||||
|
||||
private def jrrp_v_xmomi (identifier: Long, dayStamp: Long): Double =
|
||||
import cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex
|
||||
import cc.sukazyo.cono.morny.util.CommonEncrypt.hashMd5
|
||||
(java.lang.Long parseLong byteArrayToHex(hashMd5(s"$identifier@$dayStamp")).substring(0, 4)) / (0xffff toDouble)
|
||||
import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5
|
||||
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
|
||||
java.lang.Long.parseLong(MD5(s"$identifier@$dayStamp").toHex.substring(0, 4), 16) / (0xffff toDouble)
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ object IP186QueryHandler {
|
||||
commonQuery(SITE_URL + ip, QUERY_PARAM_IP)
|
||||
|
||||
@throws[IOException]
|
||||
//noinspection ScalaWeakerAccess
|
||||
def query_whois (domain: String): IP186Response =
|
||||
commonQuery(SITE_URL+"whois/"+domain, QUERY_PARAM_WHOIS)
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.internal
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.collection.immutable as simm
|
||||
import java.util as j
|
||||
|
||||
object ScalaJavaConv {
|
||||
|
||||
def jSetInteger2simm (data: j.Set[Integer]): simm.Set[Int] =
|
||||
data.asScala.toSet.map(_.intValue)
|
||||
|
||||
}
|
119
src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala
Normal file
119
src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala
Normal file
@ -0,0 +1,119 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
import cc.sukazyo.cono.morny.util.UseMath.**
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
/** Utils about $Bilibili
|
||||
*
|
||||
* contains utils:
|
||||
* - av/BV converting:
|
||||
* - [[toAv]]
|
||||
* - [[toBv]]
|
||||
*
|
||||
* @define Bilibili [[https://bilibili.com Bilibili]]
|
||||
*
|
||||
* @define AvBvFormat
|
||||
* === About AV/BV id format ===
|
||||
* the AV id is a number; the BV id is a special 10 digits base58 number, it shows as String
|
||||
* in programming.
|
||||
*
|
||||
* e.g. while the link ''`https://www.bilibili.com/video/BV17x411w7KC/`'' shows
|
||||
* the same with ''`https://www.bilibili.com/video/av170001/`'', the AV id
|
||||
* is __`170001`__, the BV id is __`BV17x411w7KC`__.
|
||||
*
|
||||
* @define AvBvSeeAlso [[https://www.zhihu.com/question/381784377/answer/1099438784 mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」?]]
|
||||
* @todo Maybe make a class `AV`/`BV` and implement the parse in the class
|
||||
*/
|
||||
object BiliTool {
|
||||
|
||||
private val V_CONV_XOR = 177451812L
|
||||
private val V_CONV_ADD = 8728348608L
|
||||
|
||||
private val BV_TABLE = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"
|
||||
private val TABLE_INT = BV_TABLE.length
|
||||
private val BV_TABLE_REVERSED =
|
||||
val mapping = mutable.HashMap.empty[Char, Int]
|
||||
for (i <- BV_TABLE.indices) mapping += (BV_TABLE(i) -> i)
|
||||
mapping.toMap
|
||||
private val BV_TEMPLATE = "1 4 1 7 "
|
||||
private val BV_TEMPLATE_FILTER = Array(9, 8, 1, 6, 2, 4)
|
||||
|
||||
/** Error of illegal BV id.
|
||||
*
|
||||
* @constructor Build a error with illegal BV details.
|
||||
* @param bv the source illegal BV id.
|
||||
* @param reason why it is illegal.
|
||||
*/
|
||||
class IllegalFormatException private (bv: String, reason: String)
|
||||
extends RuntimeException (s"`$bv is not a valid 10 digits base58 BV id: $reason`") {
|
||||
|
||||
/** Error of illegal BV id, where the reason is the BV id is not 10 digits.
|
||||
*
|
||||
* @param bv the source of illegal BV id.
|
||||
* @param length the length of the illegal BV id.
|
||||
*/
|
||||
def this (bv: String, length: Int) =
|
||||
this(bv, s"given length is $length")
|
||||
|
||||
/** Error of illegal BV id, where the reason is the BV id contains non [[BV_TABLE base58 character]].
|
||||
*
|
||||
* @param bv the source of illegal BV id.
|
||||
* @param c the illegal character
|
||||
* @param location the index of the illegal character in the illegal BV id.
|
||||
*/
|
||||
def this (bv: String, c: Char, location: Int) =
|
||||
this(bv, s"char `$c` is not in base58 char table (in position $location)")
|
||||
}
|
||||
|
||||
/** Convert an AV video id format to BV video id format for $Bilibili
|
||||
*
|
||||
* $AvBvFormat
|
||||
*
|
||||
* this method '''available while the __av-id < 2^27^__''', while it theoretically
|
||||
* available when the av-id < 2^30^. Meanwhile some digits of the BV id is a fixed
|
||||
* value (like the [[BV_TEMPLATE]] shows) -- input __bv__ can do not follow the format,
|
||||
* but it will almost certainly gives a wrong AV id (because the fixed number is not
|
||||
* processed at all!)
|
||||
*
|
||||
* @see $AvBvSeeAlso
|
||||
*
|
||||
* @param bv a BV id, which should be exactly 10 digits and all chars should be
|
||||
* a legal base58 char (which means can be found in [[BV_TABLE]]).
|
||||
* otherwise, an [[IllegalFormatException]] will be thrown.
|
||||
* @return an AV id which will shows the save video of input __bv__ in $Bilibili
|
||||
* @throws IllegalFormatException when the input __bv__ is not a legal 10 digits base58
|
||||
* formatted BV id.
|
||||
*/
|
||||
@throws[IllegalFormatException]
|
||||
def toAv (bv: String): Long = {
|
||||
var av = 0L
|
||||
if (bv.length != 10) throw IllegalFormatException(bv, bv.length)
|
||||
for (i <- BV_TEMPLATE_FILTER.indices) {
|
||||
val _get = BV_TEMPLATE_FILTER(i)
|
||||
val tableToken = BV_TABLE_REVERSED get bv(_get)
|
||||
if tableToken isEmpty then throw IllegalFormatException(bv, bv(_get), _get)
|
||||
av = av + (tableToken.get * (TABLE_INT**i).toLong)
|
||||
}
|
||||
(av - V_CONV_ADD) ^ V_CONV_XOR
|
||||
}
|
||||
|
||||
/** Convert an AV video format to a BV video format for $Bilibili.
|
||||
*
|
||||
* this method '''available while the __av-id < 2^27^__''', while it theoretically
|
||||
* available when the av-id < 2^30^.
|
||||
*
|
||||
* @param av an AV id.
|
||||
* @return a BV id which will shows the save video of input __av__ in $Bilibili
|
||||
*/
|
||||
def toBv (av: Long): String = {
|
||||
val _av = (av^V_CONV_XOR)+V_CONV_ADD
|
||||
val bv = Array(BV_TEMPLATE:_*)
|
||||
for (i <- BV_TEMPLATE_FILTER.indices) {
|
||||
import Math.{floor, pow}
|
||||
bv(BV_TEMPLATE_FILTER(i)) = BV_TABLE( (floor(_av/(TABLE_INT**i)) % TABLE_INT) toInt )
|
||||
}
|
||||
String copyValueOf bv
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
import java.nio.charset.{Charset, StandardCharsets}
|
||||
import java.security.{MessageDigest, NoSuchAlgorithmException}
|
||||
|
||||
/** Provides some re-encapsulated algorithm function, and some standard values in encrypting,
|
||||
* and some normalized utils in processing something in encrypting.
|
||||
*
|
||||
* currently there's:
|
||||
* - standard value:
|
||||
* - [[ENCRYPT_STANDARD_CHARSET]] the standard [[Charset]] to parse between [[String]]
|
||||
* and [[Bin]] in encrypting.
|
||||
* - algorithm encapsulations:
|
||||
* - [[MD5]] (MD5 Message-Digest Algorithm)
|
||||
* - [[SHA1]] (Secure Hash Algorithm 1)
|
||||
* - [[SHA256]] (Secure Hash Algorithm 2: 256bit)
|
||||
* - [[SHA512]] (Secure Hash Algorithm 2: 512bit)
|
||||
* - normalized utils
|
||||
* - [[lint_base64FileName]] remove the .base64 file-extension for base64 text file
|
||||
*
|
||||
* @define WhenString2Bin
|
||||
* [[String]] will encoded to [[Bin]] using [[Charset]] [[ENCRYPT_STANDARD_CHARSET]]
|
||||
*
|
||||
* @todo some tests
|
||||
*/
|
||||
object CommonEncrypt {
|
||||
|
||||
/** the [[Charset]] should use when converting between [[String]]
|
||||
* and [[Bin]] in encrypting */
|
||||
val ENCRYPT_STANDARD_CHARSET: Charset = StandardCharsets.UTF_8
|
||||
|
||||
/** the alias of [[Array]]`[`[[Byte]]`]`.
|
||||
* means the binary data.
|
||||
*/
|
||||
//noinspection ScalaWeakerAccess
|
||||
type Bin = Array[Byte]
|
||||
|
||||
private def hash (data: Bin)(using algorithm: String): Bin =
|
||||
try {
|
||||
MessageDigest.getInstance(algorithm) digest data
|
||||
} catch case n: NoSuchAlgorithmException =>
|
||||
throw IllegalStateException(n)
|
||||
|
||||
/** the [[https://en.wikipedia.org/wiki/MD5 MD5]] hash value of input [[Bin]] `data`. */
|
||||
def MD5(data: Bin): Bin = hash(data)(using "md5")
|
||||
/** the [[https://en.wikipedia.org/wiki/MD5 MD5]] hash value of input [[String]] `data`.
|
||||
*
|
||||
* $WhenString2Bin
|
||||
*/
|
||||
def MD5 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "md5")
|
||||
|
||||
/** the [[https://en.wikipedia.org/wiki/SHA-1 SHA-1]] hash value of input [[Bin]] `data`. */
|
||||
def SHA1 (data: Bin): Bin = hash(data)(using "sha1")
|
||||
/** the [[https://en.wikipedia.org/wiki/SHA-1 SHA-1]] hash value of input [[String]] `data`.
|
||||
*
|
||||
* $WhenString2Bin
|
||||
*/
|
||||
def SHA1 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "sha1")
|
||||
|
||||
/** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/256]] hash value of input [[Bin]] `data`. */
|
||||
def SHA256 (data: Bin): Bin = hash(data)(using "sha256")
|
||||
/** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/256]] hash value of input [[String]] `data`.
|
||||
*
|
||||
* $WhenString2Bin
|
||||
*/
|
||||
def SHA256 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "sha256")
|
||||
|
||||
/** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/512]] hash value of input [[Bin]] `data`. */
|
||||
def SHA512 (data: Bin): Bin = hash(data)(using "sha512")
|
||||
/** the [[https://en.wikipedia.org/wiki/SHA-2 SHA-2/512]] hash value of input [[String]] `data`.
|
||||
*
|
||||
* $WhenString2Bin
|
||||
*/
|
||||
def SHA512 (data: String): Bin = hash(data getBytes ENCRYPT_STANDARD_CHARSET)(using "sha512")
|
||||
|
||||
/** Try get the filename before it got encrypted.
|
||||
*
|
||||
* It assumes the base64 encrypted file should keep the original file, and plus
|
||||
* a file-extension shows the file is base64 encrypted.
|
||||
*
|
||||
* Actually, the file will try find the following file-extension and drop it:
|
||||
* - `.b64`
|
||||
* - `.64.txt`
|
||||
* - `.base64`
|
||||
* - `.base64.txt`
|
||||
* if none of those found, it will do no process anymore.
|
||||
*
|
||||
* @param encrypted the file fullname (means filename with file-extension) of base64 encrypted file.
|
||||
* @return the file fullname removed the base64 file extension.
|
||||
*/
|
||||
def lint_base64FileName (encrypted: String): String = encrypted match
|
||||
case i if i endsWith ".b64" => i dropRight ".b64".length
|
||||
case ix if ix endsWith ".b64.txt" => ix dropRight ".b64.txt".length
|
||||
case l if l endsWith ".base64" => l dropRight ".base64".length
|
||||
case lx if lx endsWith ".base64.txt" => lx dropRight ".base64.txt".length
|
||||
case u => u
|
||||
|
||||
}
|
76
src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala
Normal file
76
src/main/scala/cc/sukazyo/cono/morny/util/CommonFormat.scala
Normal file
@ -0,0 +1,76 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
import java.time.{Instant, LocalDateTime, ZoneId, ZoneOffset}
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/** Some formatting (convert some data to some standard output type)
|
||||
* methods normalized based on Morny's usage
|
||||
*
|
||||
* contains:
|
||||
* - [[DATE_TIME_PATTERN_FULL_MILLIS]] the standard date-time-millis [[String]] pattern format
|
||||
* - [[formatDate]] convert UTC time millis (and hour-offset time zone)
|
||||
* to normalized date-time-millis [[String]]
|
||||
* - [[formatDuration]] convert millis duration to normalized duration [[String]]
|
||||
*
|
||||
*/
|
||||
object CommonFormat {
|
||||
|
||||
/** the standard date-time-millis [[String]] pattern format that Morny in use.
|
||||
*
|
||||
* pattern string is pattern of [[DateTimeFormatter]].
|
||||
*/
|
||||
//noinspection ScalaWeakerAccess
|
||||
val DATE_TIME_PATTERN_FULL_MILLIS = "yyyy-MM-dd HH:mm:ss:SSS"
|
||||
|
||||
/** the formatted date-time-millis [[String]].
|
||||
*
|
||||
* time is formatted by pattern [[DATE_TIME_PATTERN_FULL_MILLIS]].
|
||||
*
|
||||
* @param timestamp millis timestamp. timestamp should be UTC alignment.
|
||||
*
|
||||
* @param utcOffset the hour offset of the time zone, the time-zone controls
|
||||
* which local time describe will use.
|
||||
*
|
||||
* for example, timestamp [[0]] describes 1970-1-1 00:00:00 in
|
||||
* UTC+0, so, use the `timestamp` `0` and `utfOffset` `0` will
|
||||
* returns `"1970-1-1 00:00:00:000"`; however, at the same time,
|
||||
* in UTC+8, the local time is 1970-1-1 08:00:00:000, so use
|
||||
* the `timestamp` `0` and the `utcOffset` `8` will returns
|
||||
* `"1970-1-1 08:00:00:000"`
|
||||
*
|
||||
* @return the time-zone local date-time-millis [[String]] describes the timestamp.
|
||||
*/
|
||||
def formatDate (timestamp: Long, utcOffset: Int): String =
|
||||
DateTimeFormatter.ofPattern(DATE_TIME_PATTERN_FULL_MILLIS).format(
|
||||
LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(timestamp),
|
||||
ZoneId.ofOffset("UTC", ZoneOffset.ofHours(utcOffset))
|
||||
)
|
||||
)
|
||||
|
||||
/** human readable [[String]] that describes the millis duration.
|
||||
*
|
||||
* {{{
|
||||
* scala> formatDuration(10)
|
||||
* val res0: String = 10ms
|
||||
*
|
||||
* scala> formatDuration(3000001)
|
||||
* val res1: String = 50min 0s 1ms
|
||||
*
|
||||
* scala> formatDuration(94179047901720L)
|
||||
* val res2: String = 1090035d 6h 38min 21s 720ms
|
||||
* }}}
|
||||
*
|
||||
* @param duration time duration, in milliseconds
|
||||
* @return time duration, human readable
|
||||
*/
|
||||
def formatDuration (duration: Long): String =
|
||||
val sb = new StringBuilder()
|
||||
if (duration > 1000 * 60 * 60 * 24) sb ++= (duration / (1000 * 60 * 60 * 24)).toString ++= "d "
|
||||
if (duration > 1000 * 60 * 60) sb ++= (duration / (1000 * 60 * 60) % 24).toString ++= "h "
|
||||
if (duration > 1000 * 60) sb ++= (duration / (1000 * 60) % 60).toString ++= "min "
|
||||
if (duration > 1000) sb ++= (duration / 1000 % 60).toString ++= "s "
|
||||
sb ++= (duration % 1000).toString ++= "ms"
|
||||
sb toString
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
/** Added the [[toHex]] method to [[Byte]] and [[Array]]`[`[[Byte]]`]`.
|
||||
*
|
||||
* the [[toHex]] method will takes [[Byte]] as a binary byte and convert
|
||||
* it to the hex [[String]] that can describe the binary byte. there are
|
||||
* always 2 digits unsigned hex number.
|
||||
*
|
||||
* for example, byte `0` is binary `0000 0000`, it will be converted to
|
||||
* `"00"`, and the byte `-1` is binary `1111 1111` which corresponding
|
||||
* `"ff"`.
|
||||
* {{{
|
||||
* scala> 0.toByte.toHex
|
||||
* val res6: String = 00
|
||||
*
|
||||
* scala> 15.toByte.toHex
|
||||
* val res10: String = 0f
|
||||
*
|
||||
* scala> -1.toByte.toHex
|
||||
* val res7: String = ff
|
||||
* }}}
|
||||
*
|
||||
* while converting byte array, the order is: the 1st element of the array
|
||||
* will be put most forward, then the following added to the tail of hex string.
|
||||
* {{{
|
||||
* scala> Array[Byte](0, 1, 2, 3).toHex
|
||||
* val res5: String = 00010203
|
||||
* }}}
|
||||
*
|
||||
*/
|
||||
object ConvertByteHex {
|
||||
|
||||
extension (b: Byte) {
|
||||
|
||||
/** convert the binary of the [[Byte]] contains to hex string.
|
||||
* @see [[ConvertByteHex]]
|
||||
*/
|
||||
def toHex: String = (b >> 4 & 0xf).toHexString + (b & 0xf).toHexString
|
||||
|
||||
}
|
||||
|
||||
extension (data: Array[Byte]) {
|
||||
|
||||
/** convert the binary of the [[Array]]`[`[[Byte]]`]` contains to hex string.
|
||||
*
|
||||
* @see [[ConvertByteHex]]
|
||||
*/
|
||||
def toHex: String =
|
||||
val sb = StringBuilder()
|
||||
for (b <- data) sb ++= (b toHex)
|
||||
sb toString
|
||||
|
||||
}
|
||||
|
||||
}
|
28
src/main/scala/cc/sukazyo/cono/morny/util/FileUtils.scala
Normal file
28
src/main/scala/cc/sukazyo/cono/morny/util/FileUtils.scala
Normal file
@ -0,0 +1,28 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
import java.io.{FileInputStream, IOException}
|
||||
import java.security.{MessageDigest, NoSuchAlgorithmException}
|
||||
import scala.util.Using
|
||||
|
||||
/**
|
||||
* @todo docs
|
||||
* @todo some tests?
|
||||
*/
|
||||
object FileUtils {
|
||||
|
||||
@throws[IOException|NoSuchAlgorithmException]
|
||||
def getMD5Three (path: String): String = {
|
||||
val buffer = Array.ofDim[Byte](8192)
|
||||
var len = 0
|
||||
val algo = MessageDigest.getInstance("MD5")
|
||||
Using (FileInputStream(path)) { stream =>
|
||||
len = stream.read(buffer)
|
||||
while (len != -1)
|
||||
algo update (buffer, 0, len)
|
||||
len = stream.read(buffer)
|
||||
}
|
||||
import ConvertByteHex.toHex
|
||||
algo.digest toHex
|
||||
}
|
||||
|
||||
}
|
13
src/main/scala/cc/sukazyo/cono/morny/util/OkHttpPublic.scala
Normal file
13
src/main/scala/cc/sukazyo/cono/morny/util/OkHttpPublic.scala
Normal file
@ -0,0 +1,13 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
import okhttp3.MediaType
|
||||
|
||||
/** some public values of [[okhttp3]] */
|
||||
object OkHttpPublic {
|
||||
|
||||
/** predefined [[okhttp3]] [[MediaType]]s */
|
||||
object MediaTypes:
|
||||
/** [[MediaType]] of [[https://en.wikipedia.org/wiki/JSON JSON]]. using encoding ''UTF-8'' */
|
||||
val JSON: MediaType = MediaType.get("application/json; charset=utf-8")
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.util.boundary
|
||||
|
||||
/**
|
||||
* @todo docs
|
||||
* @todo maybe there can have some encapsulation
|
||||
*/
|
||||
object UniversalCommand {
|
||||
|
||||
def apply (input: String): Array[String] = {
|
||||
|
||||
val builder = ArrayBuffer.empty[String]
|
||||
|
||||
extension (c: Char) {
|
||||
private inline def isUnsupported: Boolean =
|
||||
(c == '\n') || (c == '\r')
|
||||
private inline def isSeparator: Boolean =
|
||||
c == ' '
|
||||
private inline def isQuote: Boolean =
|
||||
(c == '\'') || (c == '"')
|
||||
private inline def isEscapeChar: Boolean =
|
||||
c == '\\'
|
||||
private inline def escapableInQuote: Boolean =
|
||||
c.isQuote || c.isEscapeChar
|
||||
private inline def escapable: Boolean =
|
||||
c.escapableInQuote || c.isSeparator
|
||||
}
|
||||
|
||||
var arg = StringBuilder()
|
||||
var i = 0
|
||||
while (i < input.length) {
|
||||
if (input(i) isSeparator) {
|
||||
if (arg nonEmpty) builder += arg.toString
|
||||
arg = arg.empty
|
||||
} else if (input(i) isQuote) {
|
||||
val _inside_tag = input(i)
|
||||
boundary { while (true) {
|
||||
i=i+1
|
||||
if (i >= input.length) throw IllegalArgumentException("UniversalCommand: unclosed quoted text")
|
||||
if (input(i) == _inside_tag)
|
||||
boundary.break()
|
||||
else if (input(i) isUnsupported)
|
||||
throw IllegalArgumentException("UniversalCommand: unsupported new-line")
|
||||
else if (input(i) isQuote)
|
||||
throw IllegalArgumentException("UniversalCommand: mixed \" and ' used")
|
||||
else if (input(i) isEscapeChar)
|
||||
if (i+1 >= input.length) throw IllegalArgumentException("UniversalCommand: \\ in the end")
|
||||
if (input(i+1) escapableInQuote)
|
||||
i=i+1
|
||||
arg += input(i)
|
||||
else
|
||||
arg += input(i)
|
||||
}}
|
||||
} else if (input(i) isUnsupported) {
|
||||
throw IllegalArgumentException("UniversalCommand: unsupported new-line")
|
||||
} else if (input(i) isEscapeChar) {
|
||||
if (i + 1 >= input.length) throw IllegalArgumentException("UniversalCommand: \\ in the end")
|
||||
if (input(i+1) escapable)
|
||||
i=i+1
|
||||
arg += input(i)
|
||||
} else {
|
||||
arg += input(i)
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if (arg nonEmpty) builder += arg.toString
|
||||
|
||||
builder toArray
|
||||
|
||||
}
|
||||
|
||||
}
|
19
src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala
Normal file
19
src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala
Normal file
@ -0,0 +1,19 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
import scala.annotation.targetName
|
||||
|
||||
/** @todo some tests */
|
||||
object UseMath {
|
||||
|
||||
extension (self: Int) {
|
||||
|
||||
def over (other: Int): Double = self.toDouble / other
|
||||
|
||||
}
|
||||
|
||||
extension (self: Int) {
|
||||
@targetName("pow")
|
||||
def ** (other: Int): Double = Math.pow(self, other)
|
||||
}
|
||||
|
||||
}
|
33
src/main/scala/cc/sukazyo/cono/morny/util/UseRandom.scala
Normal file
33
src/main/scala/cc/sukazyo/cono/morny/util/UseRandom.scala
Normal file
@ -0,0 +1,33 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
import scala.language.implicitConversions
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* @todo some tests maybe?
|
||||
* @todo use the using clauses to provide random instance
|
||||
*/
|
||||
object UseRandom {
|
||||
|
||||
class ChancePossibility[T <: Any] (val one: T) (using possibility: Double) {
|
||||
def nor[U] (another: U): T|U =
|
||||
if Random.nextDouble < possibility then one else another
|
||||
}
|
||||
|
||||
given Conversion[ChancePossibility[Boolean], Boolean] with
|
||||
def apply(in: ChancePossibility[Boolean]): Boolean = in nor !in.one
|
||||
|
||||
extension (num: Double) {
|
||||
|
||||
def chance_is[T <: Any] (one: T): ChancePossibility[T] =
|
||||
ChancePossibility(one)(using num)
|
||||
|
||||
}
|
||||
|
||||
def rand_half: Boolean = Random.nextBoolean
|
||||
|
||||
def rand_id: String =
|
||||
import ConvertByteHex.toHex
|
||||
Random nextBytes 6 toHex
|
||||
|
||||
}
|
22
src/main/scala/cc/sukazyo/cono/morny/util/package.scala
Normal file
22
src/main/scala/cc/sukazyo/cono/morny/util/package.scala
Normal file
@ -0,0 +1,22 @@
|
||||
package cc.sukazyo.cono.morny
|
||||
|
||||
/** Utils that [[cc.sukazyo.cono.morny]]'s code used.
|
||||
*
|
||||
* contains:
|
||||
* - [[tgapi Telegram API/Utils Extras]]
|
||||
* - extensions of language standard
|
||||
* - [[CommonEncrypt]] re-encapsulated some encrypt algorithms, and some normalized while encrypting.
|
||||
* - [[CommonFormat]] provides some format methods normalized based on Morny usage standard.
|
||||
* - [[ConvertByteHex]] extensions [[Byte]] and so on, make it easier converting binary data in it to a hex string.
|
||||
* - [[UseMath]] scala style to make Math function easier to use
|
||||
* - [[UseRandom]] scala style to use Random to generate something
|
||||
* - external library extras
|
||||
* - [[OkHttpPublic]] defines some static value for [[okhttp3]]
|
||||
* - useful misc utils
|
||||
* - [[FileUtils]] contains some easy-to-use file action.
|
||||
* - [[UniversalCommand]] provides a easy way to get an args array from a string input.
|
||||
* - others
|
||||
* - [[BiliTool about Bilibili]]
|
||||
*
|
||||
*/
|
||||
package object util {}
|
@ -32,7 +32,7 @@ public class ExtraAction {
|
||||
public <T extends BaseRequest<T, R>, R extends BaseResponse> R exec (T req, String errorMessage) {
|
||||
final R resp = bot.execute(req);
|
||||
if (!resp.isOk()) throw new EventRuntimeException.ActionFailed(
|
||||
(errorMessage.equals("") ? String.valueOf(resp.errorCode()) : errorMessage),
|
||||
(errorMessage.isEmpty() ? String.valueOf(resp.errorCode()) : errorMessage),
|
||||
resp
|
||||
);
|
||||
return resp;
|
@ -0,0 +1,31 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi
|
||||
|
||||
import cc.sukazyo.cono.morny.util.UniversalCommand
|
||||
|
||||
class InputCommand private (
|
||||
val target: String|Null,
|
||||
val command: String,
|
||||
val args: Array[String]
|
||||
) {
|
||||
|
||||
override def toString: String =
|
||||
s"{{$command}@{$target}#{${args.mkString}}"
|
||||
|
||||
}
|
||||
|
||||
object InputCommand {
|
||||
|
||||
def apply (input: Array[String]): InputCommand = {
|
||||
val _ex = input(0) split ("@", 2)
|
||||
val _args = input drop 1
|
||||
new InputCommand(
|
||||
if _ex.length == 1 then null else _ex(1),
|
||||
_ex(0),
|
||||
_args
|
||||
)
|
||||
}
|
||||
|
||||
def apply (input: String): InputCommand =
|
||||
InputCommand(UniversalCommand(input))
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi
|
||||
|
||||
object Standardize {
|
||||
|
||||
val CHANNEL_SPEAKER_MAGIC_ID = 136817688
|
||||
|
||||
val MASK_BOTAPI_ID: Long = -1000000000000
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi.event
|
||||
|
||||
import com.pengrad.telegrambot.response.BaseResponse
|
||||
|
||||
class EventRuntimeException (message: String) extends RuntimeException(message)
|
||||
|
||||
object EventRuntimeException {
|
||||
class ActionFailed (message: String, val response: BaseResponse) extends EventRuntimeException(message)
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi.formatting
|
||||
|
||||
import cc.sukazyo.cono.morny.util.CommonEncrypt
|
||||
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
|
||||
|
||||
object NamingUtils {
|
||||
|
||||
def inlineQueryId (tag: String, taggedData: String = ""): String =
|
||||
CommonEncrypt.MD5(tag+taggedData) toHex
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi.formatting
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
import cc.sukazyo.cono.morny.util.tgapi.Standardize.MASK_BOTAPI_ID
|
||||
import com.pengrad.telegrambot.model.{Chat, Message, User}
|
||||
import com.pengrad.telegrambot.model.Chat.Type
|
||||
|
||||
object TelegramFormatter {
|
||||
|
||||
extension (chat: Chat) {
|
||||
|
||||
def safe_name: String = chat.`type` match
|
||||
case Type.Private => _connectName(chat.firstName, chat.lastName)
|
||||
case _ => chat.title
|
||||
|
||||
def safe_linkHTML: String =
|
||||
if (chat.username == null)
|
||||
chat.`type` match
|
||||
// language=html
|
||||
case Type.Private => s"<a href='${_link_user(chat.id)}'>@[u:${chat.id}]</a>"
|
||||
// language=html
|
||||
case _ => s"<a href='${_link_chat(chat.id_tdLib)}'>@[c/${chat.id}]</a>"
|
||||
else s"@${h(chat.username)}"
|
||||
|
||||
//noinspection ScalaWeakerAccess
|
||||
def safe_firstnameRefHTML: String =
|
||||
chat.`type` match
|
||||
// language=html
|
||||
case Type.Private => s"<a href='${_link_user(chat.id)}'>${h(chat.firstName)}</a>"
|
||||
// language=html
|
||||
case _ => s"<a href='${_link_chat(chat.id_tdLib)}'>${h(chat.title)}</a>"
|
||||
|
||||
//noinspection ScalaWeakerAccess
|
||||
def id_tdLib: Long =
|
||||
if chat.id < 0 then (chat.id - MASK_BOTAPI_ID)abs else chat.id
|
||||
|
||||
def typeTag: String = chat.`type` match
|
||||
case Type.Private => "🔒"
|
||||
case Type.group => "💭"
|
||||
case Type.supergroup => "💬"
|
||||
case Type.channel => "📢"
|
||||
|
||||
}
|
||||
|
||||
extension (user: User) {
|
||||
|
||||
//noinspection ScalaWeakerAccess
|
||||
def fullname: String = _connectName(user.firstName, user.lastName)
|
||||
|
||||
def fullnameRefHTML: String =
|
||||
// language=html
|
||||
s"<a href='${_link_user(user.id)}'>${h(user.fullname)}</a>"
|
||||
|
||||
//noinspection ScalaWeakerAccess
|
||||
def firstnameRefHTML: String =
|
||||
// language=html
|
||||
s"<a href='${_link_user(user.id)}'>${h(user.firstName)}</a>"
|
||||
|
||||
def toLogTag: String =
|
||||
(if (user.username == null) user.fullname + " " else "@" + user.username)
|
||||
+ "[" + user.id + "]"
|
||||
|
||||
}
|
||||
|
||||
extension (m: Message) {
|
||||
|
||||
def sender_id: Long =
|
||||
if m.senderChat == null then m.from.id else m.senderChat.id
|
||||
|
||||
def sender_firstnameRefHTML: String =
|
||||
if (m.senderChat == null)
|
||||
m.from.firstnameRefHTML
|
||||
else m.senderChat.safe_firstnameRefHTML
|
||||
|
||||
}
|
||||
|
||||
private inline def _link_user (id: Long): String =
|
||||
s"tg://user?id=$id"
|
||||
|
||||
private inline def _link_chat (id: Long): String =
|
||||
s"https://t.me/c/$id"
|
||||
|
||||
private inline def _connectName (firstName: String, lastName: String): String =
|
||||
firstName + (if lastName == null then "" else " " + lastName)
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi.formatting
|
||||
|
||||
object TelegramParseEscape {
|
||||
|
||||
def escapeHtml (input: String): String =
|
||||
var process = input
|
||||
process = process.replaceAll("&", "&")
|
||||
process = process.replaceAll("<", "<")
|
||||
process = process.replaceAll(">", ">")
|
||||
process
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi.formatting
|
||||
|
||||
import com.pengrad.telegrambot.model.User
|
||||
import okhttp3.{OkHttpClient, Request}
|
||||
|
||||
import java.io.IOException
|
||||
import scala.util.matching.Regex
|
||||
import scala.util.Using
|
||||
|
||||
object TelegramUserInformation {
|
||||
|
||||
private val DC_QUERY_SOURCE_SITE = "https://t.me/"
|
||||
private val DC_QUERY_PROCESSOR_REGEX: Regex = "(cdn[1-9]).tele(sco.pe|gram-cdn.org)"r
|
||||
|
||||
private val httpClient = OkHttpClient()
|
||||
|
||||
@throws[IllegalArgumentException|IOException]
|
||||
def getDataCenterFromUser (username: String): String = {
|
||||
val request = Request.Builder().url(DC_QUERY_SOURCE_SITE + username).build
|
||||
Using (httpClient.newCall(request) execute) { response =>
|
||||
val body = response.body
|
||||
if body eq null then "<empty-upstream-response>"
|
||||
else DC_QUERY_PROCESSOR_REGEX.findFirstMatchIn(body.string) match
|
||||
case Some(res) => res.group(1)
|
||||
case None => "<no-cdn-information>"
|
||||
} get
|
||||
}
|
||||
|
||||
def getFormattedInformation (user: User): String = {
|
||||
import TelegramParseEscape.escapeHtml as h
|
||||
|
||||
val userInfo = StringBuilder()
|
||||
|
||||
userInfo ++= // language=html
|
||||
s"""userid :
|
||||
|- <code>${user.id}</code>"""
|
||||
.stripMargin
|
||||
userInfo ++= {
|
||||
if (user.username eq null) // language=html
|
||||
s"""
|
||||
|username : <u>null</u>
|
||||
|datacenter : <u>not supported</u>"""
|
||||
.stripMargin
|
||||
else // language=html
|
||||
s"""
|
||||
|username :
|
||||
|- <code>${h(user.username)}</code>
|
||||
|datacenter :
|
||||
|- <code>${h(getDataCenterFromUser(user.username))}</code>"""
|
||||
.stripMargin
|
||||
}
|
||||
userInfo ++= // language=html
|
||||
s"""
|
||||
|display name :
|
||||
|- <code>${h(user.firstName)}</code>${if user.lastName ne null then s"\n- <code>${h(user.lastName)}</code>" else ""}"""
|
||||
.stripMargin
|
||||
if (user.languageCode ne null) userInfo ++= // language=html
|
||||
s"""
|
||||
|language-code :
|
||||
|- <code>${user.languageCode}</code>"""
|
||||
.stripMargin
|
||||
|
||||
userInfo toString
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
package cc.sukazyo.cono.morny.util
|
||||
|
||||
/** @todo docs */
|
||||
package object tgapi {}
|
@ -1,18 +0,0 @@
|
||||
package cc.sukazyo.cono.morny;
|
||||
|
||||
import cc.sukazyo.cono.morny.util.UniversalCommand;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MornyCLI {
|
||||
|
||||
public static void main (String[] args) {
|
||||
|
||||
System.out.print("$ java -jar morny-coeur-"+MornySystem.VERSION_FULL()+".jar " );
|
||||
String x;
|
||||
try (Scanner line = new Scanner(System.in)) { x = line.nextLine(); }
|
||||
ServerMain.main(UniversalCommand.format(x));
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.daemon;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
import static cc.sukazyo.cono.morny.internal.ScalaJavaConv.jSetInteger2simm;
|
||||
|
||||
public class TestMedicationTimer {
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
2022-11-13T13:14:35.000+08, +08, 2022-11-13T19:00:00+08
|
||||
2022-11-13T13:14:35.174+02, +02, 2022-11-13T19:00:00+02
|
||||
1998-02-01T08:14:35.871+08, +08, 1998-02-01T19:00:00+08
|
||||
2022-11-13T00:00:00.000-01, -01, 2022-11-13T07:00:00-01
|
||||
2022-11-21T19:00:00.000+00, +00, 2022-11-21T21:00:00+00
|
||||
2022-12-31T21:00:00.000+00, +00, 2023-01-01T07:00:00+00
|
||||
2125-11-18T23:45:27.062+00, +00, 2125-11-19T07:00:00+00
|
||||
""")
|
||||
void testCalcNextRoutineTimestamp (ZonedDateTime base, ZoneOffset zoneHour, ZonedDateTime expected)
|
||||
throws IllegalArgumentException {
|
||||
final Set<Integer> at = Set.of(7, 19, 21);
|
||||
System.out.println("base.toInstant().toEpochMilli() = " + base.toInstant().toEpochMilli());
|
||||
Assertions.assertEquals(
|
||||
expected.toInstant().toEpochMilli(),
|
||||
MedicationTimer.calcNextRoutineTimestamp(base.toInstant().toEpochMilli(), zoneHour, jSetInteger2simm(at))
|
||||
);
|
||||
System.out.println(" ok");
|
||||
}
|
||||
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import static cc.sukazyo.cono.morny.util.BiliTool.*;
|
||||
|
||||
public class TestBiliTool {
|
||||
|
||||
private static final String AV_BV_DATA_CSV = """
|
||||
17x411w7KC, 170001
|
||||
1Q541167Qg, 455017605
|
||||
1mK4y1C7Bz, 882584971
|
||||
1T24y197V2, 688730800
|
||||
""";
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = AV_BV_DATA_CSV)
|
||||
void testAvToBv (String bv, int av) {
|
||||
Assertions.assertEquals(bv, toBv(av));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = AV_BV_DATA_CSV)
|
||||
void testBvToAv (String bv, int av) {
|
||||
Assertions.assertEquals(av, toAv(bv));
|
||||
}
|
||||
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import static cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex;
|
||||
import static cc.sukazyo.cono.morny.util.CommonConvert.byteToHex;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class TestCommonConvert {
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
0x00, 00
|
||||
0x01, 01
|
||||
0x20, 20
|
||||
0x77, 77
|
||||
-0x60, a0
|
||||
0x0a, 0a
|
||||
-0x01, ff
|
||||
-0x05, fb
|
||||
"""
|
||||
)
|
||||
void testByteToHex(byte source, String expected) {
|
||||
assertEquals(expected, byteToHex(source));
|
||||
}
|
||||
|
||||
public enum TestByteArrayToHexSource {
|
||||
$1(new T(new byte[]{0x00}, "00")),
|
||||
$2(new T(new byte[]{(byte)0xff}, "ff")),
|
||||
$3(new T(new byte[]{(byte)0xc3}, "c3")),
|
||||
$4(new T(new byte[]{}, "")),
|
||||
$5(new T(new byte[]{0x30,0x0a,0x00,0x04,(byte)0xb0,0x00}, "300a0004b000")),
|
||||
$6(new T(new byte[]{0x00,0x00,0x0a,(byte)0xff,(byte)0xfc,(byte)0xab,(byte)0x00,0x04}, "00000afffcab0004")),
|
||||
$7(new T(new byte[]{0x00,0x7c,0x11,0x28,(byte)0x88,(byte)0xa6,(byte)0xfc,0x30}, "007c112888a6fc30"));
|
||||
public record T (byte[] raw, String expected) {}
|
||||
public final T value;
|
||||
TestByteArrayToHexSource (T value) { this.value = value; }
|
||||
}
|
||||
@ParameterizedTest
|
||||
@EnumSource
|
||||
void testByteArrayToHex (TestByteArrayToHexSource source) {
|
||||
assertEquals(source.value.expected, byteArrayToHex(source.value.raw));
|
||||
}
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import static cc.sukazyo.cono.morny.util.CommonConvert.byteArrayToHex;
|
||||
import static cc.sukazyo.cono.morny.util.CommonEncrypt.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class TestCommonEncrypt {
|
||||
|
||||
@ParameterizedTest
|
||||
@SuppressWarnings("UnnecessaryStringEscape")
|
||||
@CsvSource(textBlock = """
|
||||
28be57d368b75051da76c068a6733284, '莲子'
|
||||
9644c5cbae223013228cd528817ba4f5, '莲子\n'
|
||||
d41d8cd98f00b204e9800998ecf8427e, ''
|
||||
""")
|
||||
void testHashMd5_String (String md5, String text) {
|
||||
assertEquals(md5, byteArrayToHex(hashMd5(text)));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package cc.sukazyo.cono.morny.util;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import static cc.sukazyo.cono.morny.util.CommonFormat.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class TestCommonFormat {
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
1664646870402, 8, 2022-10-02 01:54:30:402
|
||||
1, 8, 1970-01-01 08:00:00:001
|
||||
0, -1, 1969-12-31 23:00:00:000
|
||||
"""
|
||||
)
|
||||
void testFormatDate (long timestamp, int utfOffset, String expectedHumanReadableTime) {
|
||||
assertEquals(expectedHumanReadableTime, formatDate(timestamp, utfOffset));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(textBlock = """
|
||||
100, '100ms'
|
||||
3000, '3s 0ms'
|
||||
326117522, '3d 18h 35min 17s 522ms'
|
||||
53373805, 14h 49min 33s 805ms
|
||||
""")
|
||||
// -1, '-1ms' // WARN: maybe sometime an unexpected usage
|
||||
// -194271974291, '-291ms' //
|
||||
// """) //
|
||||
void testFormatDuration (long durationMillis, String humanReadableDuration) {
|
||||
assertEquals(humanReadableDuration, formatDuration(durationMillis));
|
||||
}
|
||||
|
||||
}
|
12
src/test/scala/cc/sukazyo/cono/morny/MornyCLI.scala
Normal file
12
src/test/scala/cc/sukazyo/cono/morny/MornyCLI.scala
Normal file
@ -0,0 +1,12 @@
|
||||
package cc.sukazyo.cono.morny
|
||||
|
||||
import cc.sukazyo.cono.morny.util.UniversalCommand
|
||||
|
||||
import scala.io.StdIn
|
||||
|
||||
@main def MornyCLI (): Unit = {
|
||||
|
||||
print("$ java -jar morny-coeur-\"+MornySystem.VERSION_FULL+\".jar ")
|
||||
ServerMain main UniversalCommand(StdIn readLine)
|
||||
|
||||
}
|
10
src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala
Normal file
10
src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala
Normal file
@ -0,0 +1,10 @@
|
||||
package cc.sukazyo.cono.morny.test
|
||||
|
||||
import org.scalatest.freespec.AnyFreeSpec
|
||||
import org.scalatest.matchers.should
|
||||
|
||||
abstract class MornyTests extends AnyFreeSpec with should.Matchers {
|
||||
|
||||
val pending_val = "[not-implemented]"
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package cc.sukazyo.cono.morny.test.utils
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
class BiliToolTest extends MornyTests with TableDrivenPropertyChecks {
|
||||
|
||||
private val examples = Table(
|
||||
("bv", "av"),
|
||||
("17x411w7KC", 170001L),
|
||||
("1Q541167Qg", 455017605L),
|
||||
("1mK4y1C7Bz", 882584971L),
|
||||
("1T24y197V2", 688730800L),
|
||||
)
|
||||
|
||||
forAll (examples) { (bv, av) => s"while using av$av/BV$bv :" - {
|
||||
import cc.sukazyo.cono.morny.util.BiliTool.{toAv, toBv}
|
||||
"av to bv works" in { toBv(av) shouldEqual bv }
|
||||
"bv to av works" in { toAv(bv) shouldEqual av }
|
||||
}}
|
||||
|
||||
"BV with unsupported length :" - {
|
||||
import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalFormatException}
|
||||
val examples = Table(
|
||||
"bv",
|
||||
"12345",
|
||||
"12345678",
|
||||
"123456789",
|
||||
// "1234567890", length 10 which is supported
|
||||
"1234567890a",
|
||||
"1234567890ab",
|
||||
"1234567890abcdef"
|
||||
)
|
||||
forAll(examples) { bv =>
|
||||
s"length ${bv.length} should throws IllegalFormatException" in:
|
||||
an [IllegalFormatException] should be thrownBy toAv(bv)
|
||||
}
|
||||
}
|
||||
|
||||
"BV with special character :" - {
|
||||
val examples = Table(
|
||||
("bv" , "contains_special"),
|
||||
("1mK4O1C7Bz", "O"),
|
||||
("1m04m1C7Bz", "0"),
|
||||
("1mK4O1I7Bz", "I"),
|
||||
("1mK4O1C7Bl", "l"),
|
||||
("1--4O1C7Bl", "[symbols]")
|
||||
)
|
||||
import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalFormatException}
|
||||
forAll(examples) { (bv, with_sp) =>
|
||||
s"'$with_sp' should throws IllegalFormatException" in:
|
||||
an [IllegalFormatException] should be thrownBy toAv(bv)
|
||||
}
|
||||
}
|
||||
|
||||
"av/bv converting should be reversible" in {
|
||||
for (_ <- 1 to 20) {
|
||||
val rand_av = Random.between(0, 999999999L)
|
||||
import cc.sukazyo.cono.morny.util.BiliTool.{toAv, toBv}
|
||||
val my_bv = toBv(rand_av)
|
||||
toAv(my_bv) shouldEqual rand_av
|
||||
toBv(toAv(my_bv)) shouldEqual my_bv
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package cc.sukazyo.cono.morny.test.utils
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
class CommonEncryptTest extends MornyTests with TableDrivenPropertyChecks {
|
||||
|
||||
"while doing hash :" - {
|
||||
|
||||
val examples = Table(
|
||||
("md5" , "text"),
|
||||
("28be57d368b75051da76c068a6733284", "莲子"),
|
||||
("9644c5cbae223013228cd528817ba4f5", "莲子\n"),
|
||||
("d41d8cd98f00b204e9800998ecf8427e", "")
|
||||
)
|
||||
|
||||
import cc.sukazyo.cono.morny.util.CommonEncrypt.MD5
|
||||
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
|
||||
forAll (examples) { (md5, text) =>
|
||||
s"while hashing text \"$text\" :" - {
|
||||
|
||||
s"the MD5 value should be $md5" in { MD5(text).toHex shouldEqual md5 }
|
||||
|
||||
"other algorithms" in pending
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
s"while hashing binary file $pending_val" in pending
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package cc.sukazyo.cono.morny.test.utils
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration}
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
class CommonFormatTest extends MornyTests with TableDrivenPropertyChecks {
|
||||
|
||||
"while using #formatDate :" - {
|
||||
|
||||
val examples = Table(
|
||||
("time_text" , "timestamp", "zone_offset"),
|
||||
("2022-10-02 01:54:30:402", 1664646870402L, 8),
|
||||
("1970-01-01 08:00:00:001", 1L, 8),
|
||||
("1969-12-31 23:00:00:000", 0L, -1),
|
||||
)
|
||||
|
||||
forAll(examples) { (time_text, timestamp, zone_offset) =>
|
||||
s"time $time_text in TimeZone($zone_offset) should be UTC timestamp $timestamp" in:
|
||||
formatDate(timestamp, zone_offset) shouldEqual time_text
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"while using #formatDuration :" - {
|
||||
|
||||
val examples = Table(
|
||||
("time_millis", "duration_text"),
|
||||
(100L , "100ms"),
|
||||
(3000L , "3s 0ms"),
|
||||
(326117522L , "3d 18h 35min 17s 522ms"),
|
||||
(53373805L , "14h 49min 33s 805ms"),
|
||||
(3600001L , "1h 0min 0s 1ms")
|
||||
)
|
||||
|
||||
forAll(examples) { (time_millis, duration_text) =>
|
||||
|
||||
s"duration ($time_millis) millis should be formatted to '$duration_text'" in:
|
||||
formatDuration(time_millis) shouldEqual duration_text
|
||||
0 should equal (0)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package cc.sukazyo.cono.morny.test.utils
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
class ConvertByteHexTest extends MornyTests with TableDrivenPropertyChecks {
|
||||
|
||||
private val examples_hex = Table(
|
||||
("byte" , "hex"),
|
||||
( 0x00 toByte, "00"),
|
||||
( 0x01 toByte, "01"),
|
||||
( 0x20 toByte, "20"),
|
||||
( 0x77 toByte, "77"),
|
||||
(-0x60 toByte, "a0"),
|
||||
( 0x0a toByte, "0a"),
|
||||
(-0x01 toByte, "ff"),
|
||||
( 0xfb toByte, "fb"),
|
||||
)
|
||||
|
||||
"while using Byte#toHex :" - forAll (examples_hex) ((byte, hex) => {
|
||||
s"byte ($byte) should be hex '$hex''" in {
|
||||
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
|
||||
(byte toHex) shouldEqual hex
|
||||
}
|
||||
})
|
||||
|
||||
private val examples_hexs = Table(
|
||||
("bytes", "hex"),
|
||||
(Array[Byte](0x00), "00"),
|
||||
(Array[Byte](0xff toByte), "ff"),
|
||||
(Array[Byte](0xc3 toByte), "c3"),
|
||||
(Array[Byte](), ""),
|
||||
(Array[Byte](0x30,0x0a,0x00,0x04,0xb0.toByte,0x00), "300a0004b000"),
|
||||
(Array[Byte](0x00,0x00,0x0a,0xff.toByte,0xfc.toByte,0xab.toByte,0x00.toByte,0x04), "00000afffcab0004"),
|
||||
(Array[Byte](0x00,0x7c,0x11,0x28,0x88.toByte,0xa6.toByte,0xfc.toByte,0x30), "007c112888a6fc30"),
|
||||
)
|
||||
|
||||
"while using Array[Byte]#toHex :" - forAll(examples_hexs) ((bytes, hex) => {
|
||||
s"byte array(${bytes mkString ","}) should be hex string $hex" in {
|
||||
import cc.sukazyo.cono.morny.util.ConvertByteHex.toHex
|
||||
(bytes toHex) shouldEqual hex
|
||||
}
|
||||
})
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package cc.sukazyo.cono.morny.test.utils
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
|
||||
class FileUtilsTest extends MornyTests {
|
||||
|
||||
"while getting the MD5 hash of a file :" in pending
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package cc.sukazyo.cono.morny.test.utils
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
|
||||
class UniversalCommandTest extends MornyTests with Matchers with TableDrivenPropertyChecks {
|
||||
|
||||
"while formatting command from String :" - {
|
||||
|
||||
import cc.sukazyo.cono.morny.util.UniversalCommand as Cmd
|
||||
|
||||
raw"args should be separated by (\u0020) ascii-space" in:
|
||||
Cmd("a b c delta e") shouldEqual Array("a", "b", "c", "delta", "e");
|
||||
"args should not be separated by non-ascii spaces" in:
|
||||
Cmd("tests ダタ セト") shouldEqual Array("tests", "ダタ セト");
|
||||
"multiple ascii-spaces should not generate empty arg in middle" in:
|
||||
Cmd("tests some of data") shouldEqual Array("tests", "some", "of", "data");
|
||||
|
||||
"""texts and ascii-spaces in '' should grouped in one arg""" in:
|
||||
Cmd("""tests 'data set'""") shouldEqual Array("tests", "data set");
|
||||
"""texts and ascii-spaces in "" should grouped in one arg""" in :
|
||||
Cmd("""tests "data set"""") shouldEqual Array("tests", "data set");
|
||||
"""mixed ' and " should throws IllegalArgumentsException""" in:
|
||||
an [IllegalArgumentException] should be thrownBy Cmd("""tests "data set' "of it'""");
|
||||
"with ' not closed should throws IllegalArgumentException" in:
|
||||
an [IllegalArgumentException] should be thrownBy Cmd("""use 'it """);
|
||||
|
||||
raw"\ should escape itself" in:
|
||||
Cmd(raw"input \\data") shouldEqual Array("input", "\\data");
|
||||
raw"\ should escape ascii-space, makes it processed as a normal character" in:
|
||||
Cmd(raw"input data\ set") shouldEqual Array("input", "data set");
|
||||
raw"\ should escape ascii-space, makes it can be an arg body" in:
|
||||
Cmd(raw"input \ some-thing") shouldEqual Array("input", " ", "some-thing");
|
||||
raw"""\ should escape "", makes it processed as a normal character""" in :
|
||||
Cmd(raw"""use \"inputted""") shouldEqual Array("use", "\"inputted");
|
||||
raw"\ should escape '', makes it processed as a normal character" in:
|
||||
Cmd(raw"use \'inputted") shouldEqual Array("use", "'inputted");
|
||||
raw"\ should escape itself which inside a quoted scope" in:
|
||||
Cmd(raw"use 'quoted \\ body'") shouldEqual Array("use", "quoted \\ body");
|
||||
raw"""\ should escape " which inside a "" scope""" in:
|
||||
Cmd(raw"""in "quoted \" body" body""") shouldEqual Array("in", "quoted \" body", "body");
|
||||
raw"""\ should escape ' which inside a "" scope""" in :
|
||||
Cmd(raw"""in "not-quoted \' body" body""") shouldEqual Array("in", "not-quoted ' body", "body");
|
||||
raw"""\ should escape ' which inside a '' scope""" in :
|
||||
Cmd(raw"""in 'quoted \' body' body""") shouldEqual Array("in", "quoted ' body", "body");
|
||||
raw"""\ should escape " which inside a ' scope""" in :
|
||||
Cmd(raw"""in 'not-quoted \" body' body""") shouldEqual Array("in", "not-quoted \" body", "body");
|
||||
raw"\ should not escape ascii-space which inside a quoted scope" in:
|
||||
Cmd(raw"""'quoted \ do not escape' did""") shouldEqual Array(raw"quoted \ do not escape", "did");
|
||||
raw"with \ in the end should throws IllegalArgumentException" in:
|
||||
an [IllegalArgumentException] should be thrownBy Cmd("something error!\\");
|
||||
|
||||
"with multi-line input should throws IllegalArgumentException" in:
|
||||
an [IllegalArgumentException] should be thrownBy Cmd("something will\nhave a new line");
|
||||
|
||||
val example_special_character = Table(
|
||||
"char",
|
||||
" ",
|
||||
"\t",
|
||||
"\\t",
|
||||
"\\a",
|
||||
"/",
|
||||
"&&",
|
||||
"\\u1234",
|
||||
)
|
||||
forAll(example_special_character) { char =>
|
||||
s"input with special character ($char) should keep origin like" in {
|
||||
Cmd(s"$char dataset data[$char]contains parsed") shouldEqual
|
||||
Array(char, "dataset", s"data[$char]contains", "parsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package cc.sukazyo.cono.morny.test.utils.tgapi
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
|
||||
class InputCommandTest extends MornyTests {
|
||||
|
||||
"while create new InputCommand :" - {
|
||||
|
||||
s"while input is $pending_val:" - {
|
||||
|
||||
s"command should be $pending_val" in pending
|
||||
s"target should be $pending_val" in pending
|
||||
|
||||
"args array should always exists" in pending
|
||||
|
||||
s"args should parsed to array $pending_val" in pending
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package cc.sukazyo.cono.morny.test.utils.tgapi.formatting
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
|
||||
class NamingUtilsTest extends MornyTests {
|
||||
|
||||
"while generating inline query result id :" - {
|
||||
|
||||
"while not use no data :" - {
|
||||
|
||||
"(different tag) should return different id" in pending
|
||||
"(same tag) should return the same id" in pending
|
||||
|
||||
}
|
||||
|
||||
"while use data :" - {
|
||||
|
||||
"(same tag) with (same data) should return the same id" in pending
|
||||
"(same tag) with (different data) should return different id" in pending
|
||||
"(different tag) with (same data) should return different id" in pending
|
||||
"change tag and data position should return different id" in pending
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package cc.sukazyo.cono.morny.test.utils.tgapi.formatting
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
|
||||
class TelegramFormatterTest extends MornyTests {
|
||||
|
||||
"some test" in pending
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package cc.sukazyo.cono.morny.test.utils.tgapi.formatting
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
|
||||
class TelegramParseEscapeTest extends MornyTests {
|
||||
|
||||
"while escape HTML document :" - {
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||
val any_other = "0ir0Q*%_\"ir[0\"#*I%T\"I{EtjpJGI{\")#W*IT}P%*IH#){#NIJB9-/q{$(Jg'9m]q|MH4j0hq}|+($NR{')}}"
|
||||
|
||||
"& must be escaped" in:
|
||||
h("a & b") shouldEqual "a & b"
|
||||
"< and > must be escaped" in:
|
||||
h("<data-error>") shouldEqual "<data-error>"
|
||||
"& and < and > must all be escaped" in:
|
||||
h("<some-a> && <some-b>") shouldEqual "<some-a> && <some-b>"
|
||||
"space and count should be kept" in:
|
||||
h("\t<<<< \n") shouldEqual "\t<<<< \n"
|
||||
"any others should kept origin like" in:
|
||||
h(any_other) shouldEqual any_other
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package cc.sukazyo.cono.morny.test.utils.tgapi.formatting
|
||||
|
||||
import cc.sukazyo.cono.morny.test.MornyTests
|
||||
import org.scalatest.prop.TableDrivenPropertyChecks
|
||||
import org.scalatest.tagobjects.{Network, Slow}
|
||||
|
||||
class TelegramUserInformationTest extends MornyTests with TableDrivenPropertyChecks {
|
||||
|
||||
private val examples_telegram_cdn = Table(
|
||||
("username", "cdn"),
|
||||
("Eyre_S", "cdn5"),
|
||||
)
|
||||
|
||||
forAll(examples_telegram_cdn) ((username, cdn) => s"while user is @$username :" - {
|
||||
|
||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramUserInformation.*
|
||||
|
||||
s"datacenter should be $cdn" taggedAs (Slow, Network) in:
|
||||
getDataCenterFromUser(username) shouldEqual cdn
|
||||
|
||||
"formatted data should as expected" in:
|
||||
pending
|
||||
|
||||
})
|
||||
|
||||
}
|
9
src/test/scala/live/LiveMain.scala
Normal file
9
src/test/scala/live/LiveMain.scala
Normal file
@ -0,0 +1,9 @@
|
||||
package live
|
||||
|
||||
import cc.sukazyo.cono.morny.test.utils.BiliToolTest
|
||||
|
||||
@main def LiveMain (args: String*): Unit = {
|
||||
|
||||
org.scalatest.run(BiliToolTest())
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user