添加客户端

This commit is contained in:
2025-04-06 13:50:16 +08:00
parent 4476be60b5
commit d5c9e663da
32 changed files with 4462 additions and 443 deletions

View File

@@ -0,0 +1,92 @@
import { fetch } from '@tauri-apps/plugin-http';
import { error } from '@tauri-apps/plugin-log'
import { QueryBiliAPI } from './utils';
export async function checkLoginStatusAsync(): Promise<boolean> {
const url = 'https://api.bilibili.com/x/web-interface/nav/stat';
const response = await fetch(url);
const json = await response.json();
return json.code === 0;
}
export async function getUidAsync(): Promise<number> {
const url = 'https://api.live.bilibili.com/xlive/web-ucenter/user/get_user_info';
const response = await fetch(url);
const json = await response.json();
if (json.data && json.data.uid) {
return json.data.uid;
}
return 0;
}
// 二维码地址及扫码密钥
export async function getLoginUrlAsync(): Promise<any> {
const url = 'https://passport.bilibili.com/x/passport-login/web/qrcode/generate';
const response = await QueryBiliAPI(url, 'GET')
if (!response.ok) {
const result = await response.text();
error('无法获取B站登陆二维码: ' + result);
throw new Error('获取二维码地址失败');
}
return await response.json();
}
export async function getLoginUrlDataAsync(): Promise<{
url: string;
qrcode_key: string;
}> {
const message = await getLoginUrlAsync();
if (message.code !== 0) {
throw new Error('获取二维码地址失败');
}
return message.data as {
url: string;
qrcode_key: string;
};
}
type QRCodeLoginInfo =
| { status: 'expired' }
| { status: 'unknown' }
| { status: 'scanned' }
| { status: 'waiting' }
| { status: 'confirmed'; cookie: string; refresh_token: string };
export async function getLoginInfoAsync(qrcodeKey: string): Promise<QRCodeLoginInfo> {
const url = `https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=${qrcodeKey}&source=main-fe-header`;
const response = await QueryBiliAPI(url);
const message = await response.json();
if (!message.data) {
throw new Error('获取登录信息失败');
}
if (message.data.code !== 0) {
switch (message.data.code) {
case 86038:
return { status: 'expired' };
case 86090:
return { status: 'scanned' };
case 86101:
return { status: 'waiting' };
default:
return { status: 'unknown' };
}
}
const cookies = response.headers.get('set-cookie');
if (!cookies) {
throw new Error('无法获取 Cookie');
}
return { status: 'confirmed', cookie: extractCookie(cookies), refresh_token: message.data.refresh_token };
}
function extractCookie(cookies: string): string {
const cookieArray = cookies
.split(',')
.map((cookie) => cookie.split(';')[0].trim())
.filter(Boolean);
const cookieSet = new Set(cookieArray);
return Array.from(cookieSet).join('; ');
}

236
src/client/data/info.ts Normal file
View File

