完善私信相关功能

This commit is contained in:
Searchstars
2024-08-25 15:18:08 +08:00
parent 28fe23d8b1
commit 01f81eb18c
5 changed files with 244 additions and 46 deletions
+49 -10
View File
@@ -9,7 +9,7 @@ const mixinKeyEncTab = [
];
// 对imgKey和subKey进行混淆编码
const getMixinKey = (orig: string) =>
const getMixinKey = (orig: string) =>
mixinKeyEncTab.map(n => orig[n]).join("").slice(0, 32);
// 账号数据接口
@@ -40,6 +40,13 @@ class BilibiliClient {
private buvid3: string | null = null;
private buvid4: string | null = null;
// 私信DeviceId,每次登录刷新
// From https://github.com/andywang425/BLTH/
private dm_deviceid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (function (name) {
let randomInt = 16 * Math.random() | 0;
return ("x" === name ? randomInt : 3 & randomInt | 8).toString(16).toUpperCase()
}));
// 获取请求头,用于模拟正常浏览器环境,降低风控概率
private getHeaders(): Record<string, string> {
return {
@@ -100,15 +107,19 @@ class BilibiliClient {
}
// 发送POST请求
private async postRequest(url: string, data: string, content_type: string): Promise<any> {
private async postRequest(url: string, data: string, content_type: string, custom_headers: any = null): Promise<any> {
console.log(`postRequest: ${url}, body: ${data}, contentType: ${content_type}`);
let headers = { ...this.getHeaders(), "Content-Type": content_type }
if(custom_headers){
headers = custom_headers
}
try {
const response = await fetch.fetch({
url,
responseType: 'json',
method: 'POST',
data,
header: { ...this.getHeaders(), "Content-Type": content_type }
header: headers
});
return response.data;
} catch (error) {
@@ -117,6 +128,14 @@ class BilibiliClient {
}
}
// 发送带Wbi签名的POST请求
private async postRequestWbi(url: string, wbiParams: any, data: string, content_type: string, custom_headers: any = null): Promise<any> {
const img_key = this.accountInfo.wbi_img.img_url.split('/').pop().split('.')[0];
const sub_key = this.accountInfo.wbi_img.sub_url.split('/').pop().split('.')[0];
const signedParams = this.encWbi(wbiParams, img_key, sub_key);
return this.postRequest(`${url}?${signedParams}`, data, content_type, custom_headers);
}
// 检查澎湃哔哩是否存在更新
async checkHyperbilibiliUpdates(): Promise<any> {
const latestVerGet = await fetch.fetch({
@@ -383,7 +402,7 @@ class BilibiliClient {
break;
case "video":
result_array.data.forEach(vid => {
if(vid.bvid){
if (vid.bvid) {
result.videos.push(vid)
}
});
@@ -398,7 +417,7 @@ class BilibiliClient {
}
// 获取通知信息数量 (例如回复我的、at我的、点赞数量)
async getMessageNotifyFeed(){
async getMessageNotifyFeed() {
const url = "https://api.vc.bilibili.com/x/im/web/msgfeed/unread";
const response = await this.getRequest(url);
return response.data.data;
@@ -406,7 +425,7 @@ class BilibiliClient {
// 获取私信Session列表
// 一次性最多拉取20个,可加end_ts做IFS(但没必要)
async getDMSessions(session_type: number, sort_rule: number){
async getDMSessions(session_type: number, sort_rule: number) {
const url = "https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions"
const response = await this.getRequestWbi(url, {
session_type,
@@ -422,14 +441,14 @@ class BilibiliClient {
// 获取私信Session聊天记录
// 若要做IFS,则end_seqno应该为最顶上那条信息的序列号
// 接口有漏洞,自带防撤回
async getDMSessionMessage(session_type: number, talker_id: string, size: number = 10, end_seqno: string){
async getDMSessionMessage(session_type: number, talker_id: string, size: number = 10, end_seqno: string) {
const url = "https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs"
var params = {
session_type,
talker_id,
size
};
if(end_seqno){
if (end_seqno) {
params["end_seqno"] = end_seqno;
}
const response = await this.getRequestWbi(url, params);
@@ -437,13 +456,33 @@ class BilibiliClient {
return response.data.data
}
// 发送私信消息
async SendDMSessionMessage(receiver_id: string, msg_type: number, content: string) {
const url = "https://api.vc.bilibili.com/web_im/v1/web_im/send_msg";
const body = `msg[sender_uid]=${this.accountInfo.mid}&msg[receiver_id]=${receiver_id}&msg[receiver_type]=1&msg[msg_type]=${msg_type}&msg[dev_id]=${this.dm_deviceid}&msg[timestamp]=${Number.parseInt(((new Date()).getTime() / 1000).toString())}&msg[content]=${encodeURIComponent(`{"content": "${content}"}`)}&csrf=${this.biliJct}&csrf_token=${this.biliJct}&msg[msg_status]=0&msg[new_face_version]=0&from_firework=0&build=0&mobi_app=web`;
var headers = { ...this.getHeaders(), "Content-Type": "application/x-www-form-urlencoded" }
headers["Host"] = "api.vc.bilibili.com"
headers["Origin"] = "https://message.bilibili.com"
headers["Referer"] = "https://message.bilibili.com/"
headers["Content-Length"] = body.length
const response = await this.postRequestWbi(url, {
w_sender_uid: this.accountInfo.mid,
w_receiver_id: receiver_id,
w_dev_id: this.dm_deviceid
}, body, "application/x-www-form-urlencoded", headers);
return response.data
}
// 根据UID批量获取用户信息
async getMultiUserInfoByUID(uids: Array<String>){
async getMultiUserInfoByUID(uids: Array<String>) {
const url = "https://api.bilibili.com/x/polymer/pc-electron/v1/user/cards";
let param = "";
uids.forEach(uid => {
param += uid
if(uids.indexOf(uid) != uids.length -1){
if (uids.indexOf(uid) != uids.length - 1) {
param += ","
}
});
@@ -1,14 +1,22 @@
<template>
<div class="sendbox">
<div class="textbox">
<text class="textbox_text">点此输入文字</text>
<div class="textbox" @click="GoInput">
<text class="textbox_text">{{this.input_text.content || "点此输入文字"}}</text>
</div>
<image src="/common/dmpage_sendbtn.png" style="margin-left: 6px"></image>
<image @click="GoSend" src="/common/dmpage_sendbtn.png" style="margin-left: 215px; margin-top: 12px"></image>
</div>
</template>
<script>
export default {}
export default {
props: ["input_text"],
GoInput(){
this.$emit("clickinput")
},
GoSend(){
this.$emit("clicksend")
}
}
</script>
<style>
@@ -17,7 +25,6 @@ export default {}
height: 73px;
border-radius: 48px;
background-color: #262626ef;
padding: 12px 20px;
position: absolute;
bottom: 55px
}
@@ -29,10 +36,19 @@ export default {}
background-color: #3D3D3D;
align-items: center;
padding: 15px;
display: flex;
justify-content: center;
position: absolute;
top: 12px;
left: 20px;
}
.textbox_text {
font-size: 20px;
font-weight: 600;
text-overflow: ellipsis;
width: 165px;
max-width: 165px;
lines: 1;
}
</style>
@@ -2,7 +2,7 @@
<template>
<div class="inputpanel">
<input-method hide={{keyboardHide}} @complete="InputComplete" @delete="InputDelete" vibratemode="short"></input-method>
<input-method style="position: absolute" hide={{keyboardHide}} @complete="InputComplete" @delete="InputDelete" vibratemode="short"></input-method>
<div class="inputactions">
<div style="margin-left: 35px" @click="ExitInput">
<image class="inputaction_img" src="/common/textinput_back.png"></image>
+7 -1
View File
@@ -39,7 +39,13 @@ export default {
var session_uids = []
this.sessions.forEach((session) => {
session_uids.push(session.talker_id)
if(session.talker_id){
session_uids.push(session.talker_id)
}
else{
// Pass掉非法UID
this.sessions = this.sessions.filter(item => item !== session)
}
})
this.session_cards = await this.$app.$def.biliclient.getMultiUserInfoByUID(session_uids)
+166 -29
View File
@@ -1,7 +1,9 @@
<import name="sendbox" src="../../components/FloatingSendBox/FloatingSendBox.ux"></import>
<import name="full-screen-input" src="../../components/FullScreenInput/FullScreenInput.ux"></import>
<template>
<div class="page">
<image style="position: absolute; margin-top: 233px" if="{{show_loading}}" src="{{anims.loading_src.value}}"></image>
<scroll class="scroll" scroll-y="true" id="mainscroll">
<div for="{{messages}}">
<div class="targetmsg-container" if="{{$item.receiver_id == myId}}">
@@ -19,8 +21,19 @@
<div class="mymsg"
style="margin-top: {{MsgBoxStyleCalcMarginTop($item.msg_seqno)}}; margin-bottom: {{MsgBoxStyleCalcMarginBottom($item.msg_seqno)}}">
<div style="background-color: #ff7da8" class="msgbox">
<text class="msgtext">
{{ JSON.parse($item.content).content || "该消息暂不支持显示" }}
<!-- 普通文本消息 -->
<text class="msgtext" if="{{$item.msg_type == 1}}">
{{ JSON.parse($item.content).content}}
</text>
<!-- 已撤回的文本消息 -->
<text class="msgtext" if="{{$item.msg_type == 5}}">
[已撤回] {{ JSON.parse($item.content).content}}
</text>
<!-- 图片消息 -->
<image class="imgmsg" if="{{$item.msg_type == 2}}" src="{{JSON.parse($item.content).url}}"></image>
<!-- 不支持显示的消息 -->
<text class="msgtext" if="{{($item.msg_type != 5) && ($item.msg_type != 2) && ($item.msg_type != 1)}}">
该消息暂不支持显示
</text>
</div>
<image style="margin-left: 5px" class="msgprofpic" src="{{myPic}}@75w_75h" alt="/common/fullgray.png"></image>
@@ -28,11 +41,17 @@
</div>
</div>
</scroll>
<sendbox style="position: absolute"></sendbox>
<sendbox input_text="{{input_text}}" @clickinput="OpenInput" @clicksend="SendMsg" style="position: absolute">
</sendbox>
<full-screen-input style="position: absolute" input_text="{{input_text}}" if="{{inputmode}}" @complete="InputComplete"
@delete="InputDelete" @send="FinishInput" @exit="ExitInput"></full-screen-input>
</div>
</template>
<script>
import router from "@system.router"
import prompt from "@system.prompt"
export default {
public: {
myId: 0,
@@ -41,43 +60,152 @@ export default {
targetPic: ""
},
private: {
messages: []
messages: [
{
msg_seqno: "placeholder",
receiver_id: 0,
content: `{"content": "占位符"}`
}
],
inputmode: false,
show_loading: true,
input_text: {
content: ""
},
anims: {
loading: null,
loading_src: {value: "/common/seqanims/loadingWhite/icons8-loading_颜色反转-1.png"}
}
},
async SendMsg() {
// 发送消息
try {
const result = await this.$app.$def.biliclient.SendDMSessionMessage(
this.targetId,
1,
this.input_text.content
)
if (result.code !== 0) {
prompt.showToast({
message: `发送失败!错误代码:${result.code} 原因:${result.message}`
})
} else {
this.UpdateMessages()
this.input_text.content = ""
}
} catch (error) {
prompt.showToast({
message: `发送失败!${error.message}`
})
}
},
ShowMessageDetail(content, type){
switch(type){
// 文本类消息直接丢给textdetail
case 1:
router.push({
uri: "pages/textdetail",
params: {
text: content,
titletext: "消息详情"
}
})
break
default:
prompt.showToast({
message: "该类型消息暂不支持展示详情"
})
break
}
},
RefreshMessageList(input_text_set = ""){
// 更改数组length以强迫快应用框架重渲染页面
this.messages.push({})
this.messages.pop()
// 同时也刷新自定义组件的ViewModel
this.input_text.content = "Loading..."
// 将内容设置为默认为空字符串的input_text_set以适配FinishInput
this.input_text.content = input_text_set
setTimeout(() => {
this.ScrollToEnd()
}, 300)
},
FinishInput() {
this.inputmode = false
this.RefreshMessageList(this.input_text.content)
},
ExitInput() {
this.inputmode = false
this.input_text.content = ""
this.RefreshMessageList()
},
InputComplete(evt) {
this.input_text.content += evt.detail.content
},
InputDelete() {
if (this.input_text.content.length > 0) {
this.input_text.content = this.input_text.content.slice(0, -1)
}
},
OpenInput() {
this.inputmode = true
},
MsgBoxStyleCalcMarginTop(msg_seqno) {
if (msg_seqno == this.messages[0].msg_seqno) {
return "85px"
}
return "15px"
return msg_seqno === this.messages[0].msg_seqno ? "85px" : "15px"
},
MsgBoxStyleCalcMarginBottom(msg_seqno) {
if (msg_seqno == this.messages[this.messages.length - 1].msg_seqno) {
return "160px"
}
return "0px"
return msg_seqno === this.messages[this.messages.length - 1].msg_seqno ? "160px" : "0px"
},
async UpdateMessages(){
var messages = (await this.$app.$def.biliclient.getDMSessionMessage(1, this.targetId, 30)).messages.reverse()
if(messages != this.messages){
this.messages = messages
setTimeout(() => {
this.ScrollToEnd()
}, 200)
async UpdateMessages(initial = false) {
// 更新消息
try {
const response = await this.$app.$def.biliclient.getDMSessionMessage(1, this.targetId, 30)
const newMessages = response.messages.reverse()
if (
newMessages.length > 0 &&
newMessages[newMessages.length - 1].msg_seqno !==
this.messages[this.messages.length - 1].msg_seqno
) {
this.messages = newMessages
setTimeout(this.ScrollToEnd, 200)
}
if (initial && this.show_loading) {
this.show_loading = false
this.anims.loading.stop()
}
} catch (error) {
prompt.showToast({
message: `更新消息失败!${error.message}`
})
}
},
ScrollToEnd(){
this.$element('mainscroll').getScrollRect({
ScrollToEnd() {
this.$element("mainscroll").getScrollRect({
success: (rect) => {
this.$element('mainscroll').scrollTo({
top: rect.height
})
this.$element("mainscroll").scrollTo({top: rect.height})
}
})
},
onShow(){
this.UpdateMessages()
onReady() {
global.runGC()
this.UpdateMessages(true) // 初始化加载消息
this.messageUpdateInterval = setInterval(this.UpdateMessages, 3000) // 每3秒更新一次消息
},
onInit(){
onDestroy() {
clearInterval(this.messageUpdateInterval)
},
onInit() {
this.anims.loading = new this.$app.$def.animengine.SequenceAnim(
"replysPage",
this.anims.loading_src,
28,
"/common/seqanims/loadingWhite/icons8-loading_颜色反转-*.png",
1000,
true
)
this.anims.loading.start()
}
}
</script>
@@ -121,12 +249,21 @@ export default {
margin-right: 55px;
}
.imgmsg {
object-fit: scale-down;
border-radius: 15px;
max-width: 156px;
max-height: 125px;
}
.msgbox {
max-width: 171px;
max-height: 140px;
border-radius: 19px;
border-radius: 20px;
padding: 15px;
background-color: #242424;
justify-content: center;
align-items: center;
}
.msgprofpic {