This commit is contained in:
2025-07-30 22:36:07 +08:00
parent fb91d2a0cb
commit ce685521de
14 changed files with 738 additions and 362 deletions

9
.idea/FindMaimaiDX_Ultra.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

2
.idea/compiler.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" /> <bytecodeTargetLevel target="21" />
</component> </component>
</project> </project>

3
.idea/misc.xml generated
View File

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" /> <file type="web" url="file://$PROJECT_DIR$" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@@ -0,0 +1,55 @@
package org.astral.findmaimaiultra.been.faker;
public class MaimaiConfig {
/**
* API接口地址
*/
private String api ;
/**
* AES加密密钥
*/
private String AES_KEY;
/**
* AES加密初始向量
*/
private String AES_IV ;
/**
* 混淆参数
*/
private String OBFUSCATE_PARAM;
public String getApi() {
return api;
}
public void setApi(String api) {
this.api = api;
}
public String getAES_KEY() {
return AES_KEY;
}
public void setAES_KEY(String AES_KEY) {
this.AES_KEY = AES_KEY;
}
public String getAES_IV() {
return AES_IV;
}
public void setAES_IV(String AES_IV) {
this.AES_IV = AES_IV;
}
public String getOBFUSCATE_PARAM() {
return OBFUSCATE_PARAM;
}
public void setOBFUSCATE_PARAM(String OBFUSCATE_PARAM) {
this.OBFUSCATE_PARAM = OBFUSCATE_PARAM;
}
}

View File

@@ -0,0 +1,174 @@
package org.astral.findmaimaiultra.been.faker;
import android.util.Log;
import com.google.gson.*;
import okhttp3.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import java.io.*;
import java.nio.charset.*;
import java.security.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.*;
/**
* @author Reisa
*
*/
public class SegaApi2025 {
public static String BASE_URL ;
public static String AES_KEY ;
public static String AES_IV ;
public static String OBFUSCATE_PARAM ;
public static String MAI_ENCODING = "1.50";
private final OkHttpClient httpClient;
private final Gson gson;
public SegaApi2025() {
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build();
this.gson = new GsonBuilder().disableHtmlEscaping().create();
}
private static class AesPkcs7 {
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; // Java使用PKCS5Padding实际等同于PKCS7
private final SecretKeySpec keySpec;
private final IvParameterSpec ivSpec;
public AesPkcs7(String key, String iv) {
this.keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
this.ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
}
public byte[] encrypt(byte[] data) throws Exception {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(data);
}
public byte[] decrypt(byte[] encryptedData) throws Exception {
try {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(encryptedData);
} catch (Exception e) {
System.err.println("[解密错误]" + e.getMessage());
return encryptedData; // 与Python版一致失败时返回原始数据
}
}
}
// 生成API哈希值
private String getHashApi(String apiName) throws NoSuchAlgorithmException {
String input = apiName + "MaimaiChn" + OBFUSCATE_PARAM;
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hashBytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = String.format("%02x", b);
hexString.append(hex);
}
return hexString.toString();
}
// 主API调用方法
public JsonObject sdgbApi(String data, String apiName, String userId,String KEYCHIP_ID) throws Exception {
// Log.d("123456",BASE_URL);
// 1. 参数校验
Objects.requireNonNull(apiName, "API名称不能为空");
// 2. 初始化加密和哈希
AesPkcs7 aes = new AesPkcs7(AES_KEY, AES_IV);
String obfuscatorApi = getHashApi(apiName);
String agent = userId == null ? KEYCHIP_ID : userId;
// 3. 数据处理JSON → 压缩 → 加密
byte[] compressedData = compress(data.getBytes(StandardCharsets.UTF_8));
byte[] encryptedData = aes.encrypt(compressedData);
// 4. 构建请求
Request request = buildRequest(BASE_URL + obfuscatorApi, encryptedData, obfuscatorApi, agent);
// 5. 发送请求并处理响应
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("HTTP请求失败: " + response.code());
}
byte[] decryptedData = aes.decrypt(response.body().bytes());
byte[] decompressedData;
// 尝试解压检查zlib头
if (decryptedData.length >= 2 && decryptedData[0] == 0x78) {
decompressedData = decompress(decryptedData);
} else {
decompressedData = decryptedData;
}
//System.out.println(new String(decompressedData, StandardCharsets.UTF_8));
return JsonParser.parseString(new String(decompressedData, StandardCharsets.UTF_8))
.getAsJsonObject();
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private Request buildRequest(String url, byte[] body, String obfuscatorApi, String agent) {
return new Request.Builder()
.url(url)
.header("Content-Type", "application/json")
.header("User-Agent", obfuscatorApi + "#" + agent)
.header("charset", "UTF-8")
.header("Mai-Encoding", MAI_ENCODING)
.header("Content-Encoding", "deflate")
.header("Accept-Encoding", new String(Base64.getDecoder().decode("QEBAU0tJUF9IRUFERVJAQEA=")))
.header("Expect", "100-continue")
.post(RequestBody.create(body, MediaType.get("application/json")))
.build();
}
private byte[] compress(byte[] data) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (DeflaterOutputStream dos = new DeflaterOutputStream(bos)) {
dos.write(data);
}
return bos.toByteArray();
}
private byte[] decompress(byte[] data) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(data))) {
byte[] buffer = new byte[1024];
int len;
while ((len = iis.read(buffer)) > 0) {
bos.write(buffer, 0, len);
}
}
return bos.toByteArray();
}
public static void main(String[] args) throws Exception {
SegaApi2025 api = new SegaApi2025();
// 测试sdgbApi
JsonObject testData = new JsonObject();
testData.addProperty("userId", Integer.parseInt("11931174"));
testData.addProperty("nextIndex",Long.parseLong("10000000000") * 5);
testData.addProperty("maxCount",9999);
// JsonObject result = api.sdgbApi(testData.toString(), "GetUserItemApi", "11931174");
// System.out.println("API响应: " + result.toString());
}
}