@@ -0,0 +1,236 @@
import { ref } from 'vue';
import { format } from 'date-fns';
import { info, error } from '@tauri-apps/plugin-log';
import { QueryBiliAPI } from './utils'; // 假设 Bili API 工具路径
import { BiliRoomInfo, BiliStreamingInfo, FetcherStatisticData } from './models'; // 假设模型路径
import { useTauriStore } from '../store/useTauriStore';
// import { useAccount } from '@/api/account'; // 如果需要账户信息
// const accountInfo = useAccount(); // 如果需要
export const STATISTIC_STORE_KEY = 'webfetcher.statistics';
/**
* 当前日期 (YYYY-MM-DD) 的统计数据 (会被持久化)
*/
export const currentStatistic = ref<FetcherStatisticData>();
/**
* 标记当前统计数据是否已更新且需要保存
*/
export const shouldUpdateStatistic = ref(false);
/**
* 直播流信息 (从B站API获取)
*/
export const streamingInfo = ref<BiliStreamingInfo>({
status: 'prepare', // 初始状态
} as BiliStreamingInfo);
/**
* 房间基本信息 (从B站API获取)
*/
export const roomInfo = ref<BiliRoomInfo>(); // 可以添加房间信息
// --- Bili API 更新相关 ---
const updateCount = ref(0); // 用于控制API调用频率的计数器
/**
* 初始化统计和信息获取逻辑
*/
export function initInfo() {
// 立即执行一次以加载或初始化当天数据
updateCallback();
// 设置定时器,定期检查和保存统计数据,并更新直播间信息
setInterval(() => {
updateCallback();
}, 5000); // 每 5 秒检查一次统计数据保存和更新直播信息
}
/**
* 定时回调函数: 处理统计数据持久化和B站信息更新
*/
async function updateCallback() {
const store = useTauriStore();
const currentDate = format(new Date(), 'yyyy-MM-dd');
const key = `${STATISTIC_STORE_KEY}.${currentDate}`;
// --- 统计数据管理 ---
// 检查是否需要加载或初始化当天的统计数据
if (!currentStatistic.value || currentStatistic.value.date !== currentDate) {
const loadedData = await store.get<FetcherStatisticData>(key);
if (loadedData && loadedData.date === currentDate) {
currentStatistic.value = loadedData;
// 确保 eventTypeCounts 存在
if (!currentStatistic.value.eventTypeCounts) {
currentStatistic.value.eventTypeCounts = {};
}
// info(`Loaded statistics for ${currentDate}`); // 日志保持不变
} else {
info(`Initializing statistics for new day: ${currentDate}`);
currentStatistic.value = {
date: currentDate,
count: 0,
eventTypeCounts: {}, // 初始化类型计数
};
await store.set(key, currentStatistic.value); // 立即保存新一天的初始结构
shouldUpdateStatistic.value = false; // 重置保存标记
// 清理旧数据逻辑 (保持不变)
const allKeys = (await store.store.keys()).filter((k) => k.startsWith(STATISTIC_STORE_KEY));
if (allKeys.length > 30) { // 例如只保留最近30天的数据
allKeys.sort(); // 按日期字符串升序排序
const oldestKey = allKeys[0];
await store.store.delete(oldestKey);
info('清理过期统计数据: ' + oldestKey);
}
}
}
// 如果数据有更新,则保存
if (shouldUpdateStatistic.value && currentStatistic.value) {
try {
await store.set(key, currentStatistic.value);
shouldUpdateStatistic.value = false; // 保存后重置标记
} catch (err) {
error("Failed to save statistics: " + err);
}
}
// --- B站信息更新 ---
let updateDelay = 30; // 默认30秒更新一次房间信息
if (streamingInfo.value.status === 'streaming' && !import.meta.env.DEV) {
updateDelay = 15; // 直播中15秒更新一次 (可以适当调整)
}
// 使用取模运算控制调用频率
if (updateCount.value % (updateDelay / 5) === 0) { // 因为主循环是5秒一次
updateRoomAndStreamingInfo();
}
updateCount.value++;
}
/**
* 记录一个接收到的事件 (由 useWebFetcher 调用)
* @param eventType 事件类型字符串 (例如 "DANMU_MSG")
*/
export function recordEvent(eventType: string) {
const currentDate = format(new Date(), 'yyyy-MM-dd');
// 确保 currentStatistic 已为当天初始化
if (!currentStatistic.value || currentStatistic.value.date !== currentDate) {
// 理论上 updateCallback 会先执行初始化,这里加个警告以防万一
console.warn("recordEvent called before currentStatistic was initialized for today.");
// 可以选择在这里强制调用一次 updateCallback 来初始化,但这可能是异步的
// await updateCallback(); // 可能会引入复杂性
return; // 或者直接返回,丢失这个事件计数
}
// 增加总数
currentStatistic.value.count++;
// 增加对应类型的计数
if (!currentStatistic.value.eventTypeCounts) {
currentStatistic.value.eventTypeCounts = {}; // 防御性初始化
}
currentStatistic.value.eventTypeCounts[eventType] = (currentStatistic.value.eventTypeCounts[eventType] || 0) + 1;
// 标记需要保存
shouldUpdateStatistic.value = true;
}
/**
* 从 command 数据中解析事件类型
* (需要根据实际接收到的数据结构调整)
*/
export function getEventType(command: any): string {
if (typeof command === 'string') {
try {
command = JSON.parse(command);
} catch (e) {
return 'UNKNOWN_FORMAT';
}
}
if (command && typeof command === 'object') {
// 优先使用 'cmd' 字段 (常见于 Web 或 OpenLive)
if (command.cmd) return command.cmd;
// 备选 'command' 字段
if (command.command) return command.command;
// 备选 'type' 字段
if (command.type) return command.type;
}
return 'UNKNOWN'; // 未知类型
}
/**
* 获取指定天数的历史统计数据
* @param days 要获取的天数,默认为 7
*/
export async function getHistoricalStatistics(days: number = 7): Promise<FetcherStatisticData[]> {
const store = useTauriStore();
const keys = (await store.store.keys())
.filter(key => key.startsWith(STATISTIC_STORE_KEY))
.sort((a, b) => b.localeCompare(a)); // 按日期降序排序
const historicalData: FetcherStatisticData[] = [];
for (let i = 0; i < Math.min(days, keys.length); i++) {
const data = await store.get<FetcherStatisticData>(keys[i]);
if (data) {
historicalData.push(data);
}
}
return historicalData.reverse(); // 返回按日期升序排列的结果
}
/**
* 更新房间和直播流信息
*/
async function updateRoomAndStreamingInfo() {
// 需要一个房间ID来查询这个ID可能来自设置、登录信息或固定配置
// const roomId = accountInfo.value?.roomid ?? settings.value.roomId; // 示例获取房间ID
const roomId = 21484828; // !!! 示例这里需要替换成实际获取房间ID的逻辑 !!!
if (!roomId) {
// error("无法获取房间ID以更新直播信息");
return;
}
try {
// 查询房间基本信息
const roomRes = await QueryBiliAPI(
`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${roomId}`
);
const json = await roomRes.json();
if (json.code === 0) {
roomInfo.value = json.data;
} else {
error(`Failed to fetch Bili room info: ${json.message}`);
}
// 查询直播流信息 (开放平台或Web接口)
// 注意这里可能需要根据所选模式openlive/direct调用不同的API
// 以下是Web接口示例
const streamRes = await QueryBiliAPI(
`https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids?uids[]=${roomInfo.value?.uid}` // 通过 UID 查询
// 或者使用 `https://api.live.bilibili.com/xlive/web-room/v1/index/getRoomBaseInfo?room_ids=${roomId}&req_biz=web_room_componet`
);
const streamJson = await streamRes.json();
if (streamJson.code === 0 && streamJson.data && roomInfo.value?.uid) {
// Web API 返回的是一个以 UID 为 key 的对象
const uidData = streamJson.data[roomInfo.value.uid.toString()];
if (uidData) {
streamingInfo.value = {
...uidData, // 合并获取到的数据
status: uidData.live_status === 1 ? 'streaming' : uidData.live_status === 2 ? 'rotating' : 'prepare',
};
} else {
// 如果没有对应UID的数据可能表示未开播或接口变更
//streamingInfo.value = { status: 'prepare', ...streamingInfo.value }; // 保留旧数据状态设为prepare
}
} else if (streamJson.code !== 0) {
error(`Failed to fetch Bili streaming info: ${streamJson.message}`);
// 可选:如果获取失败,将状态设为未知或准备
// streamingInfo.value = { status: 'prepare', ...streamingInfo.value };
}
} catch (err) {
error("Error updating room/streaming info: " + err);
}
}

