Closed3

RSSHUBでBlueskyFeedsの中身を更に取得する

ぽぽこんぽぽこん

マウント先

  - ./docker/app/ttrss/feeds.js:/app/dist/feeds-CBPLXXDU.js:ro            # Bsky Feeds  
  - ./docker/app/ttrss/post.art:/app/dist/templates/post-7b0043bb.art:ro  # BskyPost template
ぽぽこんぽぽこん

feeds.js

import { __dirname, init_esm_shims } from './esm-shims-Dqvxr0BZ.js';
import './config-BpwDbAkH.js';
import './logger-B3QfaIfn.js';
import { parseDate } from './parse-date-D4osZfpm.js';
import './dist-BPTOq7kU.js';
import './helpers-nWIDkd0K.js';
import { cache_default as cache } from './cache-cV3Kcqf-.js';
import { art } from './render-DOz85fGU.js';
import './ofetch-ByTBixob.js';
import './got-D-bXW_9k.js';
import { ViewType } from './types-A5bA50Mg.js';
import { getFeed, getFeedGenerator, resolveHandle } from './utils-D7AD0nLZ.js';
import path from 'node:path';
init_esm_shims();

// 変更済み
export const route = {
    path: '/profile/:handle/feed/:space/:routeParams?',
    categories: ['social-media'],
    view: ViewType.SocialMedia,
    example: '/bsky.app/profile/jaz.bsky.social/feed/cv:cat',
    parameters: {
        handle: 'User handle, can be found in URL',
        space: 'Space ID, can be found in URL',
    },
    features: {
        requireConfig: false,
        requirePuppeteer: false,
        antiCrawler: false,
        supportBT: false,
        supportPodcast: false,
        supportScihub: false,
    },
    name: 'Feeds',
    maintainers: ['FerrisChi'],
    handler,
};

async function handler(ctx) {
    const handle = ctx.req.param('handle');
    const space = ctx.req.param('space');
    const DID = await resolveHandle(handle, cache.tryGet);
    const uri = `at://${DID}/app.bsky.feed.generator/${space}`;
    const profile = await getFeedGenerator(uri, cache.tryGet);
    const feeds = await getFeed(uri, cache.tryGet);
    
    const items = feeds.feed.map(({ post }) => {
        // 安全なデータクリーンアップ
        const safeEmbed = post.embed ? {
            ...post.embed,
            // recordタイプの場合、authorが存在しない場合はrecord自体をnullに
            record: (post.embed.record && post.embed.record.author) ? {
                ...post.embed.record,
                author: {
                    handle: post.embed.record.author.handle || 'unknown',
                    displayName: post.embed.record.author.displayName || post.embed.record.author.handle || 'Unknown Author'
                },
                value: post.embed.record.value || { text: '' },
                uri: post.embed.record.uri || ''
            } : null
        } : null;

        return {
            title: post.record?.text?.split('\n')[0] || 'Untitled Post',
            description: art(path.join(__dirname, 'templates/post-7b0043bb.art'), {
                text: post.record?.text?.replaceAll('\n', '<br>') || '',
                embed: safeEmbed,
            }),
            author: post.author?.displayName || post.author?.handle || 'Unknown Author',
            pubDate: parseDate(post.record?.createdAt),
            link: (() => {
                const defaultLink = `https://bsky.app/profile/${post.author?.handle || 'unknown'}/post/${post.uri?.split('app.bsky.feed.post/')[1] || ''}`;
                const postText = post.record?.text || '';
                const sizuMeRegex = /(https:\/\/sizu\.me\/[^\s]+)/;
                const match = postText.match(sizuMeRegex);
                if (match && match[1]) {
                    return match[1];
                }
                // embed.external の場合も考慮する
                if (post.embed && post.embed.$type === 'app.bsky.embed.external#view' && 
                    post.embed.external && post.embed.external.uri?.includes('https://sizu.me/')) {
                    return post.embed.external.uri;
                }
                return defaultLink;
            })(),
            upvotes: post.likeCount || 0,
            comments: post.replyCount || 0,
        };
    });

    ctx.set('json', {
        DID,
        profile,
        feeds,
    });

    return {
        title: `${profile.view?.displayName || 'Unknown'} — Bluesky`,
        description: profile.view?.description?.replaceAll('\n', ' ') || '',
        link: `https://bsky.app/profile/${handle}/feed/${space}`,
        image: profile.view?.avatar,
        icon: profile.view?.avatar,
        logo: profile.view?.avatar,
        item: items,
        allowEmpty: true,
    };
}

ぽぽこんぽぽこん

post.art

{{ if text }}
    {{@ text }}<br>
{{ /if }}

{{ if embed }}
    {{ if embed.$type === 'app.bsky.embed.images#view' }}
        {{ each embed.images i }}
            <img src="{{ i.fullsize }}" alt="{{ i.alt }}"><br>
        {{ /each }}
    {{ else if embed.$type === 'app.bsky.embed.video#view' }}
        <video 
            controls
            poster="{{ embed.thumbnail }}"
            style="max-width: 100%; height: auto;"
            preload="metadata">
            <source src="{{ embed.playlist }}" type="application/x-mpegURL">
            Your browser does not support HTML5 video playback.
        </video><br>
    {{ else if embed.$type === 'app.bsky.embed.external#view' }}
        <a href="{{ embed.external.uri }}"><b>{{ embed.external.title }}</b><br>
            {{ embed.external.description }}
        </a>
    {{ else if embed.$type === 'app.bsky.embed.record#view' || embed.$type === 'app.bsky.embed.recordWithMedia#view' }}
        {{ if embed.record }}
            <p>引用ポスト:</p>
            {{ if embed.record.author }}
                <a href="https://bsky.app/profile/{{ embed.record.author.handle }}/post/{{ embed.record.uri.split('app.bsky.feed.post/')[1] }}">
                    <b>{{ embed.record.author.displayName || embed.record.author.handle }}</b><br>
                    {{ if embed.record.value }}
                        {{ embed.record.value.text.split('\n')[0] }}
                    {{ /if }}
                </a>
            {{ else }}
                <p>作成者情報が利用できません</p>
            {{ /if }}
        {{ /if }}
    {{ /if }}
{{ /if }}
このスクラップは18日前にクローズされました