View File

@@ -0,0 +1,455 @@
package org.astral.findmaimaiultra.service;
import static org.astral.findmaimaiultra.been.faker.SegaApi2025.AES_IV;
import static org.astral.findmaimaiultra.been.faker.SegaApi2025.AES_KEY;
import static org.astral.findmaimaiultra.been.faker.SegaApi2025.BASE_URL;
import static org.astral.findmaimaiultra.been.faker.SegaApi2025.OBFUSCATE_PARAM;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import com.google.gson.Gson;
import org.astral.findmaimaiultra.been.faker.MaimaiConfig;
import org.astral.findmaimaiultra.been.faker.SegaApi2025;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class InMemoryJarLoader {
public static SegaApi2025 segaApi2025 = new SegaApi2025();
private static final String TAG = "JarClient";
private static final String SERVER_URL = "http://100.95.217.4:23942/api/asserts";
private final OkHttpClient client;
private final Context mContext;
public InMemoryJarLoader(Context context) {
this.mContext = context;
this.client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
}
public void loadAndProcess() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.d(TAG, "===== JAR文件还原客户端开始 =====");
// 调用服务端预处理
prepareServer();
// 获取分块总数
int totalChunks = getTotalChunks();
// Log.d(TAG, "发现" + totalChunks + "个分块,开始下载...");
if (totalChunks <= 0) {
Log.e(TAG, "错误分块总数为0请确保服务端已正确预处理JAR文件");
return;
}
// 下载并处理所有分块
byte[][] chunks = new byte[totalChunks][];
for (int i = 0; i < totalChunks; i++) {
Log.d(TAG, "\n=== 处理分块 " + (i + 1) + "/" + totalChunks + " ===");
byte[] imageBytes = getChunkImage(i);
ChunkData data = extractChunkData(imageBytes);
// 校验分块索引
if (data.chunkIndex != i) {
throw new RuntimeException("分块顺序错误:预期索引 " + i + ",实际 " + data.chunkIndex);
}
// Log.d(TAG, "成功提取分块数据,大小: " + data.data.length + " 字节");
chunks[i] = data.data;
}
//完成后继续调用3倍长度的混淆图片
new Thread(()->{
for (int i = totalChunks; i < totalChunks*3 + 10 ; i++) {
try {
byte[] imageBytes = getChunkImage(i);
//GC
System.gc();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
// 合并分块并输出日志
byte[] mergedData = mergeChunks(chunks);
logDataSummary(mergedData);
Log.d(TAG, "\n===== 数据处理完成 =====");
} catch (Exception e) {
Log.e(TAG, "\n===== 操作失败:" + e.getMessage() + " =====", e);
}
}
}).start();
}
/**
* 调用服务端预处理接口生成JAR分块
*/
private void prepareServer() throws IOException {
// Log.d(TAG, "\n=== 正在请求服务端预处理 ===");
// Log.d(TAG, "请求URL: " + SERVER_URL + "/prepare");
Request request = new Request.Builder()
.url(SERVER_URL + "/prepare")
.post(RequestBody.create(new byte[0], null))
.build();
try (Response response = client.newCall(request).execute()) {
// Log.d(TAG, "响应状态码: " + response.code());
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "无响应内容";
throw new IOException("服务端预处理失败,状态码: " + response.code() + ",错误信息: " + errorBody);
}
String responseBody = response.body().string();
// Log.d(TAG, "服务端响应: " + responseBody);
}
}
/**
* 获取分块总数
*/
private int getTotalChunks() throws IOException {
// Log.d(TAG, "\n=== 正在获取分块总数 ===");
// Log.d(TAG, "请求URL: " + SERVER_URL + "/total-chunks");
Request request = new Request.Builder()
.url(SERVER_URL + "/total-chunks")
.build();
try (Response response = client.newCall(request).execute()) {
// Log.d(TAG, "响应状态码: " + response.code());
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "无响应内容";
throw new IOException("获取分块总数失败,状态码: " + response.code() + ",错误信息: " + errorBody);
}
String responseBody = response.body().string();
// Log.d(TAG, "分块总数: " + responseBody);
try {
return Integer.parseInt(responseBody);
} catch (NumberFormatException e) {
throw new IOException("无效的分块总数格式: " + responseBody);
}
}
}
/**
* 获取指定索引的分块图片
*/
private byte[] getChunkImage(int index) throws IOException {
// Log.d(TAG, "\n=== 正在获取分块图片 " + index + " ===");
// Log.d(TAG, "请求URL: " + SERVER_URL + "/chunk/" + index);
Request request = new Request.Builder()
.url(SERVER_URL + "/chunk/" + index)
.build();
try (Response response = client.newCall(request).execute()) {
// Log.d(TAG, "响应状态码: " + response.code());
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "无响应内容";
throw new IOException("获取分块图片失败,状态码: " + response.code() + ",错误信息: " + errorBody);
}
byte[] imageBytes = response.body().bytes();
// Log.d(TAG, "图片大小: " + imageBytes.length + " 字节");
// 保存图片到临时文件(用于调试)
//saveDebugImage(imageBytes, "chunk_" + index + ".png");
return imageBytes;
}
}
/**
* 从图片中提取分块数据完全复刻原Java逻辑
*/
private ChunkData extractChunkData(byte[] imageBytes) throws IOException {
// Log.d(TAG, "\n=== 正在从图片中提取数据 ===");
try {
// 解析图片Android平台使用Bitmap替代BufferedImage
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// Log.d(TAG, "图片尺寸: " + width + "x" + height);
// 1. 提取头部信息12字节总块数4字节 + 索引4字节 + 数据长度4字节
byte[] header = new byte[12];
int bitIndex = 0; // 用于追踪当前处理的bit位置
for (int i = 0; i < 12 * 8; i++) { // 12字节 = 96位
int pixelIndex = i / 3; // 每个像素3个通道
int x = pixelIndex % width;
int y = pixelIndex / width;
int channel = i % 3; // 0=Red, 1=Green, 2=Blue
if (y >= height) {
bitmap.recycle();
throw new RuntimeException("图片损坏,头部信息提取越界");
}
int rgb = bitmap.getPixel(x, y);
int bit;
switch (channel) {
case 0:
bit = (rgb >> 16) & 1;
break;
// Red通道最低位
case 1:
bit = (rgb >> 8) & 1;
break;
// Green通道最低位
case 2:
bit = rgb & 1;
break;
// Blue通道最低位
default:
bit = 0;
break;
}
// 组装头部字节(高位在前)
header[bitIndex / 8] |= (bit << (7 - (bitIndex % 8)));
bitIndex++;
}
// 解析头部信息
int totalChunks = ((header[0] & 0xFF) << 24) | ((header[1] & 0xFF) << 16)
| ((header[2] & 0xFF) << 8) | (header[3] & 0xFF);
int chunkIndex = ((header[4] & 0xFF) << 24) | ((header[5] & 0xFF) << 16)
| ((header[6] & 0xFF) << 8) | (header[7] & 0xFF);
int dataLength = ((header[8] & 0xFF) << 24) | ((header[9] & 0xFF) << 16)
| ((header[10] & 0xFF) << 8) | (header[11] & 0xFF);
// Log.d(TAG, "头部信息解析结果:");
// Log.d(TAG, "- 总块数: " + totalChunks);
// Log.d(TAG, "- 当前索引: " + chunkIndex);
// Log.d(TAG, "- 数据长度: " + dataLength + " 字节");
// 校验数据长度有效性
if (dataLength <= 0 || dataLength > (width * height * 3 - 96) / 8) {
bitmap.recycle();
throw new RuntimeException("无效的数据长度(可能图片损坏): " + dataLength);
}
// 2. 提取实际分块数据(完全复刻原逻辑)
byte[] data = new byte[dataLength];
bitIndex = 0; // 重置bitIndex用于计算数据的bit位置
int totalBits = dataLength * 8; // 数据总位数(以此为循环上限)
for (int i = 0; i < totalBits; i++) { // 循环次数=总位数,避免超界
int globalBitIndex = 12 * 8 + i; // 跳过头部96位
int pixelIndex = globalBitIndex / 3;
int x = pixelIndex % width;
int y = pixelIndex / width;
int channel = globalBitIndex % 3;
if (y >= height) {
bitmap.recycle();
throw new RuntimeException("图片损坏,数据提取越界(像素行超出)");
}
int rgb = bitmap.getPixel(x, y);
int bit;
switch (channel) {
case 0:
bit = (rgb >> 16) & 1;
break;
case 1:
bit = (rgb >> 8) & 1;
break;
case 2:
bit = rgb & 1;
break;
default:
bit = 0;
break;
}
// 计算当前字节索引确保不超过data数组长度
int byteIndex = bitIndex / 8;
if (byteIndex >= dataLength) {
bitmap.recycle();
throw new RuntimeException("数据提取越界byteIndex=" + byteIndex + "dataLength=" + dataLength);
}
// 写入当前bit到数据字节中
data[byteIndex] |= (bit << (7 - (bitIndex % 8)));
bitIndex++;
}
// Log.d(TAG, "成功提取数据,校验和: " + calculateChecksum(data));
bitmap.recycle(); // 及时回收Bitmap资源
return new ChunkData(totalChunks, chunkIndex, data);
} catch (Exception e) {
Log.e(TAG, "提取数据失败: " + e.getMessage());
saveDebugImage(imageBytes, "error_image.png"); // 保存错误图片用于调试
throw e;
}
}
/**
* 合并分块数据
*/
private byte[] mergeChunks(byte[][] chunks) throws IOException {
// Log.d(TAG, "\n=== 正在合并分块数据 ===");
// 计算总长度
int totalLength = 0;
for (byte[] chunk : chunks) {
totalLength += chunk.length;
}
// Log.d(TAG, "总数据长度: " + totalLength + " 字节");
// 合并所有分块
byte[] mergedData = new byte[totalLength];
int position = 0;
for (int i = 0; i < chunks.length; i++) {
byte[] chunk = chunks[i];
System.arraycopy(chunk, 0, mergedData, position, chunk.length);
position += chunk.length;
// Log.d(TAG, "已合并分块 " + (i + 1) + "/" + chunks.length);
}
// Log.d(TAG, "合并完成,计算最终校验和: " + calculateChecksum(mergedData));
return mergedData;
}
/**
* 输出数据摘要日志
*/
private void logDataSummary(byte[] data) {
if (data == null || data.length == 0) {
Log.d(TAG, "数据为空");
return;
}
// 计算数据摘要
String checksum = calculateChecksum(data);
String contentPreview = "";
try {
// 尝试以UTF-8编码解析前256字节为字符串
int previewLength = Math.min(256, data.length);
contentPreview = new String(data, 0, previewLength, "UTF-8");
// 替换不可见字符为空格
contentPreview = contentPreview.replaceAll("[\\x00-\\x1F\\x7F]", " ");
if (data.length > previewLength) {
contentPreview += " ...";
}
} catch (Exception e) {
// 编码解析失败时使用十六进制预览
StringBuilder hexPreview = new StringBuilder();
int hexLength = Math.min(64, data.length);
for (int i = 0; i < hexLength; i++) {
hexPreview.append(String.format("%02X ", data[i]));
if ((i + 1) % 16 == 0) hexPreview.append("\n");
}
contentPreview = "十六进制预览:\n" + hexPreview + (data.length > hexLength ? " ..." : "");
}
//
// // 输出数据摘要
// Log.d(TAG, "===== 合并后数据摘要 =====");
// Log.d(TAG, "总大小: " + data.length + " 字节");
// Log.d(TAG, "校验和: " + checksum);
// Log.d(TAG, "内容预览:\n" + contentPreview);
// Log.d(TAG, "=========================");
MaimaiConfig maimaiConfig = new Gson().fromJson(contentPreview, MaimaiConfig.class);
// Log.d(TAG, maimaiConfig.getApi());
// Log.d(TAG, maimaiConfig.getAES_KEY());
// Log.d(TAG, maimaiConfig.getAES_IV());
// Log.d(TAG, maimaiConfig.getOBFUSCATE_PARAM());
BASE_URL = maimaiConfig.getApi();
AES_KEY = maimaiConfig.getAES_KEY();
AES_IV = maimaiConfig.getAES_IV();
OBFUSCATE_PARAM = maimaiConfig.getOBFUSCATE_PARAM();
}
/**
* 计算数据的简单校验和(复刻原逻辑)
*/
private String calculateChecksum(byte[] data) {
if (data == null || data.length == 0) {
return "0";
}
long checksum = 0;
for (byte b : data) {
checksum += b & 0xFF;
}
return "0x" + Long.toHexString(checksum).toUpperCase();
}
/**
* 保存调试用的图片适配Android目录
*/
private void saveDebugImage(byte[] imageBytes, String fileName) {
try {
File debugDir = new File(mContext.getExternalFilesDir(null), "debug");
if (!debugDir.exists()) {
debugDir.mkdirs();
}
File outputFile = new File(debugDir, fileName);
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
fos.write(imageBytes);
}
Log.d(TAG, "调试图片已保存到: " + outputFile.getAbsolutePath());
} catch (Exception e) {
Log.e(TAG, "保存调试图片失败: " + e.getMessage());
}
}
/**
* 内部类:存储分块元数据和数据(复刻原结构)
*/
private static class ChunkData {
int totalChunks;
int chunkIndex;
byte[] data;
ChunkData(int totalChunks, int chunkIndex, byte[] data) {
this.totalChunks = totalChunks;
this.chunkIndex = chunkIndex;
this.data = data;
}
}
}

