Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 30 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,6 @@ Seira正在活跃开发中,在使用的过程中可能会有一些Bug,也会

<img width="400" alt="image" src="https://github.com/user-attachments/assets/e5f7cea1-d759-4a0e-a8ec-5ad0f0fcc274" />

## 快速开始

### 1) 准备环境

- JDK 25
- Maven
- QQ 机器人应用凭据 或 Napcat服务
- 一个可访问的 oStella API

### 2) 配置 `config.yml`

启动程序时,若不存在`config.yml`则会自动创建。请根据提示编辑 `config.yml`。
默认配置参见[seira-example-config.yml](/src/main/resources/seira-example-config.yml)

### 3) 启动

```shell
mvn -U clean compile exec:java
```

## 常用命令

> 所有命令都以 `/` 开头。指令详情请见[文档](https://docs.seira.top/overview/commands.html)。
Expand All @@ -93,16 +73,17 @@ mvn -U clean compile exec:java
| `/f` | `/f` | 获取好友列表 |
| `/fall` | `/fall` | 获取全部好友列表 |
| `/fclear` | `/fclear` | 清除好友记录 |
| `/bo` | `/bo [n] [uid/username/@user]` | 最好n个成绩图,无参时获取最佳成绩详情 |
| `/rs` | `/rs [n] [uid/username/@user]` | 最近n个成绩图,无参时获取最近一个成绩详情 |
| `/rp` | `/rp [n] [uid/username/@user]` | 最近n个通过成绩图,无参时获取最近一个通过成绩详情 |
| `/bo` | `/bo [n] [uid/username/@user] [filters...]` | 获取最多200条最好成绩后过滤;省略n时返回第一个匹配成绩详情 |
| `/rs` | `/rs [n] [uid/username/@user] [filters...]` | 获取最多200条最近成绩后过滤;省略n时返回第一个匹配成绩详情 |
| `/rp` | `/rp [n] [uid/username/@user] [filters...]` | 获取最多200条最近通过成绩后过滤;省略n时返回第一个匹配成绩详情 |
| `/tb` | `/tb [#days] [uid/username/@user]` | 获取近N天达成的BP(默认1天),并保留完整BP排名编号 |
| `/m` | `/m <id/rsN/boN> [Mod]` | 获取指定谱面信息 |
| `/ap` | `/ap <id/rsN/boN>` | 获取指定谱面音频预览 |
| `/bgp` | `/bgp <id/rsN/boN>` | 获取指定谱面背景预览 |
| `/s` | `/s <id/locId/rsN/boN>` | 获取指定的在线或本地成绩图 |
| `/sa` | `/sa <id/locId/rsN/boN>` | 获取指定成绩分析图 |
| `/ma` | `/ma [id/locId/rsN/boN] [n/#n]` | 获取指定或最近目标成绩的Miss分析;省略目标时用`#n`指定Miss |
| `/u` | `/u <uid/username/@user>` | 获取指定用户信息 |
| `/u` | `/u [uid/username/@user]` | 获取指定用户信息 |
| `/r` | `/r [id/locId/rsN/boN] [[mm:ss]-[mm:ss]]` | 生成并发送指定或最近目标的回放视频。省略范围时自动识别高光,使用`-`渲染整个回放 |
| `/rg` | `/rg <start/#Rank/end>` | 在群聊中开始、参与或结束猜 Rank 游戏 |
| `/rsc` | `/rsc [id/locId/rsN/boN] [+<id1>,<id2>...]` | 生成并发送指定或最近目标的成绩同屏回放视频;追加用户和范围顺序不限 |
Expand All @@ -115,6 +96,7 @@ mvn -U clean compile exec:java
| `/luck` | `/luck` | 今日人品 |
| `/mp` | `/mp` | 多人房间列表 |
| `/watch` | `/watch add/del/list [目标]` | 添加/删除/列出监视任务 |
| `/mpwatch` | `/mpwatch start/stop/status [目标]` | 按群成员添加、停止或查看多人房间监视;`stop all` 可停止本群全部监视 |
| `/wx` | `/wx start <UID列表> <谱面ID列表>` / `/wx stop` | 监视指定玩家在指定谱面取得的成绩,重启后自动恢复 |
| `/dcs` | `/dcs start <guild-id>.<channel-id>` / `/dcs stop` | 开启或解除当前 QQ 群与 Discord 频道的双向消息同步 |
| `/stat` | `/stat` | 服务状态和统计信息文本 |
Expand Down Expand Up @@ -169,3 +151,27 @@ mvn -U clean compile exec:java
| `/debug.db <sql>` | 对后端数据库执行指定的SQL语句 |
| `/debug.update-user-info` | 更新数据库中所有玩家的信息 |
| `/debug.active-message` | 发送一条主动消息 |

## 自行部署

> [!NOTE]
> 此节仅面向想自行部署 Seira 的用户。
> 若想直接使用,请见[使用指南](https://docs.seira.top/overview/use.html)

### 1) 准备环境

- JDK 25
- Maven
- QQ 开放平台机器人应用凭据
- 一个可访问的 oStella API

### 2) 配置 `config.yml`

启动程序时,若不存在`config.yml`则会自动创建。请根据提示编辑 `config.yml`。
默认配置参见[seira-example-config.yml](/src/main/resources/seira-example-config.yml)

### 3) 启动

```shell
mvn -U clean compile exec:java
```
13 changes: 7 additions & 6 deletions panel.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
{
"#note": "items should be no bigger than 20 elements",
"items": [
{
"type": "command",
"name": "/rp",
"desc": "查询最近通过成绩"
},
{
"type": "command",
"name": "/rs",
"desc": "查询通过成绩"
},
{
"type": "command",
"name": "/bind",
Expand All @@ -35,6 +31,11 @@
"name": "/bp",
"desc": "查询最好成绩"
},
{
"type": "command",
"name": "/tb",
"desc": "查询近期达成BP"
},
{
"type": "command",
"name": "/s",
Expand Down Expand Up @@ -97,4 +98,4 @@
}
],
"remark": "v1.10.0"
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.github.BotSeira</groupId>
<artifactId>SeiraCore</artifactId>
<version>1.10.1</version>
<version>1.11.0</version>

<properties>
<maven.compiler.source>25</maven.compiler.source>
Expand Down
117 changes: 102 additions & 15 deletions src/main/java/xyz/zcraft/seira/api/APIHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,38 @@ public static Response<UserExtended> getSelf(String accessToken) {
}

public static Response<Base64Bytes> getBoNResponse(int n, UserRef userRef) {
return getBoNResponse(n, userRef, List.of());
}

public static Response<Base64Bytes> getBoNResponse(int n, UserRef userRef, List<String> filters) {
long uid = resolveUid(userRef);
return getBase64BytesResponse("/users/" + uid + "/scores/bestof?" + "n=" + n, "获取最好成绩失败", null);
return getBase64BytesResponse(
"/users/" + uid + "/scores/bestof?n=" + n + encodeScoreFilters(filters),
"获取最好成绩失败",
null
);
}

public static Response<Base64Bytes> getUserInfoResponse(UserRef userRef) {
long uid = resolveUid(userRef);
return getBase64BytesResponse(
"/users/" + uid,
"获取玩家资料失败",
null
);
}

public static Response<Base64Bytes> getTodayBestResponse(UserRef userRef) {
return getTodayBestResponse(userRef, 1);
}

public static Response<Base64Bytes> getTodayBestResponse(UserRef userRef, int days) {
long uid = resolveUid(userRef);
return getBase64BytesResponse(
"/users/" + uid + "/scores/today-best?days=" + days,
"获取近期BP失败",
null
);
}

public static Response<Base64Bytes> getGroupLeaderboardResponse(ShortcutTarget target, List<Long> uids, String auth) {
Expand Down Expand Up @@ -187,15 +217,39 @@ public static Response<MultiplayerRoom> getMultiplayerRoom(String accessToken) {
}

public static Response<Base64Bytes> getRecentResponse(int n, UserRef userRef, boolean includeFail) {
return getRecentResponse(n, userRef, includeFail, List.of());
}

public static Response<Base64Bytes> getRecentResponse(int n, UserRef userRef, boolean includeFail, List<String> filters) {
long uid = resolveUid(userRef);
return getBase64BytesResponse("/users/" + uid + "/scores/recent" + "?n=" + n + "&fail=" + includeFail, "获取最近成绩失败", null);
return getBase64BytesResponse(
"/users/" + uid + "/scores/recent?n=" + n + "&fail=" + includeFail + encodeScoreFilters(filters),
"获取最近成绩失败",
null
);
}

private static String encodeScoreFilters(List<String> filters) {
if (filters == null || filters.isEmpty()) {
return "";
}
return "&filters=" + URLEncoder.encode(String.join(",", filters), StandardCharsets.UTF_8);
}

public static Response<Base64Bytes> getBeatmapResponse(ShortcutTarget target, String mod, String auth) {
final long beatmapId = lookupBeatmap(target, auth);
return getBase64BytesResponse("/beatmaps/" + beatmapId + (mod != null ? "?mod=" + mod : ""), "获取谱面失败", null);
}

public static Response<Base64Bytes> getBeatmapAnalysisResponse(ShortcutTarget target, String mod, String auth) {
final long beatmapId = lookupBeatmap(target, auth);
String query = "/beatmaps/" + beatmapId + "/analysis";
if (mod != null && !mod.isBlank()) {
query += "?mod=" + URLEncoder.encode(mod, StandardCharsets.UTF_8);
}
return getBase64BytesResponse(query, "获取谱面分析失败", null);
}

public static Response<Base64Bytes> getBeatmapsetBgResponse(ShortcutTarget target, String auth) {
final long beatmapsetId = lookupBeatmapset(target, auth);
return getBase64BytesResponse("/beatmapsets/" + beatmapsetId + "/background", "获取谱面集失败", null);
Expand Down Expand Up @@ -333,7 +387,11 @@ private static String getBeatmapsetQuery(ShortcutTarget target) {
}

public static Response<Base64Bytes> getScoreResponse(ShortcutTarget target) {
String scoreId = lookupScoreId(target);
return getScoreResponse(target, List.of());
}

public static Response<Base64Bytes> getScoreResponse(ShortcutTarget target, List<String> filters) {
String scoreId = lookupScoreId(target, filters);
return getBase64BytesResponse("/scores/" + scoreId, "获取成绩失败", null);
}

Expand Down Expand Up @@ -510,6 +568,25 @@ public static ReplayTaskInfo createReplayShowcaseTask(ShortcutTarget target, Str
return createReplayShowcaseTask(target, ids, auth, null);
}

public static ReplayTaskInfo createBeatmapPreviewTask(ShortcutTarget target, String mods, String auth,
QqUploadRequest qqUpload) {
long beatmapId = lookupBeatmap(target, auth);
JsonObject body = new JsonObject();
if (mods != null && !mods.isBlank()) {
body.addProperty("mods", mods);
}
if (qqUpload != null) {
body.add("qqUpload", GSON.toJsonTree(qqUpload));
}

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(ENDPOINT + "/replays/renders/preview/" + beatmapId))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build();
return getReplayTaskInfo(request);
}

public static ReplayTaskInfo createReplayShowcaseTask(ShortcutTarget target, String[] ids, String auth,
QqUploadRequest qqUpload) {
ids = ids == null ? new String[0] : ids;
Expand Down Expand Up @@ -539,13 +616,9 @@ public static ReplayTaskInfo createReplayShowcaseTask(ShortcutTarget target, Str
}

public static ReplayRenderResult waitReplayVideo(String taskId) {
try {
FileInfo qqFile = waitReplayDone(taskId);
return new ReplayRenderResult(
ENDPOINT + "/replays/" + taskId + "/video/replay.mp4", taskId, qqFile);
} catch (RuntimeException _) {
return null;
}
FileInfo qqFile = waitReplayDone(taskId);
return new ReplayRenderResult(
ENDPOINT + "/replays/" + taskId + "/video/replay.mp4", taskId, qqFile);
}

private static ReplayTaskInfo createReplayTask(ShortcutTarget target,
Expand Down Expand Up @@ -597,14 +670,18 @@ private static TimeDurationParser.TimeRange getScoreHighlight(String scoreId, in
}

private static String lookupScoreId(ShortcutTarget target) {
return lookupScoreId(target, List.of());
}

private static String lookupScoreId(ShortcutTarget target, List<String> filters) {
String scoreId;
if (target.isLocalScore()) {
scoreId = target.localScoreId();
} else if (!target.isMacro()) {
scoreId = String.valueOf(target.explicitId());
} else {
try {
final String query = getScoreQuery(target);
final String query = getScoreQuery(target) + encodeScoreFilters(filters);

HttpRequest localRequest = HttpRequest.newBuilder()
.uri(URI.create(ENDPOINT + query))
Expand Down Expand Up @@ -678,8 +755,11 @@ private static ReplayTaskInfo getReplayTaskInfo(HttpRequest request) {

Double start = data.has("start") ? data.get("start").getAsDouble() : null;
Double end = data.has("end") ? data.get("end").getAsDouble() : null;
String mods = data.has("mods") ? data.get("mods").getAsString() : null;
String selection = data.has("selection") ? data.get("selection").getAsString() : null;

return new ReplayTaskInfo(taskId, status, position, beatmap, data.getAsJsonArray("scores"), start, end);
return new ReplayTaskInfo(
taskId, status, position, beatmap, data.getAsJsonArray("scores"), start, end, mods, selection);
} catch (IOException e) {
throw requestFailure(e);
} catch (InterruptedException e) {
Expand All @@ -697,8 +777,13 @@ private static FileInfo waitReplayDone(String taskId) {
? GSON.fromJson(statusData.getAsJsonObject("qqFile"), FileInfo.class)
: null;
}
if ("failed".equalsIgnoreCase(status) || "canceled".equalsIgnoreCase(status)) {
throw new RuntimeException("回放渲染失败,状态:" + status);
if ("failed".equalsIgnoreCase(status)
|| "timeout".equalsIgnoreCase(status)
|| "canceled".equalsIgnoreCase(status)) {
String error = statusData.has("error") && !statusData.get("error").isJsonNull()
? statusData.get("error").getAsString()
: null;
throw new ReplayRenderException(status, error);
}
try {
Thread.sleep(REPLAY_POLL_INTERVAL_MS);
Expand Down Expand Up @@ -1030,7 +1115,9 @@ public record ReplayTaskInfo(
BeatmapExtended beatmap,
JsonArray scores,
Double start,
Double end
Double end,
String mods,
String selection
) {
}
}
14 changes: 14 additions & 0 deletions src/main/java/xyz/zcraft/seira/api/ReplayRenderException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package xyz.zcraft.seira.api;

public final class ReplayRenderException extends RuntimeException {
public ReplayRenderException(String status, String error) {
super(formatMessage(status, error));
}

static String formatMessage(String status, String error) {
if (error != null && !error.isBlank()) {
return "回放渲染失败:" + error.trim();
}
return "回放渲染失败,状态:" + (status == null || status.isBlank() ? "failed" : status);
}
}
1 change: 1 addition & 0 deletions src/main/java/xyz/zcraft/seira/api/data/RenderStat.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public class RenderStat {
private String progress;
private String speed;
private String eta;
private String error;
}
Loading
Loading