Skip to content

Commit

Permalink
feat(route/qq): add 腾讯频道 (/qq/pd) route (#17316)
Browse files Browse the repository at this point in the history
* feat(route/qq): add qq/pd route

* chore(format): remove `any` type hints

* chore(types): add types for qq/pd

* fix(route): fix route example of /qq/pd/guild
  • Loading branch information
mobyw authored Oct 26, 2024
1 parent 5f6d575 commit aa713c0
Show file tree
Hide file tree
Showing 3 changed files with 347 additions and 0 deletions.
146 changes: 146 additions & 0 deletions lib/routes/qq/pd/guild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Data, DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import type { Context } from 'hono';

import { Feed } from './types';
import { parseFeed } from './utils';
import cache from '@/utils/cache';

const baseUrl = 'https://pd.qq.com/g/';
const baseApiUrl = 'https://pd.qq.com/qunng/guild/gotrpc/noauth/trpc.qchannel.commreader.ComReader/';
const getGuildFeedsUrl = baseApiUrl + 'GetGuildFeeds';
const getChannelTimelineFeedsUrl = baseApiUrl + 'GetChannelTimelineFeeds';
const getFeedDetailUrl = baseApiUrl + 'GetFeedDetail';

const sortMap = {
hot: 0,
created: 1,
replied: 2,
};

export const route: Route = {
path: ['/pd/guild/:id/:sub?/:sort?'],
categories: ['bbs'],
example: '/qq/pd/guild/qrp4pkq01d/650967831/created',
parameters: {
id: '频道号',
sub: '子频道 ID,网页端 URL `subc` 参数的值,默认为 `hot`(全部)',
sort: '排序方式,`hot`(热门),`created`(最新发布),`replied`(最新回复),默认为 `created`',
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['pd.qq.com/'],
},
],
name: '腾讯频道',
maintainers: ['mobyw'],
handler,
url: 'pd.qq.com/',
};

async function handler(ctx: Context): Promise<Data> {
const { id, sub = 'hot', sort = 'created' } = ctx.req.param();

if (sort in sortMap === false) {
throw new InvalidParameterError('invalid sort parameter, should be `hot`, `created`, or `replied`');
}
const sortType = sortMap[sort];

let url = '';
let body = {};
let headers = {};

if (sub === 'hot') {
url = getGuildFeedsUrl;
// notice: do not change the order of the keys in the body
body = { count: 20, from: 7, guild_number: id, get_type: 1, feedAttchInfo: '', sortOption: sortType, need_channel_list: false, need_top_info: false };
headers = {
cookie: 'p_uin=o09000002',
'x-oidb': '{"uint32_service_type":12}',
'x-qq-client-appid': '537246381',
};
} else {
url = getChannelTimelineFeedsUrl;
// notice: do not change the order of the keys in the body
body = { count: 20, from: 7, guild_number: id, channelSign: { channel_id: sub }, feedAttchInfo: '', sortOption: sortType, need_top_info: false };
headers = {
cookie: 'p_uin=o09000002',
'x-oidb': '{"uint32_service_type":11}',
'x-qq-client-appid': '537246381',
};
}

const data = await ofetch(url, { method: 'POST', body, headers });
const feeds = data.data?.vecFeed || [];

const items = feeds.map(async (feed: Feed) => {
let subId = sub;
if (sub === 'hot') {
// get real subId for hot feeds
subId = feed.channelInfo?.sign?.channel_id || '';
}
const feedLink = baseUrl + id + '/post/' + feed.id;
const feedDetail = await cache.tryGet(feedLink, async () => {
// notice: do not change the order of the keys in the body
body = {
feedId: feed.id,
userId: feed.poster?.id,
createTime: feed.createTime,
from: 2,
detail_type: 1,
channelSign: { guild_number: id, channel_id: subId },
extInfo: {
mapInfo: [
{ key: 'qc-tabid', value: 'ark' },
{ key: 'qc-pageid', value: 'pc' },
],
},
};
headers = {
cookie: 'p_uin=o09000002',
referer: feedLink,
'x-oidb': '{"uint32_service_type":5}',
'x-qq-client-appid': '537246381',
};
const feedResponse = await ofetch(getFeedDetailUrl, { method: 'POST', body, headers });
const feedContent: Feed = feedResponse.data?.feed || {};
return {
title: feed.title?.contents[0]?.text_content?.text || feed.channelInfo?.guild_name || '',
link: feedLink,
description: parseFeed(feedContent),
pubDate: new Date(Number(feed.createTime) * 1000),
author: feed.poster?.nick,
};
});

return feedDetail;
});

const feedItems = await Promise.all(items);

let guildName = '';

if (feeds.length > 0 && feeds[0].channelInfo?.guild_name) {
guildName = feeds[0].channelInfo?.guild_name;
if (sub !== 'hot' && feeds[0].channelInfo?.name) {
guildName += ' (' + feeds[0].channelInfo?.name + ')';
}
guildName += ' - 腾讯频道';
}

return {
title: guildName,
link: baseUrl + id,
description: guildName,
item: feedItems as DataItem[],
};
}
82 changes: 82 additions & 0 deletions lib/routes/qq/pd/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Description: Types for QQ PD API.