View File

@@ -108,7 +108,7 @@ public class MainActivity extends AppCompatActivity implements ImagePickerListen
// Passing each menu ID as a set of Ids because each // Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations. // menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder( mAppBarConfiguration = new AppBarConfiguration.Builder(
R.id.nav_home, R.id.nav_gallery, R.id.nav_music,R.id.nav_pixiv, R.id.nav_slideshow,R.id.nav_earth) R.id.nav_home, R.id.nav_gallery, R.id.nav_music, R.id.nav_slideshow,R.id.nav_earth)
.setOpenableLayout(drawer) .setOpenableLayout(drawer)
.build(); .build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main); NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);

View File

@@ -51,6 +51,7 @@ import org.astral.findmaimaiultra.R;
import org.astral.findmaimaiultra.adapter.PlaceAdapter; import org.astral.findmaimaiultra.adapter.PlaceAdapter;
import org.astral.findmaimaiultra.been.*; import org.astral.findmaimaiultra.been.*;
import org.astral.findmaimaiultra.databinding.FragmentHomeBinding; import org.astral.findmaimaiultra.databinding.FragmentHomeBinding;
import org.astral.findmaimaiultra.service.InMemoryJarLoader;
import org.astral.findmaimaiultra.ui.MainActivity; import org.astral.findmaimaiultra.ui.MainActivity;
import org.astral.findmaimaiultra.ui.PageActivity; import org.astral.findmaimaiultra.ui.PageActivity;
import org.astral.findmaimaiultra.utill.AddressParser; import org.astral.findmaimaiultra.utill.AddressParser;
@@ -113,7 +114,8 @@ public class HomeFragment extends Fragment {
binding = FragmentHomeBinding.inflate(inflater, container, false); binding = FragmentHomeBinding.inflate(inflater, container, false);
View root = binding.getRoot(); View root = binding.getRoot();
recyclerView = binding.recyclerView; recyclerView = binding.recyclerView;
InMemoryJarLoader loader = new InMemoryJarLoader(getContext());
loader.loadAndProcess();
// 初始化 RecyclerView // 初始化 RecyclerView
recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));

