mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2025-12-12 09:18:14 +08:00
@@ -1,6 +1,6 @@
|
|||||||
//! @file src/client.rs
|
//! @file src/client.rs
|
||||||
//! @desc 客户端模块,负责操作米游社客户端
|
//! @desc 客户端模块,负责操作米游社客户端
|
||||||
//! @since Beta v0.4.0
|
//! @since Beta v0.4.3
|
||||||
|
|
||||||
use tauri::{
|
use tauri::{
|
||||||
AppHandle, CustomMenuItem, LogicalSize, Manager, Menu, Size, Submenu, WindowBuilder, WindowUrl,
|
AppHandle, CustomMenuItem, LogicalSize, Manager, Menu, Size, Submenu, WindowBuilder, WindowUrl,
|
||||||
@@ -22,11 +22,16 @@ fn create_utils_menu() -> Menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建米游社客户端菜单
|
// 创建米游社客户端菜单
|
||||||
fn create_mhy_menu() -> Menu {
|
fn create_mhy_menu(func: String) -> Menu {
|
||||||
let top = CustomMenuItem::new("top".to_string(), "置顶");
|
let top = CustomMenuItem::new("top".to_string(), "置顶");
|
||||||
let cancel_top = CustomMenuItem::new("cancel_top".to_string(), "取消置顶");
|
let cancel_top = CustomMenuItem::new("cancel_top".to_string(), "取消置顶");
|
||||||
|
let sign_in = CustomMenuItem::new("sign_in".to_string(), "用户登录");
|
||||||
let open_post = CustomMenuItem::new("open_post".to_string(), "打开帖子");
|
let open_post = CustomMenuItem::new("open_post".to_string(), "打开帖子");
|
||||||
let utils_menu = Submenu::new("工具".to_string(), create_utils_menu());
|
let utils_menu = Submenu::new("工具".to_string(), create_utils_menu());
|
||||||
|
// 如果是登录
|
||||||
|
if func == "config_sign_in" {
|
||||||
|
return Menu::new().add_item(sign_in);
|
||||||
|
}
|
||||||
return Menu::new()
|
return Menu::new()
|
||||||
.add_item(top)
|
.add_item(top)
|
||||||
.add_item(cancel_top)
|
.add_item(cancel_top)
|
||||||
@@ -75,7 +80,7 @@ pub async fn create_mhy_client(handle: AppHandle, func: String, url: String) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WindowBuilder::from_config(&handle, mhy_window_config)
|
WindowBuilder::from_config(&handle, mhy_window_config)
|
||||||
.menu(create_mhy_menu())
|
.menu(create_mhy_menu(func))
|
||||||
.build()
|
.build()
|
||||||
.expect("failed to create mhy_client")
|
.expect("failed to create mhy_client")
|
||||||
.on_menu_event(move |event| match event.menu_item_id() {
|
.on_menu_event(move |event| match event.menu_item_id() {
|
||||||
@@ -164,6 +169,24 @@ pub async fn create_mhy_client(handle: AppHandle, func: String, url: String) {
|
|||||||
window.center().unwrap();
|
window.center().unwrap();
|
||||||
window.set_focus().unwrap();
|
window.set_focus().unwrap();
|
||||||
}
|
}
|
||||||
|
"sign_in" => {
|
||||||
|
let window = handle.get_window("mhy_client").unwrap();
|
||||||
|
let execute_js = r#"javascript:(async function(){
|
||||||
|
// 首先检测是不是 user.mihoyo.com
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if(url.hostname !== "user.mihoyo.com"){
|
||||||
|
alert("当前页面不是米游社登录页面");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ck = document.cookie;
|
||||||
|
const arg = {
|
||||||
|
method: 'teyvat_sign_in',
|
||||||
|
payload: ck,
|
||||||
|
}
|
||||||
|
await window.__TAURI__.event.emit('post_mhy_client',JSON.stringify(arg));
|
||||||
|
})()"#;
|
||||||
|
window.eval(&execute_js).ok().unwrap();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,11 @@
|
|||||||
"windows": ["mhy_client"],
|
"windows": ["mhy_client"],
|
||||||
"enableTauriAPI": true
|
"enableTauriAPI": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"domain": "user.mihoyo.com",
|
||||||
|
"windows": ["mhy_client"],
|
||||||
|
"enableTauriAPI": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"domain": "webstatic.mihoyo.com",
|
"domain": "webstatic.mihoyo.com",
|
||||||
"windows": ["mhy_client"],
|
"windows": ["mhy_client"],
|
||||||
|
|||||||
@@ -10,17 +10,21 @@
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-btn variant="outlined" @click="scan = true" icon="mdi-qrcode-scan" />
|
<v-btn variant="outlined" @click="scan = true" icon="mdi-qrcode-scan" />
|
||||||
|
<v-btn variant="outlined" @click="toWebLogin" icon="mdi-web" />
|
||||||
<v-btn variant="outlined" @click="confirmRefreshUser" icon="mdi-refresh" :loading="loading" />
|
<v-btn variant="outlined" @click="confirmRefreshUser" icon="mdi-refresh" :loading="loading" />
|
||||||
</template>
|
</template>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { event, window as windowTauri } from "@tauri-apps/api";
|
||||||
|
import type { UnlistenFn } from "@tauri-apps/api/helpers/event";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
|
|
||||||
import TGSqlite from "../../plugins/Sqlite";
|
import TGSqlite from "../../plugins/Sqlite";
|
||||||
import { useAppStore } from "../../store/modules/app";
|
import { useAppStore } from "../../store/modules/app";
|
||||||
import { useUserStore } from "../../store/modules/user";
|
import { useUserStore } from "../../store/modules/user";
|
||||||
|
import TGClient from "../../utils/TGClient";
|
||||||
import TGLogger from "../../utils/TGLogger";
|
import TGLogger from "../../utils/TGLogger";
|
||||||
import { getDeviceFp } from "../../web/request/getDeviceFp";
|
import { getDeviceFp } from "../../web/request/getDeviceFp";
|
||||||
import TGRequest from "../../web/request/TGRequest";
|
import TGRequest from "../../web/request/TGRequest";
|
||||||
@@ -46,12 +50,175 @@ const userInfo = ref<TGApp.App.Account.BriefInfo>({
|
|||||||
avatar: "/source/UI/lumine.webp",
|
avatar: "/source/UI/lumine.webp",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let signListener: UnlistenFn;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (userStore.briefInfo.value && userStore.briefInfo.value.nickname) {
|
if (userStore.briefInfo.value && userStore.briefInfo.value.nickname) {
|
||||||
userInfo.value = userStore.briefInfo.value;
|
userInfo.value = userStore.briefInfo.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function toWebLogin(): Promise<void> {
|
||||||
|
const confirm = await showConfirm({
|
||||||
|
title: "请在子窗口中完成登录操作",
|
||||||
|
text: "请操作完成后点击子窗口的“用户登录”菜单",
|
||||||
|
});
|
||||||
|
if (!confirm) {
|
||||||
|
showSnackbar({
|
||||||
|
color: "cancel",
|
||||||
|
text: "已取消登录",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await TGClient.open("config_sign_in", "https://user.mihoyo.com");
|
||||||
|
signListener = await event.listen("config_user_sign", async (e) => {
|
||||||
|
if (typeof e.payload !== "string") {
|
||||||
|
showSnackbar({
|
||||||
|
color: "error",
|
||||||
|
text: "登录失败!",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await getTokenWeb(e.payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTokenWeb(cookie: string): Promise<void> {
|
||||||
|
await windowTauri.appWindow.setFocus();
|
||||||
|
emits("loadOuter", { show: true, title: "正在解析 cookie" });
|
||||||
|
const ck = cookie.split(";").reduce(
|
||||||
|
(prev, curr) => {
|
||||||
|
const [key, value] = curr.split("=");
|
||||||
|
prev[key.trim()] = value.trim();
|
||||||
|
return prev;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
|
if (!("login_ticket" in ck) || !("login_uid" in ck)) {
|
||||||
|
emits("loadOuter", { show: false });
|
||||||
|
showSnackbar({
|
||||||
|
text: "未检测到 login_ticket, 请确认是否登录成功!",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
setTimeout(() => {
|
||||||
|
windowTauri.WebviewWindow.getByLabel("mhy_client")?.setFocus();
|
||||||
|
}, 2000);
|
||||||
|
} catch (e) {
|
||||||
|
await TGLogger.Error("[tc-userBadge][getTokenWeb] 无法获取子窗口");
|
||||||
|
await TGClient.open("config_sign_in", "https://user.mihoyo.com/");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emits("loadOuter", { show: true, title: "正在获取 token" });
|
||||||
|
let cookieUser: TGApp.User.Account.Cookie = {
|
||||||
|
account_id: "",
|
||||||
|
ltuid: "",
|
||||||
|
stuid: "",
|
||||||
|
mid: "",
|
||||||
|
cookie_token: "",
|
||||||
|
stoken: "",
|
||||||
|
ltoken: "",
|
||||||
|
};
|
||||||
|
cookieUser.account_id = ck["login_uid"];
|
||||||
|
cookieUser.ltuid = ck["login_uid"];
|
||||||
|
cookieUser.stuid = ck["login_uid"];
|
||||||
|
const tokenRes = await TGRequest.User.byLoginTicket.getTokens(
|
||||||
|
ck["login_ticket"],
|
||||||
|
ck["login_uid"],
|
||||||
|
);
|
||||||
|
if ("retcode" in tokenRes) {
|
||||||
|
emits("loadOuter", { show: false });
|
||||||
|
showSnackbar({
|
||||||
|
text: "获取 token 失败!",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
await TGLogger.Error("[tc-userBadge][getTokenWeb] 获取 token 失败");
|
||||||
|
await TGLogger.Error(`[tc-userBadge][getTokenWeb] ${tokenRes.retcode}: ${tokenRes.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tokenRes.map((i) => {
|
||||||
|
if (i.name === "ltoken") return (cookieUser.ltoken = i.token);
|
||||||
|
if (i.name === "stoken") return (cookieUser.stoken = i.token);
|
||||||
|
});
|
||||||
|
if (cookieUser.ltoken === "" || cookieUser.stoken === "") {
|
||||||
|
emits("loadOuter", { show: false });
|
||||||
|
showSnackbar({
|
||||||
|
text: "获取 ltoken 或者 stoken 失败!",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
await TGLogger.Error("[tc-userBadge][getTokenWeb] 获取 ltoken 或者 stoken 失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ltokenRes = await TGRequest.User.byLToken.verify(cookieUser.ltoken, cookieUser.ltuid);
|
||||||
|
if (typeof ltokenRes !== "string") {
|
||||||
|
showSnackbar({
|
||||||
|
text: "ltoken 验证失败!",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cookieUser.mid = ltokenRes;
|
||||||
|
}
|
||||||
|
if (!cookieUser.stoken.startsWith("v2")) {
|
||||||
|
const stokenRes = await TGRequest.User.bySToken.update(cookieUser.stoken, cookieUser.stuid);
|
||||||
|
if ("retcode" in stokenRes) {
|
||||||
|
showSnackbar({
|
||||||
|
text: "stoken 更新失败!",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
await TGLogger.Error("[tc-userBadge][getTokenWeb] stoken 更新失败");
|
||||||
|
await TGLogger.Error(
|
||||||
|
`[tc-userBadge][getTokenWeb] ${stokenRes.retcode}: ${stokenRes.message}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cookieUser.stoken = stokenRes.token.token;
|
||||||
|
if (cookieUser.mid === "" && stokenRes.user_info.mid !== "")
|
||||||
|
cookieUser.mid = stokenRes.user_info.mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cookieTokenRes = await TGRequest.User.bySToken.getCookieToken(
|
||||||
|
cookieUser.mid,
|
||||||
|
cookieUser.stoken,
|
||||||
|
);
|
||||||
|
if (typeof cookieTokenRes !== "string") {
|
||||||
|
showSnackbar({
|
||||||
|
text: "cookie_token 获取失败!",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
await TGLogger.Error("[tc-userBadge][getTokenWeb] cookie_token 获取失败");
|
||||||
|
await TGLogger.Error(
|
||||||
|
`[tc-userBadge][getTokenWeb] ${cookieTokenRes.retcode}: ${cookieTokenRes.message}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cookieUser.cookie_token = cookieTokenRes;
|
||||||
|
}
|
||||||
|
userStore.cookie.value = cookieUser;
|
||||||
|
try {
|
||||||
|
await windowTauri.WebviewWindow.getByLabel("mhy_client")?.close();
|
||||||
|
} catch (e) {
|
||||||
|
await TGLogger.Error("[tc-userBadge][getTokenWeb] 无法获取子窗口");
|
||||||
|
showSnackbar({
|
||||||
|
text: "请手动关闭子窗口!",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
signListener();
|
||||||
|
emits("loadOuter", { show: false });
|
||||||
|
// 检测cookie是否每项都有
|
||||||
|
if (Object.values(cookieUser).some((i) => i === "")) {
|
||||||
|
showSnackbar({
|
||||||
|
text: "获取 cookie 失败!部分项为空!",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
await TGLogger.Error("[tc-userBadge][getTokenWeb] 获取 cookie 失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showSnackbar({
|
||||||
|
text: "登录成功!",
|
||||||
|
color: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshUser() {
|
async function refreshUser() {
|
||||||
const ck = userStore.cookie.value;
|
const ck = userStore.cookie.value;
|
||||||
if (ck === undefined || JSON.stringify(ck) === "{}") {
|
if (ck === undefined || JSON.stringify(ck) === "{}") {
|
||||||
@@ -194,6 +361,10 @@ async function confirmRefreshUser(): Promise<void> {
|
|||||||
}
|
}
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (signListener) signListener();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
.tcu-box {
|
.tcu-box {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<TOverlay v-model="visible" hide blur-val="20px" :to-click="onCancel">
|
<TOverlay v-model="visible" hide blur-val="20px" :to-click="onCancel">
|
||||||
<div class="tog-box">
|
<div class="tog-box">
|
||||||
<div class="tog-top">
|
<div class="tog-top">
|
||||||
<div class="tog-title">请使用米游社APP进行扫码操作</div>
|
<div class="tog-title">请使用原神进行扫码操作</div>
|
||||||
<div class="tog-subtitle">所需米游社版本 >= 2.57.1</div>
|
<div class="tog-subtitle">仅支持官服,渠道服请使用网页登录</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tog-mid">
|
<div class="tog-mid">
|
||||||
<qrcode-vue
|
<qrcode-vue
|
||||||
@@ -24,6 +24,7 @@ import { computed, onUnmounted, reactive, ref, watch } from "vue";
|
|||||||
|
|
||||||
import Mys from "../../plugins/Mys";
|
import Mys from "../../plugins/Mys";
|
||||||
import { useUserStore } from "../../store/modules/user";
|
import { useUserStore } from "../../store/modules/user";
|
||||||
|
import TGLogger from "../../utils/TGLogger";
|
||||||
import TGRequest from "../../web/request/TGRequest";
|
import TGRequest from "../../web/request/TGRequest";
|
||||||
import showSnackbar from "../func/snackbar";
|
import showSnackbar from "../func/snackbar";
|
||||||
import TOverlay from "../main/t-overlay.vue";
|
import TOverlay from "../main/t-overlay.vue";
|
||||||
@@ -58,7 +59,6 @@ const cookie = reactive<TGApp.User.Account.Cookie>({
|
|||||||
ltuid: "",
|
ltuid: "",
|
||||||
stuid: "",
|
stuid: "",
|
||||||
mid: "",
|
mid: "",
|
||||||
game_token: "",
|
|
||||||
cookie_token: "",
|
cookie_token: "",
|
||||||
stoken: "",
|
stoken: "",
|
||||||
ltoken: "",
|
ltoken: "",
|
||||||
@@ -113,11 +113,10 @@ async function cycleGetData() {
|
|||||||
color: "error",
|
color: "error",
|
||||||
});
|
});
|
||||||
if (res.retcode === -106) {
|
if (res.retcode === -106) {
|
||||||
// 二维码过期
|
|
||||||
await freshQr();
|
await freshQr();
|
||||||
} else {
|
} else {
|
||||||
// 取消轮询
|
clearInterval(cycleTimer);
|
||||||
if (cycleTimer) clearInterval(cycleTimer);
|
cycleTimer = null;
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -126,14 +125,22 @@ async function cycleGetData() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (res.stat === "Confirmed") {
|
if (res.stat === "Confirmed") {
|
||||||
// 取消轮询
|
clearInterval(cycleTimer);
|
||||||
if (cycleTimer) clearInterval(cycleTimer);
|
cycleTimer = null;
|
||||||
|
if (res.payload.proto !== "OpenToken") {
|
||||||
|
await TGLogger.Warn(`[to-gameLogin] 检测到非Combo协议:${res.payload.proto}`);
|
||||||
|
showSnackbar({
|
||||||
|
text: "请使用原神进行扫码操作",
|
||||||
|
color: "error",
|
||||||
|
});
|
||||||
|
visible.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data: TGApp.Plugins.Mys.GameLogin.StatusPayloadRaw = JSON.parse(res.payload.raw);
|
const data: TGApp.Plugins.Mys.GameLogin.StatusPayloadRaw = JSON.parse(res.payload.raw);
|
||||||
cookie.account_id = data.uid;
|
cookie.account_id = data.uid;
|
||||||
cookie.ltuid = data.uid;
|
cookie.ltuid = data.uid;
|
||||||
cookie.stuid = data.uid;
|
cookie.stuid = data.uid;
|
||||||
cookie.game_token = data.token;
|
await getTokens(data.open_token);
|
||||||
await getTokens();
|
|
||||||
showSnackbar({
|
showSnackbar({
|
||||||
text: "登录成功",
|
text: "登录成功",
|
||||||
color: "success",
|
color: "success",
|
||||||
@@ -145,27 +152,29 @@ async function cycleGetData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTokens(): Promise<void> {
|
async function getTokens(game_token: string): Promise<void> {
|
||||||
const stokenRes = await TGRequest.User.bgGameToken.getStoken(
|
const stokenRes = await TGRequest.User.bgGameToken.getStoken(cookie.account_id, game_token);
|
||||||
cookie.account_id,
|
|
||||||
cookie.game_token,
|
|
||||||
);
|
|
||||||
if (!("retcode" in stokenRes)) {
|
if (!("retcode" in stokenRes)) {
|
||||||
cookie.stoken = stokenRes.token.token;
|
cookie.stoken = stokenRes.token.token;
|
||||||
cookie.mid = stokenRes.user_info.mid;
|
cookie.mid = stokenRes.user_info.mid;
|
||||||
|
} else {
|
||||||
|
await TGLogger.Error("[to-gameLogin] 获取stoken失败");
|
||||||
}
|
}
|
||||||
const cookieTokenRes = await TGRequest.User.bgGameToken.getCookieToken(
|
const cookieTokenRes = await TGRequest.User.bgGameToken.getCookieToken(
|
||||||
cookie.account_id,
|
cookie.account_id,
|
||||||
cookie.game_token,
|
game_token,
|
||||||
);
|
);
|
||||||
if (typeof cookieTokenRes === "string") cookie.cookie_token = cookieTokenRes;
|
if (typeof cookieTokenRes === "string") cookie.cookie_token = cookieTokenRes;
|
||||||
|
else await TGLogger.Error("[to-gameLogin] 获取cookieToken失败");
|
||||||
const ltokenRes = await TGRequest.User.bySToken.getLToken(cookie.mid, cookie.stoken);
|
const ltokenRes = await TGRequest.User.bySToken.getLToken(cookie.mid, cookie.stoken);
|
||||||
if (typeof ltokenRes === "string") cookie.ltoken = ltokenRes;
|
if (typeof ltokenRes === "string") cookie.ltoken = ltokenRes;
|
||||||
|
else await TGLogger.Error("[to-gameLogin] 获取ltoken失败");
|
||||||
userStore.cookie.value = cookie;
|
userStore.cookie.value = cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (cycleTimer) clearInterval(cycleTimer);
|
if (cycleTimer !== null) clearInterval(cycleTimer);
|
||||||
|
cycleTimer = null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* @file plugins/Mys/utils/doGameLogin
|
* @file plugins/Mys/utils/doGameLogin
|
||||||
* @description 获取 gameToken,曲线获取 stoken
|
* @description 获取 gameToken,曲线获取 stoken
|
||||||
* @since Beta v0.3.0
|
* @since Beta v0.4.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { http } from "@tauri-apps/api";
|
import { http } from "@tauri-apps/api";
|
||||||
|
|
||||||
const device = crypto.randomUUID();
|
import { getDeviceInfo } from "../../../utils/toolFunc";
|
||||||
|
import { getRequestHeader } from "../../../web/utils/getRequestHeader";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 获取登录二维码
|
* @description 获取登录二维码
|
||||||
* @since Beta v0.3.0
|
* @since Beta v0.4.3
|
||||||
* @returns {Promise<TGApp.Plugins.Mys.GameLogin.GetLoginQrData|TGApp.BBS.Response.Base>}
|
* @returns {Promise<TGApp.Plugins.Mys.GameLogin.GetLoginQrData|TGApp.BBS.Response.Base>}
|
||||||
*/
|
*/
|
||||||
export async function getLoginQr(): Promise<
|
export async function getLoginQr(): Promise<
|
||||||
TGApp.Plugins.Mys.GameLogin.GetLoginQrData | TGApp.BBS.Response.Base
|
TGApp.Plugins.Mys.GameLogin.GetLoginQrData | TGApp.BBS.Response.Base
|
||||||
> {
|
> {
|
||||||
const url = "https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/fetch";
|
const url = "https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/fetch";
|
||||||
|
const device = getDeviceInfo("device_id");
|
||||||
const data = {
|
const data = {
|
||||||
app_id: "4",
|
app_id: "4",
|
||||||
device,
|
device,
|
||||||
};
|
};
|
||||||
|
const header = getRequestHeader({}, "POST", data, "common");
|
||||||
return await http
|
return await http
|
||||||
.fetch<TGApp.Plugins.Mys.GameLogin.GetLoginQrResponse | TGApp.BBS.Response.Base>(url, {
|
.fetch<TGApp.Plugins.Mys.GameLogin.GetLoginQrResponse | TGApp.BBS.Response.Base>(url, {
|
||||||
|
headers: header,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: http.Body.json(data),
|
body: http.Body.json(data),
|
||||||
})
|
})
|
||||||
@@ -34,7 +38,7 @@ export async function getLoginQr(): Promise<
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 获取登录状态
|
* @description 获取登录状态
|
||||||
* @since Beta v0.3.0
|
* @since Beta v0.4.3
|
||||||
* @param {string} ticket 二维码 ticket
|
* @param {string} ticket 二维码 ticket
|
||||||
* @returns {Promise<TGApp.Plugins.Mys.GameLogin.GetLoginStatusData | TGApp.BBS.Response.Base>}
|
* @returns {Promise<TGApp.Plugins.Mys.GameLogin.GetLoginStatusData | TGApp.BBS.Response.Base>}
|
||||||
*/
|
*/
|
||||||
@@ -42,9 +46,12 @@ export async function getLoginStatus(
|
|||||||
ticket: string,
|
ticket: string,
|
||||||
): Promise<TGApp.Plugins.Mys.GameLogin.GetLoginStatusData | TGApp.BBS.Response.Base> {
|
): Promise<TGApp.Plugins.Mys.GameLogin.GetLoginStatusData | TGApp.BBS.Response.Base> {
|
||||||
const url = "https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/query";
|
const url = "https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/query";
|
||||||
|
const device = getDeviceInfo("device_id");
|
||||||
const data = { app_id: "4", device, ticket };
|
const data = { app_id: "4", device, ticket };
|
||||||
|
const header = getRequestHeader({}, "POST", data, "common");
|
||||||
return await http
|
return await http
|
||||||
.fetch<TGApp.Plugins.Mys.GameLogin.GetLoginStatusResponse | TGApp.BBS.Response.Base>(url, {
|
.fetch<TGApp.Plugins.Mys.GameLogin.GetLoginStatusResponse | TGApp.BBS.Response.Base>(url, {
|
||||||
|
headers: header,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: http.Body.json(data),
|
body: http.Body.json(data),
|
||||||
})
|
})
|
||||||
|
|||||||
8
src/plugins/Mys/types/GameLogin.d.ts
vendored
8
src/plugins/Mys/types/GameLogin.d.ts
vendored
@@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* @file plugins/Mys/types/GameLogin.d.ts
|
* @file plugins/Mys/types/GameLogin.d.ts
|
||||||
* @description Mys 插件 Game 登录类型定义文件
|
* @description Mys 插件 Game 登录类型定义文件
|
||||||
* @since Beta v0.3.0
|
* @since Beta v0.4.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Mys 插件 Game 登录类型
|
* @description Mys 插件 Game 登录类型
|
||||||
* @since Beta v0.3.0
|
* @since Beta v0.4.3
|
||||||
* @namespace TGApp.Plugins.Mys.GameLogin
|
* @namespace TGApp.Plugins.Mys.GameLogin
|
||||||
* @memberof TGApp.Plugins.Mys
|
* @memberof TGApp.Plugins.Mys
|
||||||
*/
|
*/
|
||||||
@@ -76,7 +76,7 @@ declare namespace TGApp.Plugins.Mys.GameLogin {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 反序列化后的登录状态数据
|
* @description 反序列化后的登录状态数据
|
||||||
* @since Beta v0.3.0
|
* @since Beta v0.4.3
|
||||||
* @interface StatusPayloadRaw
|
* @interface StatusPayloadRaw
|
||||||
* @property {string} uid 用户 UID
|
* @property {string} uid 用户 UID
|
||||||
* @property {string} token 用户 token
|
* @property {string} token 用户 token
|
||||||
@@ -84,6 +84,6 @@ declare namespace TGApp.Plugins.Mys.GameLogin {
|
|||||||
*/
|
*/
|
||||||
interface StatusPayloadRaw {
|
interface StatusPayloadRaw {
|
||||||
uid: string;
|
uid: string;
|
||||||
token: string;
|
open_token: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/types/BBS/Response.d.ts
vendored
40
src/types/BBS/Response.d.ts
vendored
@@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* @file types/BBS/Response.d.ts
|
* @file types/BBS/Response.d.ts
|
||||||
* @description BBS 返回数据类型定义文件
|
* @description BBS 返回数据类型定义文件
|
||||||
* @since Beta v0.3.9
|
* @since Beta v0.4.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description BBS 返回数据类型定义
|
* @description BBS 返回数据类型定义
|
||||||
* @since Beta v0.3.6
|
* @since Beta v0.4.3
|
||||||
* @namespace TGApp.BBS.Response
|
* @namespace TGApp.BBS.Response
|
||||||
* @memberof TGApp.BBS
|
* @memberof TGApp.BBS
|
||||||
*/
|
*/
|
||||||
@@ -149,6 +149,42 @@ declare namespace TGApp.BBS.Response {
|
|||||||
need_realperson: boolean;
|
need_realperson: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 通过 stoken v1 获取 stoken v2 的返回类型
|
||||||
|
* @interface getTokenBySToken
|
||||||
|
* @since Beta v0.4.3
|
||||||
|
* @extends BaseWithData
|
||||||
|
* @property {getStokenByGameTokenData} data - 返回数据
|
||||||
|
* @return getTokenBySToken
|
||||||
|
*/
|
||||||
|
interface getTokenBySToken extends BaseWithData {
|
||||||
|
data: getStokenByGameTokenData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 通过 stoken v1 获取 stoken v2 的返回类型数据
|
||||||
|
* @interface getTokenBySTokenData
|
||||||
|
* @since Beta v0.4.3
|
||||||
|
* @property {unknown} need_realperson - 是否需要实名认证
|
||||||
|
* @property {string} token.token - token 值
|
||||||
|
* @property {number} token.token_type - token 类型
|
||||||
|
* @description user_info 只写了用到的字段
|
||||||
|
* @property {string} user_info.aid - 用户 aid
|
||||||
|
* @property {string} user_info.mid - 用户 mid
|
||||||
|
* @returns getTokenBySTokenData
|
||||||
|
*/
|
||||||
|
interface getTokenBySTokenData {
|
||||||
|
need_realperson: boolean;
|
||||||
|
token: {
|
||||||
|
token: string;
|
||||||
|
token_type: number;
|
||||||
|
};
|
||||||
|
user_info: {
|
||||||
|
aid: string;
|
||||||
|
mid: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 通过 gameToken 获取 cookie_token 的返回类型
|
* @description 通过 gameToken 获取 cookie_token 的返回类型
|
||||||
* @interface getCookieTokenByGameToken
|
* @interface getCookieTokenByGameToken
|
||||||
|
|||||||
8
src/types/User/Account.d.ts
vendored
8
src/types/User/Account.d.ts
vendored
@@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* @file types/User/Account.d.ts
|
* @file types/User/Account.d.ts
|
||||||
* @description 用户账号相关类型定义文件
|
* @description 用户账号相关类型定义文件
|
||||||
* @since Beta v0.3.8
|
* @since Beta v0.4.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 用户账号相关类型定义命名空间
|
* @description 用户账号相关类型定义命名空间
|
||||||
* @since Beta v0.3.8
|
* @since Beta v0.4.3
|
||||||
* @namespace TGApp.User.Account
|
* @namespace TGApp.User.Account
|
||||||
* @memberof TGApp.User
|
* @memberof TGApp.User
|
||||||
*/
|
*/
|
||||||
@@ -52,12 +52,11 @@ declare namespace TGApp.User.Account {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 用户 Cookie 类型
|
* @description 用户 Cookie 类型
|
||||||
* @since Beta v0.3.8
|
* @since Beta v0.4.3
|
||||||
* @interface Cookie
|
* @interface Cookie
|
||||||
* @memberof TGApp.User.Account
|
* @memberof TGApp.User.Account
|
||||||
* @property {string} account_id 账号 ID
|
* @property {string} account_id 账号 ID
|
||||||
* @property {string} cookie_token Cookie Token
|
* @property {string} cookie_token Cookie Token
|
||||||
* @property {string} game_token 游戏 Token
|
|
||||||
* @property {string} ltoken LToken
|
* @property {string} ltoken LToken
|
||||||
* @property {string} ltuid LTUID
|
* @property {string} ltuid LTUID
|
||||||
* @property {string} mid MID
|
* @property {string} mid MID
|
||||||
@@ -67,7 +66,6 @@ declare namespace TGApp.User.Account {
|
|||||||
interface Cookie {
|
interface Cookie {
|
||||||
account_id: string;
|
account_id: string;
|
||||||
cookie_token: string;
|
cookie_token: string;
|
||||||
game_token: string;
|
|
||||||
ltoken: string;
|
ltoken: string;
|
||||||
ltuid: string;
|
ltuid: string;
|
||||||
mid: string;
|
mid: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @file utils/TGClient.ts
|
* @file utils/TGClient.ts
|
||||||
* @desc 负责米游社客户端的 callback 处理
|
* @desc 负责米游社客户端的 callback 处理
|
||||||
* @since Beta v0.4.2
|
* @since Beta v0.4.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { event, invoke } from "@tauri-apps/api";
|
import { event, invoke } from "@tauri-apps/api";
|
||||||
@@ -28,7 +28,7 @@ interface InvokeArg {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @class TGClient
|
* @class TGClient
|
||||||
* @since Beta v0.3.9
|
* @since Beta v0.4.3
|
||||||
* @description 米游社客户端
|
* @description 米游社客户端
|
||||||
*/
|
*/
|
||||||
class TGClient {
|
class TGClient {
|
||||||
@@ -272,7 +272,7 @@ class TGClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @func handleCustomCallback
|
* @func handleCustomCallback
|
||||||
* @since Beta v0.3.9
|
* @since Beta v0.4.3
|
||||||
* @desc 处理自定义的 callback
|
* @desc 处理自定义的 callback
|
||||||
* @param {TGApp.Plugins.JSBridge.Arg<any>} arg - 事件参数
|
* @param {TGApp.Plugins.JSBridge.Arg<any>} arg - 事件参数
|
||||||
* @returns {Promise<void>} - 返回值
|
* @returns {Promise<void>} - 返回值
|
||||||
@@ -294,6 +294,10 @@ class TGClient {
|
|||||||
await this.loadJSBridge();
|
await this.loadJSBridge();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "teyvat_sign_in": {
|
||||||
|
await event.emit("config_user_sign", arg.payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "teyvat_touch": {
|
case "teyvat_touch": {
|
||||||
const executeJS = `javascript:(() => {
|
const executeJS = `javascript:(() => {
|
||||||
// 鼠标移动监听
|
// 鼠标移动监听
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @file web/request/TGRequest.ts
|
* @file web/request/TGRequest.ts
|
||||||
* @description 应用用到的请求函数
|
* @description 应用用到的请求函数
|
||||||
* @since Beta v0.3.6
|
* @since Beta v0.4.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { genAuthkey, genAuthkey2 } from "./genAuthkey";
|
import { genAuthkey, genAuthkey2 } from "./genAuthkey";
|
||||||
@@ -16,7 +16,7 @@ import { getGameAccountsByCookie, getGameAccountsBySToken } from "./getGameAccou
|
|||||||
import { getGameRecord } from "./getGameRecord";
|
import { getGameRecord } from "./getGameRecord";
|
||||||
import { getLTokenBySToken } from "./getLToken";
|
import { getLTokenBySToken } from "./getLToken";
|
||||||
import { getGameRoleListByLToken } from "./getRoleList";
|
import { getGameRoleListByLToken } from "./getRoleList";
|
||||||
import { getStokenByGameToken } from "./getStoken";
|
import { getStokenByGameToken, getTokenBySToken } from "./getStoken";
|
||||||
import getSyncAvatarDetail from "./getSyncAvatarDetail";
|
import getSyncAvatarDetail from "./getSyncAvatarDetail";
|
||||||
import getSyncAvatarListAll from "./getSyncAvatarListAll";
|
import getSyncAvatarListAll from "./getSyncAvatarListAll";
|
||||||
import { getTokensByLoginTicket } from "./getTokens";
|
import { getTokensByLoginTicket } from "./getTokens";
|
||||||
@@ -49,6 +49,7 @@ const TGRequest = {
|
|||||||
getRoleList: getGameRoleListByLToken,
|
getRoleList: getGameRoleListByLToken,
|
||||||
},
|
},
|
||||||
bySToken: {
|
bySToken: {
|
||||||
|
update: getTokenBySToken,
|
||||||
getAccounts: getGameAccountsBySToken,
|
getAccounts: getGameAccountsBySToken,
|
||||||
getCookieToken: getCookieTokenBySToken,
|
getCookieToken: getCookieTokenBySToken,
|
||||||
getLToken: getLTokenBySToken,
|
getLToken: getLTokenBySToken,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @file web/request/getGameAccounts.ts
|
* @file web/request/getGameAccounts.ts
|
||||||
* @description 获取游戏账号信息相关请求函数
|
* @description 获取游戏账号信息相关请求函数
|
||||||
* @since Alpha v0.1.5
|
* @since Beta v0.4.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { http } from "@tauri-apps/api";
|
import { http } from "@tauri-apps/api";
|
||||||
@@ -26,7 +26,7 @@ export async function getGameAccountsBySToken(
|
|||||||
stuid,
|
stuid,
|
||||||
stoken,
|
stoken,
|
||||||
};
|
};
|
||||||
const params = { stoke: stoken, game_biz: TGConstant.Utils.GAME_BIZ };
|
const params = { stoken, stuid, game_biz: TGConstant.Utils.GAME_BIZ };
|
||||||
return await getGameAccounts(url, cookie, params);
|
return await getGameAccounts(url, cookie, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ async function getGameAccounts(
|
|||||||
query: params,
|
query: params,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.data.retcode !== 0) return res.data;
|
if (res.data.retcode !== 0) return <TGApp.BBS.Response.Base>res.data;
|
||||||
return res.data.data.list;
|
return res.data.data.list;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* @file web/request/getStoken.ts
|
* @file web/request/getStoken.ts
|
||||||
* @description 获取 stoken
|
* @description 获取 stoken
|
||||||
* @since Beta v0.3.0
|
* @since Beta v0.4.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { http } from "@tauri-apps/api";
|
import { http } from "@tauri-apps/api";
|
||||||
|
|
||||||
import TGConstant from "../constant/TGConstant";
|
import TGConstant from "../constant/TGConstant";
|
||||||
|
import { getRequestHeader } from "../utils/getRequestHeader";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 获取 stoken
|
* @description 获取 stoken
|
||||||
@@ -19,7 +20,7 @@ export async function getStokenByGameToken(
|
|||||||
accountId: string,
|
accountId: string,
|
||||||
gameToken: string,
|
gameToken: string,
|
||||||
): Promise<TGApp.BBS.Response.getStokenByGameTokenData | TGApp.BBS.Response.Base> {
|
): Promise<TGApp.BBS.Response.getStokenByGameTokenData | TGApp.BBS.Response.Base> {
|
||||||
const url = "https://api-takumi.mihoyo.com/account/ma-cn-session/app/getTokenByGameToken";
|
const url = "https://api-takumi.mihoyo.com/account/ma-cn-session/app/getSTokenByGameToken";
|
||||||
const data = { account_id: Number(accountId), game_token: gameToken };
|
const data = { account_id: Number(accountId), game_token: gameToken };
|
||||||
const header = {
|
const header = {
|
||||||
"x-rpc-app_id": TGConstant.BBS.APP_ID,
|
"x-rpc-app_id": TGConstant.BBS.APP_ID,
|
||||||
@@ -30,6 +31,38 @@ export async function getStokenByGameToken(
|
|||||||
headers: header,
|
headers: header,
|
||||||
body: http.Body.json(data),
|
body: http.Body.json(data),
|
||||||
})
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res.data);
|
||||||
|
if (res.data.retcode !== 0) return res.data;
|
||||||
|
return res.data.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description stoken v1 到 v2
|
||||||
|
* @since Beta v0.4.3
|
||||||
|
* @param {string} stoken 账户 ID
|
||||||
|
* @param {string} stuid 游戏 Token
|
||||||
|
* @returns {Promise<TGApp.BBS.Response.getStokenByGameTokenData | TGApp.BBS.Response.Base>}
|
||||||
|
*/
|
||||||
|
export async function getTokenBySToken(
|
||||||
|
stoken: string,
|
||||||
|
stuid: string,
|
||||||
|
): Promise<TGApp.BBS.Response.getTokenBySTokenData | TGApp.BBS.Response.Base> {
|
||||||
|
const url = "https://passport-api.mihoyo.com/account/ma-cn-session/app/getTokenBySToken";
|
||||||
|
const cookie = {
|
||||||
|
stoken,
|
||||||
|
stuid,
|
||||||
|
};
|
||||||
|
const header = {
|
||||||
|
"x-rpc-app_id": TGConstant.BBS.APP_ID,
|
||||||
|
...getRequestHeader(cookie, "POST", "", "prod"),
|
||||||
|
};
|
||||||
|
return await http
|
||||||
|
.fetch<TGApp.BBS.Response.getTokenBySToken | TGApp.BBS.Response.Base>(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: header,
|
||||||
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.data.retcode !== 0) return res.data;
|
if (res.data.retcode !== 0) return res.data;
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @file web/request/getTokens.ts
|
* @file web/request/getTokens.ts
|
||||||
* @description 获取游戏 Token
|
* @description 获取游戏 Token
|
||||||
* @since Alpha v0.1.5
|
* @since Beta v0.4.3
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { http } from "@tauri-apps/api";
|
import { http } from "@tauri-apps/api";
|
||||||
@@ -11,7 +11,7 @@ import TGUtils from "../utils/TGUtils";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 根据 login_ticket 获取游戏 Token,包括 stoken 和 ltoken
|
* @description 根据 login_ticket 获取游戏 Token,包括 stoken 和 ltoken
|
||||||
* @since Alpha v0.1.5
|
* @since Beta v0.4.3
|
||||||
* @param {string} ticket 登录票证
|
* @param {string} ticket 登录票证
|
||||||
* @param {string} uid 登录用户 uid
|
* @param {string} uid 登录用户 uid
|
||||||
* @returns {Promise<TGApp.BBS.Response.getTokensRes[] | TGApp.BBS.Response.Base>}
|
* @returns {Promise<TGApp.BBS.Response.getTokensRes[] | TGApp.BBS.Response.Base>}
|
||||||
@@ -26,7 +26,7 @@ export async function getTokensByLoginTicket(
|
|||||||
};
|
};
|
||||||
const url = TGApi.GameTokens.getTokens;
|
const url = TGApi.GameTokens.getTokens;
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
const params = { login_ticket: ticket, token_types: 3, uid };
|
const params = { login_ticket: ticket, token_types: "3", uid };
|
||||||
const header = TGUtils.User.getHeader(cookie, "GET", params, "common");
|
const header = TGUtils.User.getHeader(cookie, "GET", params, "common");
|
||||||
return await http
|
return await http
|
||||||
.fetch<TGApp.BBS.Response.getTokens | TGApp.BBS.Response.Base>(url, {
|
.fetch<TGApp.BBS.Response.getTokens | TGApp.BBS.Response.Base>(url, {
|
||||||
|
|||||||
Reference in New Issue
Block a user