Compare commits

..

2 Commits

Author SHA1 Message Date
6e82227447
bug fix for scala port 2023-09-17 15:18:57 +08:00
a8b7562b51
complete scala port 2023-09-16 23:12:44 +08:00
94 changed files with 2346 additions and 1222 deletions

View File

@ -4,8 +4,6 @@
.vscode/
.gradle/
.settings/
/src/test/java/test/*
/src/test/resources/test/*
#build
/build/

File diff suppressed because it is too large Load Diff

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
package cc.sukazyo.cono.morny.util.tgapi;
public class Standardize {
public static final int CHANNEL_SPEAKER_MAGIC_ID = 136817688;
}

View File

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

View File

@ -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("&", "&amp;");
raw = raw.replaceAll("<", "&lt;");
raw = raw.replaceAll(">", "&gt;");
return raw;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

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

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("&", "&amp;")
process = process.replaceAll("<", "&lt;")
process = process.replaceAll(">", "&gt;")
process
}

View File

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

View File

@ -0,0 +1,4 @@
package cc.sukazyo.cono.morny.util
/** @todo docs */
package object tgapi {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &amp; b"
"< and > must be escaped" in:
h("<data-error>") shouldEqual "&lt;data-error&gt;"
"& and < and > must all be escaped" in:
h("<some-a> && <some-b>") shouldEqual "&lt;some-a&gt; &amp;&amp; &lt;some-b&gt;"
"space and count should be kept" in:
h("\t<<<< \n") shouldEqual "\t&lt;&lt;&lt;&lt; \n"
"any others should kept origin like" in:
h(any_other) shouldEqual any_other
}
}

View File

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

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