Typecho 1015 4

    折腾博客 之 通过Freshrss API实现朋友文章

    AI摘要:本文介绍了通过Freshrss API实现获取朋友文章的方法。作者在部署Freshrss时遇到了一些问题,于是参考了一篇博客文章,使用Docker部署了Freshrss。然后作者介绍了如何获取API密码,并使用CF Workers来实现获取朋友文章的功能。最后,作者给出了一个使用PHP实现的调用示例。

    之前玩静态博客用过hexo-circle-of-friends来获取友情链接中的好友文章.
    如今这个项目我部署的过程中总是出现各种意外,不想继续折腾了,于是想起在大佬处看到 用FreshRSS 实现友圈rss订阅
    于是开始今日的折腾

    Docker部署Freshrss

    使用docker run 命令

    docker run -d --restart unless-stopped --log-opt max-size=10m \
      -p 8282:80 \
      -e TZ=Asia/Shanghai \
      -e 'CRON_MIN=1,31' \
      -v /root/rss/data:/var/www/FreshRSS/data \
      -v /root/rss/extensions:/var/www/FreshRSS/extensions \
      --name freshrss \
      freshrss/freshrss

    获取API密码

    1. 认证中打开允许 API 访问 (用于手机应用)选项
    2. 账户-API管理中输入API密码

    CF Workers

    这时候就要使用CF大法了.

    addEventListener('fetch', event => {
        event.respondWith(handleRequest(event.request));
    });
    
    const siteurl = 'Freshssr地址';
    
    async function handleRequest(request) {
        // 直接使用固定的用户名与密码
        const user = '用户名';
        const password = 'API密码';
        try {
            const authToken = await login(user, password);
            if (!authToken) {
                return new Response('Login failed.', { status: 403 });
            }
    
            const articles = await fetchArticles(authToken);
            const subscriptions = await fetchSubscriptions(authToken);
            const formattedArticles = formatArticles(articles, subscriptions);
    
            return new Response(JSON.stringify(formattedArticles, null, 2), {
                headers: { 'Content-Type': 'application/json' }
            });
        } catch (error) {
            return new Response('Error: ' + error.message, { status: 500 });
        }
    }
    
    async function login(user, password) {
        const loginUrl = `${siteurl}/api/greader.php/accounts/ClientLogin?Email=${encodeURIComponent(user)}&Passwd=${encodeURIComponent(password)}`;
        const response = await fetch(loginUrl);
        const text = await response.text();
        const match = text.match(/Auth=(.*)/);
        return match ? match[1] : null;
    }
    
    async function fetchArticles(authToken) {
        const articlesUrl = `${siteurl}/api/greader.php/reader/api/0/stream/contents/reading-list?&n=1000`;
        const response = await fetch(articlesUrl, {
            headers: {
                'Authorization': `GoogleLogin auth=${authToken}`
            }
        });
        const data = await response.json();
        return data.items || [];
    }
    
    async function fetchSubscriptions(authToken) {
        const subscriptionsUrl = `${siteurl}/api/greader.php/reader/api/0/subscription/list?output=json`;
        const response = await fetch(subscriptionsUrl, {
            headers: {
                'Authorization': `GoogleLogin auth=${authToken}`
            }
        });
        const data = await response.json();
        return data.subscriptions || [];
    }
    
    function formatArticles(articles, subscriptions) {
        const subscriptionMap = new Map();
        subscriptions.forEach(subscription => {
            subscriptionMap.set(subscription.id, subscription);
        });
    
        return articles.map(article => {
            const strippedContent = stripHtml(article.summary.content);
            const desc_length = strippedContent.length;
            const short_desc = desc_length > 20 
                ? strippedContent.substring(0, 99) + '...' 
                : strippedContent;
    
            const subscriptionId = article.origin.streamId;
            const subscription = subscriptionMap.get(subscriptionId);
    
            return {
                site_name: article.origin.title,
                title: article.title,
                link: article.alternate[0].href,
                time: new Date(article.published * 1000).toISOString(), // Convert timestamp to ISO string
                description: short_desc,
                icon: subscription ? `${siteurl}/${subscription.iconUrl.split('/').pop()}` : ''
            };
        });
    }
    
    function stripHtml(html) {
        return html.replace(/<[^>]*>?/gm, ''); // 使用正则表达式去除HTML标签
    }

    访问看看,输出 https://rss.imsun.workers.dev/
    由于cf的加载速度不是很理想,当然也可以使用php或者bash脚本生成json文件,这样可以加速页面加载的速度,大佬已经有代码贴出来了我就不复制了.
    如果是使用静态博客的话 则可以选择使用github工作流 生成静态文件并通过vercel加速访问

    调用

    我把朋友圈文章加在了友情链接页面

    使用php

    <?php
    // 获取JSON数据
    $jsonData = file_get_contents('https://rss.imsun.workers.dev/');
    
    // 检查是否成功获取数据
    if ($jsonData === false) {
        die('Error fetching JSON data.');
    }
    
    // 将JSON数据解析为PHP数组
    $articles = json_decode($jsonData, true);
    
    // 检查是否成功解析JSON
    if ($articles === null) {
        die('Error decoding JSON data.');
    }
    
    // 对文章按时间排序(最新的排在前面)
    usort($articles, function ($a, $b) {
        return strtotime($b['time']) - strtotime($a['time']);
    });
    
    // 设置每页显示的文章数量
    $itemsPerPage = 30;
    
    // 生成文章列表
    echo '<header class="archive--header"><h1 class="post--single__title">朋友文章</h1><h2 class="post--single__subtitle">通过Freshrss获取 </h2>
    </header><div class="articleList">';
    foreach (array_slice($articles, 0, $itemsPerPage) as $article) {
        // 格式化发布时间
        $date = new DateTime($article['time']);
        $date->setTimezone(new DateTimeZone('Asia/Shanghai')); // 设置为中国上海时区
        $formattedDate = $date->format('Y年m月d日 H:i:s');
    
        echo '<article class="post--item"><div class="content">';
    
        echo '<a target=_blank href="' . htmlspecialchars($article['link']) . '"><h2 class="post--title">' . htmlspecialchars($article['title']) . '</h2></a>';
        echo ' <div class="description">' . htmlspecialchars($article['description']) . '</div>';
        echo '<div class="meta"><img src="' . htmlspecialchars($article['icon']) . '"  class="icon" width="16" height="16" alt="Icon" />'. htmlspecialchars($article['site_name']) .'';
        echo '<svg class="icon" viewBox="0 0 1024 1024" width="16" height="16">
        <path
            d="M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m0 73.142857C323.486476 170.666667 170.666667 323.486476 170.666667 512s152.81981 341.333333 341.333333 341.333333 341.333333-152.81981 341.333333-341.333333S700.513524 170.666667 512 170.666667z m36.571429 89.697523v229.86362h160.865523v73.142857H512a36.571429 36.571429 0 0 1-36.571429-36.571429V260.388571h73.142858z">
        </path>
    </svg><time> ' . htmlspecialchars($formattedDate) . '</time></div>
        </div> ';
                
        echo '</article>';
    }
    echo '</div>';
    ?>

    大功告成

    使用JS

    <div class="articleList" id="articleList"></div>
    <button id="load-more">加载更多</button>
    
    <script>
    document.addEventListener('DOMContentLoaded', async () => {
        const jsonData = await fetchJsonData('https://cdn.jkjoy.cn/rss/rss.json');
        if (!jsonData) {
            console.error('Failed to fetch JSON data.');
            return;
        }
        const articles = jsonData;
    
        // 对文章按时间排序(最新的排在前面)
        articles.sort((a, b) => new Date(b.time) - new Date(a.time));
    
        // 初始化分页变量
        let currentPage = 1;
        const itemsPerPage = 30;
    
        // 加载第一页文章
        loadArticles(currentPage, itemsPerPage, articles);
    
        // 设置“加载更多”按钮点击事件
        const loadMoreButton = document.getElementById('load-more');
        loadMoreButton.addEventListener('click', () => {
            currentPage++;
            loadArticles(currentPage, itemsPerPage, articles);
        });
    });
    
    async function fetchJsonData(url) {
        try {
            const response = await fetch(url);
            if (response.ok) {
                return await response.json();
            } else {
                console.error('HTTP error:', response.status, response.statusText);
            }
        } catch (error) {
            console.error('Fetch error:', error);
        }
        return null;
    }
    
    function loadArticles(page, perPage, articles) {
        const startIndex = (page - 1) * perPage;
        const endIndex = page * perPage;
        const articleListElem = document.getElementById('articleList');
        
        articles.slice(startIndex, endIndex).forEach(article => {
            const articleElem = createArticleElement(article);
            articleListElem.appendChild(articleElem);
        });
    }
    
    function createArticleElement(article) {
        // 格式化发布时间
        const date = new Date(article.time);
        const formattedDate = new Intl.DateTimeFormat('zh-CN', {
            year: 'numeric', 
            month: '2-digit', 
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            hour12: false
        }).format(date);
    
        const div = document.createElement('div');
        div.className = 'post--item';
    
        const contentDiv = document.createElement('div');
        contentDiv.className = 'content';
    
        const link = document.createElement('a');
        link.href = article.link;
        link.target = '_blank';
        const title = document.createElement('h2');
        title.className = 'post--title';
        title.textContent = article.title;
        link.appendChild(title);
    
        const descriptionDiv = document.createElement('div');
        descriptionDiv.className = 'description';
        descriptionDiv.textContent = article.description;
    
        const metaDiv = document.createElement('div');
        metaDiv.className = 'meta';
    
        const iconImg = document.createElement('img');
        iconImg.src = article.icon;
        iconImg.width = 16;
        iconImg.height = 16;
        iconImg.alt = 'Icon';
        iconImg.className = 'icon';
    
        const siteName = document.createElement('span');
        siteName.textContent = article.site_name;
    
        const timeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        timeIcon.className = 'icon';
        timeIcon.setAttribute('viewBox', '0 0 1024 1024');
        timeIcon.setAttribute('width', '16');
        timeIcon.setAttribute('height', '16');
        timeIcon.innerHTML = `<path d="M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m0 73.142857C323.486476 170.666667 170.666667 323.486476 170.666667 512s152.81981 341.333333 341.333333 341.333333 341.333333-152.81981 341.333333-341.333333S700.513524 170.666667 512 170.666667z m36.571429 89.697523v229.86362h160.865523v73.142857H512a36.571429 36.571429 0 0 1-36.571429-36.571429V260.388571h73.142858z"></path>`;
    
        const timeText = document.createElement('time');
        timeText.textContent = formattedDate;
    
        metaDiv.appendChild(iconImg);
        metaDiv.appendChild(siteName);
        metaDiv.appendChild(timeIcon);
        metaDiv.appendChild(timeText);
    
        contentDiv.appendChild(link);
        contentDiv.appendChild(descriptionDiv);
        contentDiv.appendChild(metaDiv);
    
        div.appendChild(contentDiv);
    
        return div;
    }
    </script>
    老孙
    资深网民孙先生

    Warning: Undefined array key "HTTP_ACCEPT_LANGUAGE" in /www/sites/imsun.org/index/usr/themes/farallon/comments.php on line 4

    Deprecated: stripos(): Passing null to parameter #1 ($haystack) of type string is deprecated in /www/sites/imsun.org/index/usr/themes/farallon/comments.php on line 4
    1. 王云子
      王云子
      2024-08-01 12:22

      话说为什么打开链接前三个都是我的博文哈哈哈哈

        1. 老孙 作者
          2024-08-01 12:31
          @王云子

          它这个api是按照单个订阅源排列的,所以看到的前面的全是你的文章。
          实际显示效果是 在 链接页面显示的效果

    2. 蜘蛛抱蛋
      2024-07-29 09:48

      我也是这么用的,不过freshrss只能按照抓取时间排序,意味着新增的源永远排在前面,然后可定义的参数比较少,比如网站图标链接等等,除了这两点确实很合适

        1. 老孙 作者
          2024-07-29 10:23
          @蜘蛛抱蛋

          如果使用php的话这一切都不是问题 😂