export type Feed = {
id: string;
feed_type: number; // 1-post, 2-article
patternInfo: string; // JSON string
channelInfo: ChannelInfo;
title: {
contents: FeedContent[];
};
contents: {
contents: FeedContent[];
};
images: FeedImage[];
poster: {
id: string;
nick: string;
};
createTime: string;
};

export type ChannelInfo = {
name: string;
guild_number: string;
guild_name: string;
sign: {
guild_id: string;
channel_id: string;
};
};

export type FeedContent = {
type: number;
pattern_id: string;
text_content?: {
text: string;
};
emoji_content?: {
id: string;
type: string;
};
url_content?: {
url: string;
displayText: string;
type: number;
};
};

export type FeedImage = {
picId: string;
picUrl: string;
width: number;
height: number;
pattern_id?: string;
};

export type FeedPattern = {
id: string;
props?: {
textAlignment: number; // 0-left, 1-center, 2-right
};
data: FeedPatternData[];
};

export type FeedPatternData = {
type: number; // 1-text, 2-emoji, 5-link, 6-image, 9-newline
text?: string;
props?: FeedFontProps;
fileId?: string;
taskId?: string;
url?: string;
width?: number;
height?: number;
desc?: string;
href?: string;
};

export type FeedFontProps = {
fontWeight: number; // 400-normal, 700-bold
italic: boolean;
underline: boolean;
};
119 changes: 119 additions & 0 deletions lib/routes/qq/pd/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Description: QQ PD utils

import { Feed, FeedImage, FeedPattern, FeedFontProps, FeedPatternData } from './types';

const patternTypeMap = {
1: 'text',
2: 'emoji',
5: 'link',
6: 'image',
9: 'newline',
};

const textAlignmentMap = {
0: 'left',
1: 'center',
2: 'right',
};

function parseText(text: string, props: FeedFontProps | undefined): string {
if (props === undefined) {
return text;
}
let style = '';
if (props.fontWeight === 700) {
style += 'font-weight: bold;';
}
if (props.italic) {
style += 'font-style: italic;';
}
if (props.underline) {
style += 'text-decoration: underline;';
}
if (style === '') {
return text;
}
return `<span style="${style}">${text}</span>`;
}

function parseDataItem(item: FeedPatternData, texts: string[], images: { [id: string]: FeedImage }): string {
let imageId = '';
switch (patternTypeMap[item.type] || undefined) {
case 'text':
return parseText(texts.shift() ?? '', item.props);
case 'newline':
texts.shift();
return '<br />';
case 'link':
return `<a href="${item.href ?? '#'}" target="_blank">${item.desc ?? ''}</a>`;
case 'image':
imageId = item.fileId || item.taskId || '';
return `<img src="${images[imageId].picUrl}" style="max-width: 100%; width: ${images[imageId].width}px;"><br />`;
default:
return '';
}
}

function parseArticle(feed: Feed, texts: string[], images: { [id: string]: FeedImage }): string {
let result = '';
if (feed.patternInfo === undefined || feed.patternInfo === null || feed.patternInfo === '') {
feed.patternInfo = '[]';
}
const patterns: FeedPattern[] = JSON.parse(feed.patternInfo);
for (const pattern of patterns) {
if (pattern.props === undefined) {
continue;
}
const textAlign = pattern.props.textAlignment || 0;
result += '<p style="text-align: ' + textAlignmentMap[textAlign] + ';">';
for (const item of pattern.data) {
result += parseDataItem(item, texts, images);
}
result += '</p>';
}
return result;
}

function parsePost(feed: Feed, texts: string[], images: { [id: string]: FeedImage }): string {
for (const content of feed.contents.contents) {
if (content.text_content) {
texts.push(content.text_content.text);
}
}
let result = '';
for (const text of texts) {
result += text;
}
for (const image of Object.values(images)) {
result += '<p style="text-align: center">';
result += `<img src="${image.picUrl}" style="max-width: 100%; width: ${image.width}px;">`;
result += '</p>';
}
return result;
}

export function parseFeed(feed: Feed): string {
const texts: string[] = [];
const images: { [id: string]: FeedImage } = {};
for (const content of feed.contents.contents) {
if (content.text_content) {
texts.push(content.text_content.text);
}
}
for (const image of feed.images) {
images[image.picId] = {
picId: image.picId,
picUrl: image.picUrl,
width: image.width,
height: image.height,
};
}
if (feed.feed_type === 1) {
// post: text and attachments
return parsePost(feed, texts, images);
} else if (feed.feed_type === 2) {
// article: pattern info
return parseArticle(feed, texts, images);
}
return '';
}

0 comments on commit aa713c0

Please sign in to comment.