View File

@@ -0,0 +1,243 @@
import { isLoggedIn, useAccount } from "@/api/account";
import { attachConsole, info, warn } from "@tauri-apps/plugin-log";
import { useSettings } from "../store/useSettings";
import { useWebFetcher } from "@/store/useWebFetcher";
import { useBiliCookie } from "../store/useBiliCookie";
import { getBuvid, getRoomKey } from "./utils";
import { initInfo } from "./info";
import { TrayIcon, TrayIconOptions } from '@tauri-apps/api/tray';
import { Menu } from "@tauri-apps/api/menu";
import { getCurrentWindow } from "@tauri-apps/api/window";
import {
isPermissionGranted,
onAction,
requestPermission,
sendNotification,
} from '@tauri-apps/plugin-notification';
import { openUrl } from "@tauri-apps/plugin-opener";
import { CN_HOST } from "@/data/constants";
import { invoke } from "@tauri-apps/api/core";
import { check } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
const accountInfo = useAccount();
export const clientInited = ref(false);
let tray: TrayIcon;
export async function initAll() {
if (clientInited.value) {
return;
}
let permissionGranted = await isPermissionGranted();
// If not we need to request it
if (!permissionGranted) {
const permission = await requestPermission();
permissionGranted = permission === 'granted';
if (permissionGranted) {
info('Notification permission granted');
}
}
initNotificationHandler();
const detach = await attachConsole();
const settings = useSettings();
const biliCookie = useBiliCookie();
await settings.init();
info('[init] 已加载账户信息');
biliCookie.init();
info('[init] 已加载bilibili cookie');
initInfo();
info('[init] 开始更新数据');
if (isLoggedIn && accountInfo.value.isBiliVerified) {
const danmakuInitNoticeRef = window.$notification.info({
title: '正在初始化弹幕客户端...',
closable: false
});
const result = await initDanmakuClient();
danmakuInitNoticeRef.destroy();
if (result.success) {
window.$notification.success({
title: '弹幕客户端初始化完成',
duration: 3000
});
} else {
window.$notification.error({
title: '弹幕客户端初始化失败: ' + result.message,
});
}
}
info('[init] 已加载弹幕客户端');
// 初始化系统托盘图标和菜单
const menu = await Menu.new({
items: [
{
id: 'quit',
text: '退出',
action: () => {
invoke('quit_app');
},
},
],
});
const iconData = await (await fetch('https://oss.suki.club/vtsuru/icon.ico')).arrayBuffer();
const appWindow = getCurrentWindow();
const options: TrayIconOptions = {
// here you can add a tray menu, title, tooltip, event handler, etc
menu: menu,
title: 'VTsuru.Client',
tooltip: 'VTsuru 事件收集器',
icon: iconData,
action: (event) => {
switch (event.type) {
case 'DoubleClick':
appWindow.show();
break;
case 'Click':
appWindow.show();
break;
}
}
};
tray = await TrayIcon.new(options);
clientInited.value = true;
}
export function OnClientUnmounted() {
if (clientInited.value) {
clientInited.value = false;
}
tray.close();
}
async function checkUpdate() {
const update = await check();
if (update) {
console.log(
`found update ${update.version} from ${update.date} with notes ${update.body}`
);
let downloaded = 0;
let contentLength = 0;
// alternatively we could also call update.download() and update.install() separately
await update.downloadAndInstall((event) => {
switch (event.event) {
case 'Started':
contentLength = event.data.contentLength || 0;
console.log(`started downloading ${event.data.contentLength} bytes`);
break;
case 'Progress':
downloaded += event.data.chunkLength;
console.log(`downloaded ${downloaded} from ${contentLength}`);
break;
case 'Finished':
console.log('download finished');
break;
}
});
console.log('update installed');
await relaunch();
}
}
export const isInitedDanmakuClient = ref(false);
export const isInitingDanmakuClient = ref(false);
export async function initDanmakuClient() {
const biliCookie = useBiliCookie();
const settings = useSettings();
if (isInitedDanmakuClient.value || isInitingDanmakuClient.value) {
return { success: true, message: '' };
}
isInitingDanmakuClient.value = true;
let result = { success: false, message: '' };
try {
if (isLoggedIn) {
if (settings.settings.useDanmakuClientType === 'openlive') {
result = await initOpenLive();
} else {
const cookie = await biliCookie.getBiliCookie();
if (!cookie) {
if (settings.settings.fallbackToOpenLive) {
settings.settings.useDanmakuClientType = 'openlive';
settings.save();
info('未设置bilibili cookie, 根据设置切换为openlive');
result = await initOpenLive();
} else {
info('未设置bilibili cookie, 跳过弹幕客户端初始化');
window.$notification.warning({
title: '未设置bilibili cookie, 跳过弹幕客户端初始化',
duration: 5,
});
result = { success: false, message: '未设置bilibili cookie' };
}
} else {
const resp = await callStartDanmakuClient();
if (!resp?.success) {
warn('加载弹幕客户端失败: ' + resp?.message);
result = { success: false, message: resp?.message };
} else {
info('已加载弹幕客户端');
result = { success: true, message: '' };
}
}
}
}
return result;
} catch (err) {
warn('加载弹幕客户端失败: ' + err);
return { success: false, message: '加载弹幕客户端失败' };
} finally {
if (result) {
isInitedDanmakuClient.value = true;
}
isInitingDanmakuClient.value = false;
}
}
export async function initOpenLive() {
const reuslt = await callStartDanmakuClient();
if (reuslt?.success == true) {
info('已加载弹幕客户端 [openlive]');
} else {
warn('加载弹幕客户端失败 [openlive]: ' + reuslt?.message);
}
return reuslt;
}
function initNotificationHandler(){
onAction((event) => {
if (event.extra?.type === 'question-box') {
openUrl(CN_HOST + '/manage/question-box');
}
});
}
export async function callStartDanmakuClient() {
const biliCookie = useBiliCookie();
const settings = useSettings();
const webFetcher = useWebFetcher();
if (settings.settings.useDanmakuClientType === 'direct') {
const key = await getRoomKey(
accountInfo.value.biliRoomId!, await biliCookie.getBiliCookie() || '');
if (!key) {
warn('获取房间密钥失败, 无法连接弹幕客户端');
return { success: false, message: '无法获取房间密钥' };
}
const buvid = await getBuvid();
if (!buvid) {
warn('获取buvid失败, 无法连接弹幕客户端');
return { success: false, message: '无法获取buvid' };
}
return await webFetcher.Start('direct', {
roomId: accountInfo.value.biliRoomId!,
buvid: buvid.data,
token: key,
tokenUserId: biliCookie.uId!,
}, true);
} else {
return await webFetcher.Start('openlive', undefined, true);
}
}