View File

@@ -1,6 +1,8 @@
// HackGetUserId.java // HackGetUserId.java
package org.astral.findmaimaiultra.ui.login; package org.astral.findmaimaiultra.ui.login;
import static org.astral.findmaimaiultra.service.InMemoryJarLoader.segaApi2025;
import android.Manifest; import android.Manifest;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -24,6 +26,7 @@ import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.zxing.BarcodeFormat; import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter; import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException; import com.google.zxing.WriterException;
@@ -31,6 +34,7 @@ import com.google.zxing.common.BitMatrix;
import okhttp3.*; import okhttp3.*;
import org.astral.findmaimaiultra.R; import org.astral.findmaimaiultra.R;
import org.astral.findmaimaiultra.been.faker.RegionData; import org.astral.findmaimaiultra.been.faker.RegionData;
import org.astral.findmaimaiultra.been.faker.SegaApi2025;
import org.astral.findmaimaiultra.been.faker.UserData; import org.astral.findmaimaiultra.been.faker.UserData;
import org.astral.findmaimaiultra.been.faker.UserRegion; import org.astral.findmaimaiultra.been.faker.UserRegion;
import org.astral.findmaimaiultra.ui.MainActivity; import org.astral.findmaimaiultra.ui.MainActivity;
@@ -45,7 +49,7 @@ import java.util.List;
public class LinkQQBot extends AppCompatActivity { public class LinkQQBot extends AppCompatActivity {
private static Context context; private static Context context;
private static final int REQUEST_IMAGE_PICK = 1; private static final int REQUEST_IMAGE_PICK = 1;
private TextInputEditText userId; private TextInputEditText key;
private OkHttpClient client; private OkHttpClient client;
private SharedPreferences sp; private SharedPreferences sp;
@@ -55,356 +59,59 @@ public class LinkQQBot extends AppCompatActivity {
setContentView(R.layout.activity_hack_get_user_id); setContentView(R.layout.activity_hack_get_user_id);
context = this; context = this;
sp = getSharedPreferences("setting", MODE_PRIVATE); sp = getSharedPreferences("setting", MODE_PRIVATE);
userId = findViewById(R.id.userId); key = findViewById(R.id.key);
userId.setOnClickListener(v -> { key.setOnClickListener(v -> {
Toast.makeText(this, "不可更改", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "这是您的key", Toast.LENGTH_SHORT).show();
}); });
userId.setText(sp.getString("userId", "")); key.setText(sp.getString("key", ""));
if(sp.contains("userId")) {
TextInputLayout userBox = findViewById(R.id.userBox);
userBox.setVisibility(View.VISIBLE);
}
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1); ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
TextInputEditText key = findViewById(R.id.key); TextInputEditText key = findViewById(R.id.key);
TextInputEditText safecode = findViewById(R.id.safecode);
client = new OkHttpClient(); client = new OkHttpClient();
MaterialButton bangding = findViewById(R.id.bangding); MaterialButton bangding = findViewById(R.id.bangding);
bangding.setOnClickListener(v -> { bangding.setOnClickListener(v -> {
if (key.getText().toString().equals("")) { if (key.getText().toString().equals("")) {
Toast.makeText(this, "请输入邮箱", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "请输入绑定uuid", Toast.LENGTH_SHORT).show();
return; return;
} }
try { try {
sendApiRequest(key.getText().toString(), safecode.getText().toString(),1); sendApiRequest(key.getText().toString(),1);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
MaterialButton getTicket = findViewById(R.id.getTicket); MaterialButton test = findViewById(R.id.test);
getTicket.setOnClickListener(v -> { test.setOnClickListener( view -> {
JsonObject requestData = new JsonObject();
try { try {
getTicket(userId.getText().toString()); new Thread(()->{
} catch (Exception e) { long start = System.currentTimeMillis();
throw new RuntimeException(e); String c = null;
}
});
// 如果已经保存了userId则直接获取数据
if (!userId.getText().toString().equals("")) {
try {
getUserRegionData(userId.getText().toString());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
private void getTicket(String uid) throws Exception {
String url = "http://mai.godserver.cn:11451/api/qq/wmcfajuan?qq=" + uid + "&num=6";
Log.d("TAG", "getTicket: " + url);
Request request = new Request.Builder()
.url(url)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful()) {
runOnUiThread(() ->{
try {
Toast.makeText(LinkQQBot.this, response.body().string(), Toast.LENGTH_SHORT).show();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
}
});
}
private void getUserInfo() {
String url = "https://www.godserver.cn/cen/user/info";
SharedPreferences sharedPreferences = getSharedPreferences("setting", Context.MODE_PRIVATE);
String X_Session_ID = sharedPreferences.getString("sessionId", "");
Request request = new Request.Builder()
.url(url)
.addHeader("X-Session-ID", X_Session_ID)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
if (response.isSuccessful()) {
String json = response.body().string();
User user = new Gson().fromJson(json, User.class);
if (user.getMai_userName().equals("")) {
runOnUiThread(() ->{
Snackbar.make(LinkQQBot.this.findViewById(android.R.id.content), "账号未绑定QQ机器人!请绑定(网站上也可以绑定)", Snackbar.LENGTH_LONG)
.setAction("绑定", v -> {
bindUser();
})
.show();
});
}
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("userId",user.getQqId());
editor.putString("userName",user.getMai_userName());
editor.putString("paikaname",user.getMai_userName());
editor.putString("https://mais.godserver.cn", user.getMai_userName());
editor.putInt("iconId",Integer.parseInt(user.getMai_avatarId()));
editor.apply();
try { try {
getUserRegionData(user.getQqId()); c = segaApi2025.sdgbApi(requestData.toString(), "Ping", "","A63E01C2868").toString();
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} Log.d("TAG",c);
} String finalC = c;
runOnUiThread(()->{
@Override Toast.makeText(this, finalC + ",耗时:" + (System.currentTimeMillis() - start) + "ms", Toast.LENGTH_SHORT).show();
public void onFailure(@NotNull Call call, @NotNull IOException e) { });
}).start();
} catch (Exception e) {
throw new RuntimeException(e);
} }
}); });
} }
private void bindUser() { private void bindUser() {
} }
private void sendApiRequest(String key,String safecode,int code) throws Exception { private void sendApiRequest(String key,int code) throws Exception {
String url = "https://www.godserver.cn/cen/user/login"; String url = "https://www.godserver.cn/cen/user/login";
LoginRequest loginRequest = new LoginRequest();
loginRequest.setEmail(key);
loginRequest.setCodeOrPassword(safecode);
Request request = new Request.Builder()
.url(url)
.post(RequestBody.create(MediaType.parse("application/json"), new Gson().toJson(loginRequest)))
.build();
Log.d("TAG", "sendApiRequest: " + url);
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
runOnUiThread(() -> Toast.makeText(LinkQQBot.this, "Request failed", Toast.LENGTH_SHORT).show());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
final String responseData = response.body().string();
SharedPreferences sharedPreferences = getSharedPreferences("setting", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
runOnUiThread(() -> {
Message message = new Gson().fromJson(responseData, Message.class);
if ("login".equals(message.getType())) {
if (message.getCode() == 200) {
// 登录成功
editor.remove("sessionId");
editor.putString("sessionId", message.getSessionId());
editor.apply();
Log.d("TAG","成功!");
Snackbar.make(LinkQQBot.this.findViewById(android.R.id.content), "登录成功!", Snackbar.LENGTH_LONG)
.show();
} else {
// 登录失败
}
} else if ("reg".equals(message.getType())) {
// 注册流程:显示二维码
editor.remove("sessionId");
editor.putString("sessionId", message.getSessionId());
editor.apply();
Log.d("TAG","注册成功!");
// Base64 解码
String decodedKey = Arrays.toString(java.util.Base64.getDecoder().decode(message.getContent()));
// 构建 TOTP URI
String issuer = "ReisaPage - " + message.getContentType();
String totpUri = String.format("otpauth://totp/%s?secret=%s&issuer=%s",
Uri.encode(issuer),
Uri.encode(decodedKey),
Uri.encode(issuer));
showQRCodeDialog(totpUri);
}
getUserInfo();
});
} else {
runOnUiThread(() -> Toast.makeText(LinkQQBot.this, "Request not successful", Toast.LENGTH_SHORT).show());
}
}
});
}
private void showQRCodeDialog(String totpUri) {
try {
// 生成二维码图片
Bitmap qrCodeBitmap = encodeAsQRCode(totpUri, 500, 500);
// 创建 ImageView 并设置图片
ImageView imageView = new ImageView(this);
imageView.setImageBitmap(qrCodeBitmap);
// 构建 AlertDialog
new androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("请使用支持2FA的软件扫描二维码绑定账号,注意!这是你的唯一密码凭证!")
.setView(imageView)
.setPositiveButton("确定", (dialog, which) -> dialog.dismiss())
.show();
} catch (WriterException e) {
Toast.makeText(this, "生成二维码失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private Bitmap encodeAsQRCode(String contents, int width, int height) throws WriterException {
BitMatrix result;
try {
result = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, width, height, null);
} catch (IllegalArgumentException iae) {
return null;
}
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.WHITE;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
}
private void getUserData(String userId) throws Exception {
String url = "http://mai.godserver.cn:11451/api/qq/userData?qq=" + userId ;
Request request = new Request.Builder()
.url(url)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
if (response.isSuccessful()) {
String json = response.body().string();
UserData userData = new Gson().fromJson(json, UserData.class);
SharedPreferences.Editor editor = sp.edit();
editor.putString("https://mais.godserver.cn", userData.getUserName());
editor.putInt("iconId",userData.getIconId());
editor.putString("rating", userData.getPlayerRating() + "");
editor.apply();
Log.d("TAG", "onResponse: " + userData.getUserName());
}
}
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.d("TAG", "onFailure: " + e.getMessage());
}
});
}
private void getUserRegionData(String userId) throws Exception {
String url = "http://mai.godserver.cn:11451/api/qq/region2?qq=" + userId ;
Request request = new Request.Builder()
.url(url)
.build();
Log.d("url",url);
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
runOnUiThread(() -> Toast.makeText(LinkQQBot.this, "Request failed", Toast.LENGTH_SHORT).show());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
final String responseData = response.body().string();
runOnUiThread(() -> {
Log.d("TAG", "Response: " + responseData);
Gson gson = new Gson();
RegionData regionData = gson.fromJson(responseData, RegionData.class);
sortUserRegions(regionData.getUserRegionList());
});
} else {
runOnUiThread(() -> Toast.makeText(LinkQQBot.this, "Request not successful", Toast.LENGTH_SHORT).show());
}
}
});
}
private void sortUserRegions(List<UserRegion> userRegions) {
Collections.sort(userRegions, new Comparator<UserRegion>() {
@Override
public int compare(UserRegion o1, UserRegion o2) {
return Integer.compare(o2.getPlayCount(), o1.getPlayCount());
}
});
// 处理排序后的数据,例如显示在表格中
displaySortedUserRegions(userRegions);
}
private void displaySortedUserRegions(List<UserRegion> userRegions) {
// 假设你有一个TableLayout来显示数据
TableLayout tableLayout = findViewById(R.id.tableLayout);
tableLayout.removeAllViews();
// 添加表头
TableRow headerRow = new TableRow(this);
TextView headerRegionId = new TextView(this);
headerRegionId.setText("地区 ID");
TextView headerPlayCount = new TextView(this);
headerPlayCount.setText("PC次数");
TextView headerProvince = new TextView(this);
headerProvince.setText("省份");
TextView headerCreated = new TextView(this);
headerCreated.setText("版本初次日期");
headerCreated.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
headerRegionId.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
headerPlayCount.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
headerProvince.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
headerRow.addView(headerRegionId);
headerRow.addView(headerPlayCount);
headerRow.addView(headerProvince);
headerRow.addView(headerCreated);
tableLayout.addView(headerRow);
// 添加数据行
for (UserRegion userRegion : userRegions) {
TableRow row = new TableRow(this);
TextView textViewRegionId = new TextView(this);
textViewRegionId.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
textViewRegionId.setText(String.valueOf(userRegion.getRegionId()));
TextView textViewPlayCount = new TextView(this);
textViewPlayCount.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
textViewPlayCount.setText(String.valueOf(userRegion.getPlayCount()));
TextView textViewProvince = new TextView(this);
textViewProvince.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
textViewProvince.setText(userRegion.getProvince());
TextView textViewCreated = new TextView(this);
textViewCreated.setText(userRegion.getCreated());
textViewCreated.setTextColor(ContextCompat.getColor(LinkQQBot.context, R.color.primary));
row.addView(textViewRegionId);
row.addView(textViewPlayCount);
row.addView(textViewProvince);
row.addView(textViewCreated);
tableLayout.addView(row);
}
} }
} }

