diff --git a/src/common/data.ts b/src/common/data.ts index 5532c7f..6a3b0e5 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -4,6 +4,7 @@ import { NTQQGroupApi } from '../ntqqapi/api/group' import { log } from './utils/log' import { isNumeric } from './utils/helper' import { NTQQFriendApi } from '../ntqqapi/api' +import { WebApiGroupMember } from '@/ntqqapi/api/webapi' export const selfInfo: SelfInfo = { uid: '', @@ -11,6 +12,10 @@ export const selfInfo: SelfInfo = { nick: '', online: true, } +export const WebGroupData = { + GroupData: new Map>(), + GroupTime: new Map() +}; export let groups: Group[] = [] export let friends: Friend[] = [] export let friendRequests: Map = new Map() diff --git a/src/common/utils/request.ts b/src/common/utils/request.ts new file mode 100644 index 0000000..8ae463d --- /dev/null +++ b/src/common/utils/request.ts @@ -0,0 +1,87 @@ +import https from 'node:https'; +import http from 'node:http'; + +export class RequestUtil { + // 适用于获取服务器下发cookies时获取,仅GET + static async HttpsGetCookies(url: string): Promise> { + return new Promise>((resolve, reject) => { + const protocol = url.startsWith('https://') ? https : http; + protocol.get(url, (res) => { + const cookiesHeader = res.headers['set-cookie']; + if (!cookiesHeader) { + resolve(new Map()); + } else { + const cookiesMap = new Map(); + cookiesHeader.forEach((cookieStr) => { + cookieStr.split(';').forEach((cookiePart) => { + const trimmedPart = cookiePart.trim(); + if (trimmedPart.includes('=')) { + const [key, value] = trimmedPart.split('=').map(part => part.trim()); + cookiesMap.set(key, decodeURIComponent(value)); // 解码cookie值 + } + }); + }); + resolve(cookiesMap); + } + }).on('error', (error) => { + reject(error); + }); + }); + } + + // 请求和回复都是JSON data传原始内容 自动编码json + static async HttpGetJson(url: string, method: string = 'GET', data?: any, headers: Record = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise { + let option = new URL(url); + const protocol = url.startsWith('https://') ? https : http; + const options = { + hostname: option.hostname, + port: option.port, + path: option.href, + method: method, + headers: headers + }; + return new Promise((resolve, reject) => { + const req = protocol.request(options, (res: any) => { + let responseBody = ''; + res.on('data', (chunk: string | Buffer) => { + responseBody += chunk.toString(); + }); + + res.on('end', () => { + try { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + if (isJsonRet) { + const responseJson = JSON.parse(responseBody); + resolve(responseJson as T); + } else { + resolve(responseBody as T); + } + } else { + reject(new Error(`Unexpected status code: ${res.statusCode}`)); + } + } catch (parseError) { + reject(parseError); + } + }); + }); + + req.on('error', (error: any) => { + reject(error); + }); + if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + if (isArgJson) { + req.write(JSON.stringify(data)); + } else { + req.write(data); + } + + } + req.end(); + }); + } + + // 请求返回都是原始内容 + static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: Record = {}) { + return this.HttpGetJson(url, method, data, headers, false, false); + } +} \ No newline at end of file diff --git a/src/ntqqapi/api/webapi.ts b/src/ntqqapi/api/webapi.ts index 542d6bb..2e40994 100644 --- a/src/ntqqapi/api/webapi.ts +++ b/src/ntqqapi/api/webapi.ts @@ -1,76 +1,379 @@ -import { groups } from '../../common/data' -import { log } from '../../common/utils' -import { NTQQUserApi } from './user' - -export class WebApi { - private static bkn: string - private static skey: string - private static pskey: string - private static cookie: string - private defaultHeaders: Record = { - 'User-Agent': 'QQ/8.9.28.635 CFNetwork/1312 Darwin/21.0.0', +import { WebGroupData, groups, selfInfo } from '@/common/data'; +import { log } from '@/common/utils/log'; +import { NTQQUserApi } from './user'; +import { RequestUtil } from '@/common/utils/request'; +export enum WebHonorType { + ALL = 'all', + TALKACTIVE = 'talkative', + PERFROMER = 'performer', + LEGEND = 'legend', + STORONGE_NEWBI = 'strong_newbie', + EMOTION = 'emotion' +} +export interface WebApiGroupMember { + uin: number + role: number + g: number + join_time: number + last_speak_time: number + lv: { + point: number + level: number } - - constructor() {} - - public async addGroupDigest(groupCode: string, msgSeq: string) { - const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292` - const res = await this.request(url) - return await res.json() + card: string + tags: string + flag: number + nick: string + qage: number + rm: number +} +interface WebApiGroupMemberRet { + ec: number + errcode: number + em: string + cache: number + adm_num: number + levelname: any + mems: WebApiGroupMember[] + count: number + svr_time: number + max_count: number + search_count: number + extmode: number +} +export interface WebApiGroupNoticeFeed { + u: number//发送者 + fid: string//fid + pubt: number//时间 + msg: { + text: string + text_face: string + title: string, + pics?: { + id: string, + w: string, + h: string + }[] } - - public async getGroupDigest(groupCode: string) { - const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20` - const res = await this.request(url) - log(res.headers) - return await res.json() + type: number + fn: number + cn: number + vn: number + settings: { + is_show_edit_card: number + remind_ts: number + tip_window_type: number + confirm_required: number } - - private genBkn(sKey: string) { - return NTQQUserApi.genBkn(sKey) + read_num: number + is_read: number + is_all_confirm: number +} +export interface WebApiGroupNoticeRet { + ec: number + em: string + ltsm: number + srv_code: number + read_only: number + role: number + feeds: WebApiGroupNoticeFeed[] + group: { + group_id: number + class_ext: number } - private async init() { - if (!WebApi.bkn) { - const group = groups[0] - WebApi.skey = (await NTQQUserApi.getSkey(group.groupName, group.groupCode)).data - WebApi.bkn = this.genBkn(WebApi.skey) - let cookie = await NTQQUserApi.getCookieWithoutSkey() - const pskeyRegex = /p_skey=([^;]+)/ - const match = cookie.match(pskeyRegex) - const pskeyValue = match ? match[1] : null - WebApi.pskey = pskeyValue - if (cookie.indexOf('skey=;') !== -1) { - cookie = cookie.replace('skey=;', `skey=${WebApi.skey};`) - } - WebApi.cookie = cookie - // for(const kv of WebApi.cookie.split(";")){ - // const [key, value] = kv.split("="); - // } - // log("set cookie", key, value) - // await session.defaultSession.cookies.set({ - // url: 'https://qun.qq.com', // 你要请求的域名 - // name: key.trim(), - // value: value.trim(), - // expirationDate: Date.now() / 1000 + 300000, // Cookie 过期时间,例如设置为当前时间之后的300秒 - // }); - // } - } - } - - private async request(url: string, method: 'GET' | 'POST' = 'GET', headers: Record = {}) { - await this.init() - url += '&bkn=' + WebApi.bkn - let _headers: Record = { - ...this.defaultHeaders, - ...headers, - Cookie: WebApi.cookie, - credentials: 'include', - } - log('request', url, _headers) - const options = { - method: method, - headers: _headers, - } - return fetch(url, options) + sta: number, + gln: number + tst: number, + ui: any + server_time: number + svrt: number + ad: number +} +interface GroupEssenceMsg { + group_code: string + msg_seq: number + msg_random: number + sender_uin: string + sender_nick: string + sender_time: number + add_digest_uin: string + add_digest_nick: string + add_digest_time: number + msg_content: any[] + can_be_removed: true +} +export interface GroupEssenceMsgRet { + retcode: number + retmsg: string + data: { + msg_list: GroupEssenceMsg[] + is_end: boolean + group_role: number + config_page_url: string + } +} +export class WebApi { + static async getGroupEssenceMsg(GroupCode: string, page_start: string) { + const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; + const _Skey = await NTQQUserApi.getSkey(); + const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin; + if (!_Skey || !_Pskey) { + //获取Cookies失败 + return undefined; + } + const Bkn = WebApi.genBkn(_Skey); + const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20'; + let ret; + try { + ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }); + } catch { + return undefined; + } + //console.log(url, CookieValue); + if (ret.retcode !== 0) { + return undefined; + } + return ret; + } + static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise { + log('webapi 获取群成员', GroupCode); + let MemberData: Array = new Array(); + try { + let cachedData = WebGroupData.GroupData.get(GroupCode); + let cachedTime = WebGroupData.GroupTime.get(GroupCode); + + if (!cachedTime || Date.now() - cachedTime > 1800 * 1000 || !cached) { + const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; + const _Skey = await NTQQUserApi.getSkey(); + const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; + if (!_Skey || !_Pskey) { + return MemberData; + } + const Bkn = WebApi.genBkn(_Skey); + const retList: Promise[] = []; + const fastRet = await RequestUtil.HttpGetJson('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); + if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { + return []; + } else { + for (const key in fastRet.mems) { + MemberData.push(fastRet.mems[key]); + } + } + //初始化获取PageNum + const PageNum = Math.ceil(fastRet.count / 40); + //遍历批量请求 + for (let i = 2; i <= PageNum; i++) { + const ret: Promise = RequestUtil.HttpGetJson('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue }); + retList.push(ret); + } + //批量等待 + for (let i = 1; i <= PageNum; i++) { + const ret = await (retList[i]); + if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) { + continue; + } + for (const key in ret.mems) { + MemberData.push(ret.mems[key]); + } + } + WebGroupData.GroupData.set(GroupCode, MemberData); + WebGroupData.GroupTime.set(GroupCode, Date.now()); + } else { + MemberData = cachedData as Array; + } + } catch { + return MemberData; + } + return MemberData; + } + // public static async addGroupDigest(groupCode: string, msgSeq: string) { + // const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`; + // const res = await this.request(url); + // return await res.json(); + // } + + // public async getGroupDigest(groupCode: string) { + // const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`; + // const res = await this.request(url); + // return await res.json(); + // } + static async setGroupNotice(GroupCode: string, Content: string = '') { + //https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn} + //qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1} + const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; + const _Skey = await NTQQUserApi.getSkey(); + const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; + let ret: any = undefined; + //console.log(CookieValue); + if (!_Skey || !_Pskey) { + //获取Cookies失败 + return undefined; + } + const Bkn = WebApi.genBkn(_Skey); + const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}'; + const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn; + try { + ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }); + return ret; + } catch (e) { + return undefined; + } + return undefined; + } + static async getGrouptNotice(GroupCode: string): Promise { + const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; + const _Skey = await NTQQUserApi.getSkey(); + const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin; + let ret: WebApiGroupNoticeRet | undefined = undefined; + //console.log(CookieValue); + if (!_Skey || !_Pskey) { + //获取Cookies失败 + return undefined; + } + const Bkn = WebApi.genBkn(_Skey); + const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' + Bkn + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20'; + try { + ret = await RequestUtil.HttpGetJson(url, 'GET', '', { 'Cookie': CookieValue }); + if (ret?.ec !== 0) { + return undefined; + } + return ret; + } catch (e) { + return undefined; + } + return undefined; + } + static genBkn(sKey: string) { + sKey = sKey || ''; + let hash = 5381; + + for (let i = 0; i < sKey.length; i++) { + const code = sKey.charCodeAt(i); + hash = hash + (hash << 5) + code; + } + + return (hash & 0x7FFFFFFF).toString(); + } + //实现未缓存 考虑2h缓存 + static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { + const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com']; + const _Skey = await NTQQUserApi.getSkey(); + if (!_Skey || !_Pskey) { + //获取Cookies失败 + return undefined; + } + async function getDataInternal(Internal_groupCode: string, Internal_type: number) { + let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString(); + let res = ''; + let resJson; + try { + res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue }); + const match = res.match(/window\.__INITIAL_STATE__=(.*?);/); + if (match) { + resJson = JSON.parse(match[1].trim()); + } + if (Internal_type === 1) { + return resJson?.talkativeList; + } else { + return resJson?.actorList; + } + } catch (e) { + log('获取当前群荣耀失败', url, e); + } + return undefined; + } + + let HonorInfo: any = { group_id: groupCode }; + const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin; + + if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) { + try { + let RetInternal = await getDataInternal(groupCode, 1); + if (!RetInternal) { + throw new Error('获取龙王信息失败'); + } + HonorInfo.current_talkative = { + user_id: RetInternal[0]?.uin, + avatar: RetInternal[0]?.avatar, + nickname: RetInternal[0]?.name, + day_count: 0, + description: RetInternal[0]?.desc + } + HonorInfo.talkative_list = []; + for (const talkative_ele of RetInternal) { + HonorInfo.talkative_list.push({ + user_id: talkative_ele?.uin, + avatar: talkative_ele?.avatar, + description: talkative_ele?.desc, + day_count: 0, + nickname: talkative_ele?.name + }); + } + } catch (e) { + log(e); + } + } + if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { + try { + let RetInternal = await getDataInternal(groupCode, 2); + if (!RetInternal) { + throw new Error('获取群聊之火失败'); + } + HonorInfo.performer_list = []; + for (const performer_ele of RetInternal) { + HonorInfo.performer_list.push({ + user_id: performer_ele?.uin, + nickname: performer_ele?.name, + avatar: performer_ele?.avatar, + description: performer_ele?.desc + }); + } + } catch (e) { + log(e); + } + } + if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { + try { + let RetInternal = await getDataInternal(groupCode, 3); + if (!RetInternal) { + throw new Error('获取群聊炽焰失败'); + } + HonorInfo.legend_list = []; + for (const legend_ele of RetInternal) { + HonorInfo.legend_list.push({ + user_id: legend_ele?.uin, + nickname: legend_ele?.name, + avatar: legend_ele?.avatar, + desc: legend_ele?.description + }); + } + } catch (e) { + log('获取群聊炽焰失败', e); + } + } + if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { + try { + let RetInternal = await getDataInternal(groupCode, 6); + if (!RetInternal) { + throw new Error('获取快乐源泉失败'); + } + HonorInfo.emotion_list = []; + for (const emotion_ele of RetInternal) { + HonorInfo.emotion_list.push({ + user_id: emotion_ele?.uin, + nickname: emotion_ele?.name, + avatar: emotion_ele?.avatar, + desc: emotion_ele?.description + }); + } + } catch (e) { + log('获取快乐源泉失败', e); + } + } + //冒尖小春笋好像已经被tx扬了 + if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { + HonorInfo.strong_newbie_list = []; + } + return HonorInfo; } } diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts index e60d681..a76dca6 100644 --- a/src/ntqqapi/ntcall.ts +++ b/src/ntqqapi/ntcall.ts @@ -4,7 +4,6 @@ import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeR import { v4 as uuidv4 } from 'uuid' import { log } from '../common/utils/log' import { NTQQWindow, NTQQWindowApi, NTQQWindows } from './api/window' -import { WebApi } from './api/webapi' import { HOOK_LOG } from '../common/config' export enum NTQQApiClass {