282
src/client/data/models.ts Normal file
View File

@@ -0,0 +1,282 @@
export interface EventFetcherStateModel {
online: boolean;
status: { [errorCode: string]: string };
version?: string;
todayReceive: number;
useCookie: boolean;
type: EventFetcherType;
}
export enum EventFetcherType {
Application,
OBS,
Server,
Tauri,
}
export type BiliRoomInfo = {
uid: number;
room_id: number;
short_id: number;
attention: number;
online: number;
is_portrait: boolean;
description: string;
live_status: number;
area_id: number;
parent_area_id: number;
parent_area_name: string;
old_area_id: number;
background: string;
title: string;
user_cover: string;
keyframe: string;
is_strict_room: boolean;
live_time: string;
tags: string;
is_anchor: number;
room_silent_type: string;
room_silent_level: number;
room_silent_second: number;
area_name: string;
pendants: string;
area_pendants: string;
hot_words: string[];
hot_words_status: number;
verify: string;
new_pendants: {
frame: {
name: string;
value: string;
position: number;
desc: string;
area: number;
area_old: number;
bg_color: string;
bg_pic: string;
use_old_area: boolean;
};
badge: unknown; // null in the example, adjust to proper type if known
mobile_frame: {
name: string;
value: string;
position: number;
desc: string;
area: number;
area_old: number;
bg_color: string;
bg_pic: string;
use_old_area: boolean;
};
mobile_badge: unknown; // null in the example, adjust to proper type if known
};
up_session: string;
pk_status: number;
pk_id: number;
battle_id: number;
allow_change_area_time: number;
allow_upload_cover_time: number;
studio_info: {
status: number;
master_list: any[]; // empty array in the example, adjust to proper type if known
};
}
export type FetcherStatisticData = {
date: string;
count: number;
eventTypeCounts: { [eventType: string]: number };
};
export type BiliStreamingInfo = {
status: 'prepare' | 'streaming' | 'cycle';
streamAt: Date;
roomId: number;
title: string;
coverUrl: string;
frameUrl: string;
areaName: string;
parentAreaName: string;
online: number;
attention: number;
};
// Nested type for Vip Label
interface VipLabel {
path: string;
text: string;
label_theme: string;
text_color: string;
bg_style: number;
bg_color: string;
border_color: string;
use_img_label: boolean;
img_label_uri_hans: string;
img_label_uri_hant: string;
img_label_uri_hans_static: string;
img_label_uri_hant_static: string;
}
// Nested type for Avatar Icon
interface AvatarIcon {
icon_type: number;
// Assuming icon_resource could contain arbitrary data or be empty
icon_resource: Record<string, unknown> | {};
}
// Nested type for Vip Info
interface VipInfo {
type: number;
status: number;
due_date: number; // Likely a Unix timestamp in milliseconds
vip_pay_type: number;
theme_type: number;
label: VipLabel;
avatar_subscript: number;
nickname_color: string;
role: number;
avatar_subscript_url: string;
tv_vip_status: number;
tv_vip_pay_type: number;
tv_due_date: number; // Likely a Unix timestamp in milliseconds or 0
avatar_icon: AvatarIcon;
}
// Nested type for Pendant Info
interface PendantInfo {
pid: number;
name: string;
image: string; // URL
expire: number; // Likely a timestamp or duration
image_enhance: string; // URL
image_enhance_frame: string; // URL or empty string
n_pid: number;
}
// Nested type for Nameplate Info
interface NameplateInfo {
nid: number;
name: string;
image: string; // URL
image_small: string; // URL
level: string;
condition: string;
}
// Nested type for Official Info
interface OfficialInfo {
role: number;
title: string;
desc: string;
type: number;
}
// Nested type for Profession Info
interface ProfessionInfo {
id: number;
name: string;
show_name: string;
is_show: number; // Likely 0 or 1
category_one: string;
realname: string;
title: string;
department: string;
certificate_no: string;
certificate_show: boolean;
}
// Nested type for Honours Colour
interface HonoursColour {
dark: string; // Hex color code
normal: string; // Hex color code
}
// Nested type for Honours Info
interface HonoursInfo {
mid: number;
colour: HonoursColour;
// Assuming tags could be an array of strings if not null
tags: string[] | null;
is_latest_100honour: number; // Likely 0 or 1
}
// Nested type for Attestation Common Info
interface CommonAttestationInfo {
title: string;
prefix: string;
prefix_title: string;
}
// Nested type for Attestation Splice Info
interface SpliceAttestationInfo {
title: string;
}
// Nested type for Attestation Info
interface AttestationInfo {
type: number;
common_info: CommonAttestationInfo;
splice_info: SpliceAttestationInfo;
icon: string;
desc: string;
}
// Nested type for Expert Info
interface ExpertInfo {
title: string;
state: number;
type: number;
desc: string;
}
// Nested type for Level Exp Info
interface LevelExpInfo {
current_level: number;
current_min: number;
current_exp: number;
next_exp: number; // -1 might indicate max level or data not applicable
level_up: number; // Likely a Unix timestamp
}
// Main User Profile Type
export type BiliUserProfile = {
mid: number;
name: string;
sex: string; // Could be more specific like '男' | '女' | '保密' if desired
face: string; // URL
sign: string;
rank: number;
level: number;
jointime: number; // Likely a Unix timestamp or 0
moral: number;
silence: number; // Likely 0 or 1
email_status: number; // Likely 0 or 1
tel_status: number; // Likely 0 or 1
identification: number; // Likely 0 or 1
vip: VipInfo;
pendant: PendantInfo;
nameplate: NameplateInfo;
official: OfficialInfo;
birthday: number; // Likely a Unix timestamp
is_tourist: number; // Likely 0 or 1
is_fake_account: number; // Likely 0 or 1
pin_prompting: number; // Likely 0 or 1
is_deleted: number; // Likely 0 or 1
in_reg_audit: number; // Likely 0 or 1
is_rip_user: boolean;
profession: ProfessionInfo;
face_nft: number;
face_nft_new: number;
is_senior_member: number; // Likely 0 or 1
honours: HonoursInfo;
digital_id: string;
digital_type: number;
attestation: AttestationInfo;
expert_info: ExpertInfo;
// Assuming name_render could be various types or null
name_render: any | null;
country_code: string;
level_exp: LevelExpInfo;
coins: number; // Can be float
following: number;
follower: number;
};