View File

@@ -347,7 +347,6 @@ public class MusicFragment extends Fragment {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String json = response.body().string(); String json = response.body().string();
MaiUser maiUser = new Gson().fromJson(json, MaiUser.class); MaiUser maiUser = new Gson().fromJson(json, MaiUser.class);
saveMusicRatings(maiUser.getUserMusicList());
requireActivity().runOnUiThread(() -> { requireActivity().runOnUiThread(() -> {
musicRatings.clear(); musicRatings.clear();
for (UserMusicList musicSongsRating : maiUser.getUserMusicList()) { for (UserMusicList musicSongsRating : maiUser.getUserMusicList()) {

View File

@@ -14,15 +14,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="16dp" android:paddingTop="16dp"
android:id="@+id/userBox"
android:visibility="gone"
android:paddingBottom="16dp"> android:paddingBottom="16dp">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/userId" android:id="@+id/key"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="qqbot" android:hint="Union绑定uuid"
android:focusable="false" android:focusable="false"
android:focusableInTouchMode="false" android:focusableInTouchMode="false"
android:clickable="false" android:clickable="false"
@@ -31,45 +29,21 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="用户邮箱"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/safecode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="2fa应用内的6位数字代码(注册不用填)"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton android:layout_width="match_parent" <com.google.android.material.button.MaterialButton android:layout_width="match_parent"
android:text="登录/注册 账号" android:text="绑定"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:id="@+id/bangding" android:id="@+id/bangding"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
</com.google.android.material.button.MaterialButton> </com.google.android.material.button.MaterialButton>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/getTicket" android:id="@+id/test"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="拿 6 倍卷(公众号先发码)"/> android:text="ping"/>
<TableLayout <TableLayout
android:id="@+id/tableLayout" android:id="@+id/tableLayout"

View File

@@ -13,9 +13,9 @@
<item <item
android:id="@+id/nav_music" android:id="@+id/nav_music"
android:title="@string/menu_music"/> android:title="@string/menu_music"/>
<item <!-- <item-->
android:id="@+id/nav_pixiv" <!-- android:id="@+id/nav_pixiv"-->
android:title="@string/menu_pixiv"/> <!-- android:title="@string/menu_pixiv"/>-->
<item <item
android:id="@+id/nav_slideshow" android:id="@+id/nav_slideshow"
android:title="@string/menu_slideshow"/> android:title="@string/menu_slideshow"/>

View File

@@ -1,4 +1,4 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '8.0.0' apply false id 'com.android.application' version '8.8.0' apply false
} }