mirror of
https://github.com/d0zingcat/BroadcastChannel.git
synced 2026-05-13 15:09:12 +00:00
Merge pull request #130 from bunizao/main
Add `reaction` display support
This commit is contained in:
@@ -23,6 +23,7 @@ STATIC_PROXY=""
|
||||
GOOGLE_SEARCH_SITE=""
|
||||
TAGS=""
|
||||
COMMENTS=""
|
||||
REACTIONS=""
|
||||
LINKS=""
|
||||
NAVS=""
|
||||
RSS_BEAUTIFY=""
|
||||
RSS_BEAUTIFY=""
|
||||
|
||||
@@ -131,6 +131,9 @@ TAGS=tag1,tag2,tag3
|
||||
## Show comments
|
||||
COMMENTS=true
|
||||
|
||||
## Show reactions
|
||||
REACTIONS=true
|
||||
|
||||
## List of links in the Links page, Separate using commas and semicolons
|
||||
LINKS=Title1,URL1;Title2,URL3;Title3,URL3;
|
||||
|
||||
|
||||
@@ -129,6 +129,9 @@ TAGS=标签A,标签B,标签C
|
||||
## 展示评论
|
||||
COMMENTS=true
|
||||
|
||||
## 展示 Reactions
|
||||
REACTIONS=true
|
||||
|
||||
## 链接页面中的超链接, 使用英文逗号和分号分割
|
||||
LINKS=Title1,URL1;Title2,URL3;Title3,URL3;
|
||||
|
||||
|
||||
@@ -234,6 +234,12 @@
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.tg-emoji {
|
||||
width: 1.15em;
|
||||
height: 1.15em;
|
||||
vertical-align: -0.15em;
|
||||
}
|
||||
|
||||
.sticker {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
@@ -261,6 +267,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
.reaction-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.reaction-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px 3px 6px;
|
||||
font-size: 12px;
|
||||
color: var(--secondary-color);
|
||||
background: var(--code-background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.reaction-pill-paid {
|
||||
background: rgba(255, 196, 0, 0.12);
|
||||
border-color: rgba(255, 196, 0, 0.35);
|
||||
color: #9a6a00;
|
||||
}
|
||||
|
||||
.reaction-emoji {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.reaction-emoji img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.reaction-count {
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tag-box {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -327,6 +327,17 @@ audio::-webkit-media-controls-panel {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.reaction-box {
|
||||
border-left: 2px solid var(--border-color);
|
||||
padding: 6px 0px 24px 30px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.text-box + .reaction-box {
|
||||
margin-top: -12px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.text-box p:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ const { post, isItem } = Astro.props
|
||||
|
||||
const channel = getEnv(import.meta.env, Astro, 'CHANNEL')
|
||||
const COMMENTS = getEnv(import.meta.env, Astro, 'COMMENTS')
|
||||
const REACTIONS = getEnv(import.meta.env, Astro, 'REACTIONS')
|
||||
|
||||
const datetime = dayjs(post.datetime).tz(timezone)
|
||||
const timeago = datetime.isBefore(dayjs().subtract(1, 'w')) ? datetime.format('HH:mm · ll · ddd') : datetime.fromNow()
|
||||
@@ -29,6 +30,28 @@ const timeago = datetime.isBefore(dayjs().subtract(1, 'w')) ? datetime.format('H
|
||||
</div>
|
||||
</div>
|
||||
{post.content.length > 0 && <div class={`text-box content`} set:html={post.content} />}
|
||||
{
|
||||
REACTIONS && post.reactions?.length > 0 && (
|
||||
<div class="reaction-box">
|
||||
<div class="reaction-list">
|
||||
{post.reactions.map((reaction) => (
|
||||
<span class={`reaction-pill${reaction.isPaid ? ' reaction-pill-paid' : ''}`}>
|
||||
<span class="reaction-emoji">
|
||||
{
|
||||
reaction.isPaid
|
||||
? '\u2B50'
|
||||
: reaction.emojiImage
|
||||
? <img src={reaction.emojiImage} alt={reaction.emoji || 'emoji'} loading="lazy" />
|
||||
: reaction.emoji
|
||||
}
|
||||
</span>
|
||||
<span class="reaction-count">{reaction.count}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
post.tags.length > 0 && (
|
||||
<div class="tag-box" style={post.content.length === 0 ? 'padding-top: 30px;' : ''}>
|
||||
|
||||
@@ -13,6 +13,42 @@ const cache = new LRUCache({
|
||||
},
|
||||
})
|
||||
|
||||
// Normalize emoji variants (e.g., heart variants)
|
||||
function normalizeEmoji(emoji) {
|
||||
const emojiMap = {
|
||||
'\u2764': '\u2764\uFE0F',
|
||||
'\u263A': '\u263A\uFE0F',
|
||||
'\u2639': '\u2639\uFE0F',
|
||||
'\u2665': '\u2764\uFE0F',
|
||||
}
|
||||
return emojiMap[emoji] || emoji
|
||||
}
|
||||
|
||||
function getCustomEmojiImage(emojiId, staticProxy = '') {
|
||||
if (!emojiId)
|
||||
return null
|
||||
const imageUrl = `https://t.me/i/emoji/${emojiId}.webp`
|
||||
return `${staticProxy}${imageUrl}`
|
||||
}
|
||||
|
||||
async function hydrateTgEmoji($, content, { staticProxy } = {}) {
|
||||
const emojiNodes = $(content).find('tg-emoji')?.toArray() ?? []
|
||||
if (!emojiNodes.length)
|
||||
return
|
||||
|
||||
await Promise.all(emojiNodes.map((emojiEl) => {
|
||||
const emojiId = $(emojiEl).attr('emoji-id')
|
||||
if (!emojiId)
|
||||
return
|
||||
|
||||
const imageUrl = getCustomEmojiImage(emojiId, staticProxy)
|
||||
if (imageUrl) {
|
||||
const imageMarkup = `<img class="tg-emoji" src="${imageUrl}" alt="" loading="lazy" />`
|
||||
$(emojiEl).replaceWith(imageMarkup)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
function getVideoStickers($, item, { staticProxy, index }) {
|
||||
return $(item).find('.js-videosticker_video')?.map((_index, video) => {
|
||||
const url = $(video)?.attr('src')
|
||||
@@ -101,7 +137,8 @@ function getReply($, item, { channel }) {
|
||||
return $.html(reply)
|
||||
}
|
||||
|
||||
function modifyHTMLContent($, content, { index } = {}) {
|
||||
async function modifyHTMLContent($, content, { index, staticProxy } = {}) {
|
||||
await hydrateTgEmoji($, content, { staticProxy })
|
||||
$(content).find('.emoji')?.removeAttr('style')
|
||||
$(content).find('a')?.each((_index, a) => {
|
||||
$(a)?.attr('title', $(a)?.text())?.removeAttr('onclick')
|
||||
@@ -137,11 +174,59 @@ function modifyHTMLContent($, content, { index } = {}) {
|
||||
return content
|
||||
}
|
||||
|
||||
function getPost($, item, { channel, staticProxy, index = 0 }) {
|
||||
function getReactions($, item, staticProxy) {
|
||||
const reactions = []
|
||||
const reactionNodes = $(item).find('.tgme_widget_message_reactions .tgme_reaction').toArray()
|
||||
|
||||
for (const reaction of reactionNodes) {
|
||||
const isPaid = $(reaction).hasClass('tgme_reaction_paid')
|
||||
let emoji = ''
|
||||
let emojiId
|
||||
let emojiImage
|
||||
|
||||
const standardEmoji = $(reaction).find('.emoji b')
|
||||
if (standardEmoji.length) {
|
||||
emoji = normalizeEmoji(standardEmoji.text().trim())
|
||||
}
|
||||
|
||||
const tgEmoji = $(reaction).find('tg-emoji')
|
||||
if (tgEmoji.length && !emoji) {
|
||||
emojiId = tgEmoji.attr('emoji-id')
|
||||
if (emojiId) {
|
||||
const imageUrl = getCustomEmojiImage(emojiId, staticProxy)
|
||||
if (imageUrl) {
|
||||
emojiImage = imageUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPaid && !emoji && !emojiImage) {
|
||||
emoji = '\u2B50'
|
||||
}
|
||||
|
||||
const clone = $(reaction).clone()
|
||||
clone.find('.emoji, tg-emoji, i').remove()
|
||||
const count = clone.text().trim()
|
||||
|
||||
if (count) {
|
||||
reactions.push({
|
||||
emoji,
|
||||
emojiId,
|
||||
emojiImage,
|
||||
count,
|
||||
isPaid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return reactions
|
||||
}
|
||||
|
||||
async function getPost($, item, { channel, staticProxy, index = 0, reactionsEnabled } = {}) {
|
||||
item = item ? $(item).find('.tgme_widget_message') : $('.tgme_widget_message')
|
||||
const content = $(item).find('.js-message_reply_text')?.length > 0
|
||||
? modifyHTMLContent($, $(item).find('.tgme_widget_message_text.js-message_text'), { index })
|
||||
: modifyHTMLContent($, $(item).find('.tgme_widget_message_text'), { index })
|
||||
? await modifyHTMLContent($, $(item).find('.tgme_widget_message_text.js-message_text'), { index, staticProxy })
|
||||
: await modifyHTMLContent($, $(item).find('.tgme_widget_message_text'), { index, staticProxy })
|
||||
const title = content?.text()?.match(/^.*?(?=[。\n]|http\S)/g)?.[0] ?? content?.text() ?? ''
|
||||
const id = $(item).attr('data-post')?.replace(new RegExp(`${channel}/`, 'i'), '')
|
||||
|
||||
@@ -179,6 +264,7 @@ function getPost($, item, { channel, staticProxy, index = 0 }) {
|
||||
}
|
||||
return `${p1}${staticProxy}${p2}`
|
||||
}),
|
||||
reactions: reactionsEnabled ? getReactions($, item, staticProxy) : [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +283,7 @@ export async function getChannelInfo(Astro, { before = '', after = '', q = '', t
|
||||
const host = getEnv(import.meta.env, Astro, 'TELEGRAM_HOST') ?? 't.me'
|
||||
const channel = getEnv(import.meta.env, Astro, 'CHANNEL')
|
||||
const staticProxy = getEnv(import.meta.env, Astro, 'STATIC_PROXY') ?? '/static/'
|
||||
const reactionsEnabled = getEnv(import.meta.env, Astro, 'REACTIONS')
|
||||
|
||||
const url = id ? `https://${host}/${channel}/${id}?embed=1&mode=tme` : `https://${host}/s/${channel}`
|
||||
const headers = Object.fromEntries(Astro.request.headers)
|
||||
@@ -221,19 +308,21 @@ export async function getChannelInfo(Astro, { before = '', after = '', q = '', t
|
||||
|
||||
const $ = cheerio.load(html, {}, false)
|
||||
if (id) {
|
||||
const post = getPost($, null, { channel, staticProxy })
|
||||
const post = await getPost($, null, { channel, staticProxy, reactionsEnabled })
|
||||
cache.set(cacheKey, post)
|
||||
return post
|
||||
}
|
||||
const posts = $('.tgme_channel_history .tgme_widget_message_wrap')?.map((index, item) => {
|
||||
return getPost($, item, { channel, staticProxy, index })
|
||||
})?.get()?.reverse().filter(post => ['text'].includes(post.type) && post.id && post.content)
|
||||
const posts = (await Promise.all(
|
||||
$('.tgme_channel_history .tgme_widget_message_wrap')?.map((index, item) => {
|
||||
return getPost($, item, { channel, staticProxy, index, reactionsEnabled })
|
||||
})?.get() ?? [],
|
||||
))?.reverse().filter(post => ['text'].includes(post.type) && post.id && post.content)
|
||||
|
||||
const channelInfo = {
|
||||
posts,
|
||||
title: $('.tgme_channel_info_header_title')?.text(),
|
||||
description: $('.tgme_channel_info_description')?.text(),
|
||||
descriptionHTML: modifyHTMLContent($, $('.tgme_channel_info_description'))?.html(),
|
||||
descriptionHTML: (await modifyHTMLContent($, $('.tgme_channel_info_description'), { staticProxy }))?.html(),
|
||||
avatar: $('.tgme_page_photo_image img')?.attr('src'),
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ const targetWhitelist = [
|
||||
|
||||
export async function GET({ request, params, url }) {
|
||||
try {
|
||||
const target = new URL(params.url + url.search)
|
||||
const rawTarget = params.url + url.search
|
||||
const normalizedTarget = rawTarget.startsWith('//') ? `https:${rawTarget}` : rawTarget
|
||||
const target = new URL(normalizedTarget)
|
||||
if (!targetWhitelist.some(domain => target.hostname.endsWith(domain))) {
|
||||
return Response.redirect(target.toString(), 302)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user