View File

@@ -0,0 +1,27 @@
import { QAInfo } from "@/api/api-models";
import { useSettings } from "../store/useSettings";
import { isPermissionGranted, onAction, sendNotification } from "@tauri-apps/plugin-notification";
import { openUrl } from "@tauri-apps/plugin-opener";
import { CN_HOST } from "@/data/constants";
export async function onReceivedQuestion(question: QAInfo) {
const setting = useSettings();
if (setting.settings.notificationSettings.enableTypes.includes("question-box")) {
window.$notification.info({
title: "收到提问",
description: '收到来自 [' + question.sender.name || '匿名用户' + '] 的提问',
duration: 5,
});
let permissionGranted = await isPermissionGranted();
if (permissionGranted) {
sendNotification({
title: "收到提问",
body: '来自 [' + question.sender.name || '匿名用户' + '] 的提问',
silent: false,
extra: { type: 'question-box' },
});
}
}
}

77
src/client/data/utils.ts Normal file
View File

@@ -0,0 +1,77 @@
import { fetch } from '@tauri-apps/plugin-http';
import { useBiliCookie } from '../store/useBiliCookie';
import { QueryPostAPI } from '@/api/query';
import { OPEN_LIVE_API_URL } from '@/data/constants';
import { error } from '@tauri-apps/plugin-log';
export async function QueryBiliAPI(url: string, method: string = 'GET', cookie: string = '') {
const u = new URL(url);
return fetch(url, {
method: method,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
Origin: '',
Cookie: cookie || (await useBiliCookie().getBiliCookie()) || '',
'Upgrade-Insecure-Requests': '1',
},
});
}
export async function getRoomKey(roomId: number, cookie: string) {
try {
const result = await QueryBiliAPI(
'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo?id=' + roomId
);
const json = await result.json();
if (json.code === 0) return json.data.token;
else {
error(`无法获取直播间key: ${json.message}`);
}
} catch (err) {
error(`无法获取直播间key: ${err}`);
}
}
export async function getBuvid() {
try {
const result = await QueryBiliAPI('https://api.bilibili.com/x/web-frontend/getbuvid');
if (result.ok) {
const json = await result.json();
if (json.code === 0) return json.data.buvid;
else {
error(`无法获取buvid: ${json.message}`);
}
} else {
error(`无法获取buvid: ${result.statusText}`);
}
} catch (err) {
error(`无法获取buvid: ${err}`);
}
}
export async function getAuthInfo(): Promise<{
data: any;
message: string;
}> {
try {
const data = await QueryPostAPI<any>(OPEN_LIVE_API_URL + 'start');
if (data.code == 200) {
console.log(`[open-live] 已获取认证信息`);
return {
data: data.data,
message: '',
};
} else {
return {
data: null,
message: data.message,
};
}
} catch (err) {
return {
data: null,
message: err?.toString() || '未知错误',
};
}
}