普通视图

发现新文章,点击刷新页面。
昨天以前老孙博客

Typecho根据slug添加icon

2025年5月5日 10:50

使用穷举的方式来匹配自定义icon

根据分类的slug来匹配

                            <?php 
                            switch($categories->slug) {
                                case 'images': echo '<i class="bi bi-images me-1"></i>';
                            break;
                                case 'share': echo '<i class="bi bi-share-fill me-1"></i>';
                            break;
                                case 'NULL': echo '<i class="bi bi-speaker-fill me-1"></i>';
                            break;
                                case 'memos': echo '<i class="bi bi-chat me-1"></i>';
                            break;
                                case 'codes': echo '<i class="bi bi-code me-1"></i>';
                            break;
                                case 'logs': echo '<i class="bi bi-person-fill me-1"></i>';
                            break;
                                case 'test': echo '<i class="bi bi-calendar-fill me-1"></i>';
                            break;
                                case 'tools': echo '<i class="bi bi-tools me-1"></i>';
                            break;
                                case 'music': echo '<i class="bi bi-music-note me-1"></i>';
                            break;
                                case 'links': echo '<i class="bi bi-link me-1"></i>';
                            break;
                                case 'video': echo '<i class="bi bi-camera-video me-1"></i>';
                            break;
                                case 'life': echo '<i class="bi bi-heart-fill me-1"></i>';
                            break;
                                case 'study': echo '<i class="bi bi-book-fill me-1"></i>';
                            break;
                                case 'news': echo '<i class="bi bi-newspaper me-1"></i>';
                            break;
                                case 'themes': echo '<i class="bi bi-palette me-1"></i>';
                            break;
                                case 'plugins': echo '<i class="bi bi-gear-fill me-1"></i>';
                            break;
                                case 'photo': echo '<i class="bi bi-images me-1"></i>';
                            break;
                                default: echo '<i class="bi bi-folder-fill me-1"></i>';
                            } ?>

同样也可以根据自定义页面的slug匹配

<?php $pages = Typecho_Widget::widget('Widget_Contents_Page_List'); ?>
<?php while($pages->next()): ?>
<li>
    <a href="<?php $pages->permalink(); ?>">
    <?php 
    switch($pages->slug) {
        case 'about': echo '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z"/><circle cx="12" cy="7" r="4" /><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /></svg> '; // 关于页面
        break;
        case 'links': echo '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-link" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z"/><path d="M10 14a3.5 3.5 0 0 0 5 0l4 -4a3.5 3.5 0 0 0 -5 -5l-.5 .5" /><path d="M14 10a3.5 3.5 0 0 0 -5 0l-4 4a3.5 3.5 0 0 0 5 5l.5 -.5" /></svg>'; // 链接页面
        break;
        case 'archives': echo '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-archive" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z"/><rect x="3" y="4" width="18" height="4" rx="2" /><path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" /><line x1="10" y1="12" x2="14" y2="12" /></svg>'; // 归档页面
        break;
        case 'gbook': echo '<svg  xmlns="http://www.w3.org/2000/svg"  class="icon icon-tabler icon-tabler-article" width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="2"  stroke-linecap="round"  stroke-linejoin="round"  class="icon icon-tabler icons-tabler-outline icon-tabler-article"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M7 8h10" /><path d="M7 12h10" /><path d="M7 16h10" /></svg>'; // 博客页面
        break;
        case 'messages': echo '<svg  xmlns="http://www.w3.org/2000/svg"  class="icon icon-tabler icon-tabler-messages" width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="2"  stroke-linecap="round"  stroke-linejoin="round"  class="icon icon-tabler icons-tabler-outline icon-tabler-messages"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M21 14l-3 -3h-7a1 1 0 0 1 -1 -1v-6a1 1 0 0 1 1 -1h9a1 1 0 0 1 1 1v10" /><path d="M14 15v2a1 1 0 0 1 -1 1h-7l-3 3v-10a1 1 0 0 1 1 -1h2" /></svg>'; // 留言页面
        break;
        default: echo '<svg  xmlns="http://www.w3.org/2000/svg"  class="icon icon-tabler icon-tabler-file" width="24"  height="24"  viewBox="0 0 24 24"  fill="none"  stroke="currentColor"  stroke-width="2"  stroke-linecap="round"  stroke-linejoin="round"  class="icon icon-tabler icons-tabler-outline icon-tabler-file"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" /></svg>'; // 默认图标
    } ?>
    <span><?php $pages->title(); ?></span>
    </a>
</li>
<?php endwhile; ?>

清明节 · 香港一日游

2025年4月5日 07:56

作为一个在广东生活了十几年的人来说,没去过香港算不算得上是一件很稀罕的事儿呢?

老板请客
老板请客

不得不说,香港的物价真的离谱.

香港街头
香港街头

摩天轮
摩天轮

游客贼多,摩天轮都要排队两小时

菲佣节日聚会
菲佣节日聚会

天桥下,地下通道出入口很多菲律宾人

摆渡轮
摆渡轮

坐摆渡轮也排队一个小时

星光大道
星光大道

星光大道

星光大道

星光大道李小龙

星光大道李小龙

太空馆
太空馆

Mastodon新通知推送NoneBot

2025年4月21日 16:25

起因

看了@1900 长毛象新通知推送TGBot

深受启发,于是通过ChatGPT 4.1 写了一个python脚本

定时检测Mastodon消息,有新通知会通过QQbot的API URL 发送消息通知给指定的QQ用户(Nonebot兼容)

使用

构建了一个docker镜像jkjoy/mastodon2qqbot

代码仓库在 https://github.com/jkjoy/dockerfile/blob/main/mastodon2qqbot/main.py

使用docker run 命令启动

docker run -d \
  -e MASTODON_INSTANCE="https://你的mastodon实例" \
  -e MASTODON_TOKEN="你的token" \
  -e QQ_API="https://bot.0tz.top/send_private_msg" \
  -e QQ_ID="你的QQ号码" \
  -e CHECK_INTERVAL="30" \
  jkjoy/mastodon2qqbot

30秒检查一次 Mastodon 消息

通过默认的QQ机器人2280858259 发送消息给我使用的QQ80116747

默认的QQ API 是https://bot.0tz.top/send_private_msg

使用默认QQ API需要添加QQ机器人2280858259为好友

Typecho 批量插入附件 并实现图片预览[2025/04/25更新]

2025年4月7日 10:52

在文章的附件选项页加入批量插入所有附件的按钮

并自动识别图片与普通文件,实现图片预览功能

2025-04-07T03:20:33.png
2025-04-07T03:20:33.png

Markdown语法格式自动修正为

![2025-04-07T02:49:18.png](https://img.imsun.org/2025/04/750116001.png)

具体代码实现
在主题的functions.php最后插入

/**
 * Typecho后台附件增强:图片预览、批量插入、保留官方删除按钮与逻辑
 * @author jkjoy
 * @date 2025-04-25
 */
Typecho_Plugin::factory('admin/write-post.php')->bottom = array('AttachmentHelper', 'addEnhancedFeatures');
Typecho_Plugin::factory('admin/write-page.php')->bottom = array('AttachmentHelper', 'addEnhancedFeatures');

class AttachmentHelper {
    public static function addEnhancedFeatures() {
        ?>
        <style>
        #file-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:15px;padding:15px;list-style:none;margin:0;}
        #file-list li{position:relative;border:1px solid #e0e0e0;border-radius:4px;padding:10px;background:#fff;transition:all 0.3s ease;list-style:none;margin:0;}
        #file-list li:hover{box-shadow:0 2px 8px rgba(0,0,0,0.1);}
        #file-list li.loading{opacity:0.7;pointer-events:none;}
        .att-enhanced-thumb{position:relative;width:100%;height:150px;margin-bottom:8px;background:#f5f5f5;overflow:hidden;border-radius:3px;display:flex;align-items:center;justify-content:center;}
        .att-enhanced-thumb img{width:100%;height:100%;object-fit:contain;display:block;}
        .att-enhanced-thumb .file-icon{display:flex;align-items:center;justify-content:center;width:100%;height:100%;font-size:40px;color:#999;}
        .att-enhanced-finfo{padding:5px 0;}
        .att-enhanced-fname{font-size:13px;margin-bottom:5px;word-break:break-all;color:#333;}
        .att-enhanced-fsize{font-size:12px;color:#999;}
        .att-enhanced-factions{display:flex;justify-content:space-between;align-items:center;margin-top:8px;gap:8px;}
        .att-enhanced-factions button{flex:1;padding:4px 8px;border:none;border-radius:3px;background:#e0e0e0;color:#333;cursor:pointer;font-size:12px;transition:all 0.2s ease;}
        .att-enhanced-factions button:hover{background:#d0d0d0;}
        .att-enhanced-factions .btn-insert{background:#467B96;color:white;}
        .att-enhanced-factions .btn-insert:hover{background:#3c6a81;}
        .att-enhanced-checkbox{position:absolute;top:5px;right:5px;z-index:2;width:18px;height:18px;cursor:pointer;}
        .batch-actions{margin:15px;display:flex;gap:10px;align-items:center;}
        .btn-batch{padding:8px 15px;border-radius:4px;border:none;cursor:pointer;transition:all 0.3s ease;font-size:10px;display:inline-flex;align-items:center;justify-content:center;}
        .btn-batch.primary{background:#467B96;color:white;}
        .btn-batch.primary:hover{background:#3c6a81;}
        .btn-batch.secondary{background:#e0e0e0;color:#333;}
        .btn-batch.secondary:hover{background:#d0d0d0;}
        .upload-progress{position:absolute;bottom:0;left:0;width:100%;height:2px;background:#467B96;transition:width 0.3s ease;}
        </style>
        <script>
        $(document).ready(function() {
            // 批量操作UI按钮
            var $batchActions = $('<div class="batch-actions"></div>')
                .append('<button type="button" class="btn-batch primary" id="batch-insert">批量插入</button>')
                .append('<button type="button" class="btn-batch secondary" id="select-all">全选</button>')
                .append('<button type="button" class="btn-batch secondary" id="unselect-all">取消全选</button>');
            $('#file-list').before($batchActions);

            // 插入格式
            Typecho.insertFileToEditor = function(title, url, isImage) {
                var textarea = $('#text'), 
                    sel = textarea.getSelection(),
                    insertContent = isImage ? '![' + title + '](' + url + ')' : 
                                            '[' + title + '](' + url + ')';
                textarea.replaceSelection(insertContent + '\n');
                textarea.focus();
            };

            // 批量插入
            $('#batch-insert').on('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                var content = '';
                $('#file-list li').each(function() {
                    if ($(this).find('.att-enhanced-checkbox').is(':checked')) {
                        var $li = $(this);
                        var title = $li.find('.att-enhanced-fname').text();
                        var url = $li.data('url');
                        var isImage = $li.data('image') == 1;
                        content += isImage ? '![' + title + '](' + url + ')\n' : '[' + title + '](' + url + ')\n';
                    }
                });
                if (content) {
                    var textarea = $('#text');
                    var pos = textarea.getSelection();
                    var newContent = textarea.val();
                    newContent = newContent.substring(0, pos.start) + content + newContent.substring(pos.end);
                    textarea.val(newContent);
                    textarea.focus();
                }
            });

            $('#select-all').on('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                $('#file-list .att-enhanced-checkbox').prop('checked', true);
                return false;
            });
            $('#unselect-all').on('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                $('#file-list .att-enhanced-checkbox').prop('checked', false);
                return false;
            });

            // 防止复选框冒泡
            $(document).on('click', '.att-enhanced-checkbox', function(e) {e.stopPropagation();});

            // 增强文件列表样式,但不破坏li原结构和官方按钮
            function enhanceFileList() {
                $('#file-list li').each(function() {
                    var $li = $(this);
                    if ($li.hasClass('att-enhanced')) return;
                    $li.addClass('att-enhanced');
                    // 只增强,不清空li
                    // 增加批量选择框
                    if ($li.find('.att-enhanced-checkbox').length === 0) {
                        $li.prepend('<input type="checkbox" class="att-enhanced-checkbox" />');
                    }
                    // 增加图片预览(如已有则不重复加)
                    if ($li.find('.att-enhanced-thumb').length === 0) {
                        var url = $li.data('url');
                        var isImage = $li.data('image') == 1;
                        var fileName = $li.find('.insert').text();
                        var $thumbContainer = $('<div class="att-enhanced-thumb"></div>');
                        if (isImage) {
                            var $img = $('<img src="' + url + '" alt="' + fileName + '" />');
                            $img.on('error', function() {
                                $(this).replaceWith('<div class="file-icon">🖼️</div>');
                            });
                            $thumbContainer.append($img);
                        } else {
                            $thumbContainer.append('<div class="file-icon">📄</div>');
                        }
                        // 插到插入按钮之前
                        $li.find('.insert').before($thumbContainer);
                    }

                });
            }

            // 插入按钮事件
            $(document).on('click', '.btn-insert', function(e) {
                e.preventDefault();
                e.stopPropagation();
                var $li = $(this).closest('li');
                var title = $li.find('.att-enhanced-fname').text();
                Typecho.insertFileToEditor(title, $li.data('url'), $li.data('image') == 1);
            });

            // 上传完成后增强新项
            var originalUploadComplete = Typecho.uploadComplete;
            Typecho.uploadComplete = function(attachment) {
                setTimeout(function() {
                    enhanceFileList();
                }, 200);
                if (typeof originalUploadComplete === 'function') {
                    originalUploadComplete(attachment);
                }
            };

            // 首次增强
            enhanceFileList();
        });
        </script>
        <?php
    }
}
?>

QQ-weather-bot

2025年3月25日 17:23

由于最近QQ风控似乎更加严格了,有两个QQ号码被封号,还有一个经常掉线.

为了检测QQ机器人是否在线 我就让 Claude 3.5 写了一个定时发送消息的 python 程序

单纯只发个消息未免 单调,于是 就调用高德地图 的 天气查询 APi 查询 指定地点的天气情况

于是 实现了 指定QQ机器人 给 指定 QQ用户 指定频率 发送 天气情况的功能

2025-03-25T09:21:37.png
2025-03-25T09:21:37.png

我真是太机智了,一下子就解决了 两个问题

Docker镜像 jkjoy/qq-weather-bot

最近一周 & 不吐不快

2025年3月25日 17:06

本来日子平平淡淡.没有波澜.
什么也不想写.


把主题更新了一下,把之前没有完善的功能完善一番,优化了部分功能的实现方式


重新移植了一个主题,就是因为上班摸鱼随便弄着玩.


前两天荆门市宣传部突然打电话找我,说我已备案的网站上出现了不合规定的内容责令我删除.

主要内容有两处:

  1. 含有vmess科学上网的内容
  2. 含有ChatGPT相关内容

翻墙我可以理解,但是chatgpt 为啥就不行呢
我好奇问了一句

给我回复是,chatgpt 国内能用不?是不是得翻墙?

此话一出我竟无言以对.

于是删除了相关页面.


本以为这事情就完事了

昨天,又打电话来询问.
问能不能提供文章删除的证明.
我说文章都已经删除了,怎么还需要我证明我已经删除了呢?
你访问不到不就证明我删除了么

Mango - 又一款Typecho Theme

2025年3月20日 10:50

简介

这是一款从wordpress 移植的双栏主题

原作者是 绘主题

预览

https://www.imsun.pw

使用

使用友情链接 必须使用插件Links

后台
后台

更新日志

1.1.0

  • 添加了菜单icon,匹配slug显示图标
  • 添加了分类icon,匹配slug显示图标
  • 添加了分类页面图片,匹配mid显示图片,图片在主题assets/img目录下,后缀名为.png
  • 添加了首页幻灯片的显示,在主题设置中配置幻灯片的文章cids,多个用逗号分隔,文章必须包含图片
  • 修复评论区的样式问题
  • 优化缩略图样式
页面slugicon
memos📝
links🔗
about👤
tags🏷
categories📂
search🔍
archives📜
comments💬
help
gbook📖
分类slugicon
images
share
NULL
memos
codes
logs
test
tools
music
links
video
life
study
news
themes
plugins
photo
default

1.0.1

  • 修复固定链接
  • 修复侧边缩略图
  • 增加评论等级

1.0.0

初始版本

下载

https://github.com/jkjoy/Typecho-Theme-Mango

本地下载

使用Docker快速部署邮件服务器 Mailserver

2025年3月15日 15:20

起因

想部署一个简单的邮件服务器 抛弃各种免费的企业邮局

主要是因为有太多账号都已经忘记了,而找回账号又是一阵折腾.

部署

Docker Compose 方式部署

services:
    mailserver:
        image: ghcr.io/docker-mailserver/docker-mailserver
        container_name: mailserver
        #env_file: ./.env
        healthcheck:
            retries: 0
            test: ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1
            timeout: 3s
        hostname: mail.imsun.org
        ports:
            - 25:25
            - 143:143
            - 465:465
            - 587:587
            - 993:993
        restart: always
        stop_grace_period: 1m
        volumes:
            - ./data/dms/mail-data/:/var/mail/
            - ./data/dms/mail-state/:/var/mail-state/
            - ./data/dms/mail-logs/:/var/log/mail/
            - ./data/dms/config/:/tmp/docker-mailserver/
            - /etc/localtime:/etc/localtime:ro
  • 务必开放以上端口 25 143 465 587 993
  • 如服务器端口被封禁则可以映射到其他端口

创建账户

启动容器之后

在容器中执行

setup email add admin@example.com

输入密码
重复确认密码
example.com域名为例

为域名添加DNS记录

A 记录 mail 指向 服务器IP
MX 记录 @ 指向 mail.example.com
PTR 记录 @ 指向 mail.example.com

4.21 更新 google 会提示拒收邮件..原因是没有验证SPF or DKIM

添加
TXT记录@指向 v=spf1 a mx ip4:74.48.*.* ~all

如此便可以使用smtp发送邮件了

SMTP 地址 mail.example.com
账户 : admin@example.com
密码 :
端口 : 587

webmail 客户端 roundcube

当然可以使用webmail客户端 roundcube 来进行收发邮件

使用Docker compose来快速部署

services:
  roundcubemail:
    image: roundcube/roundcubemail:1.6.9-apache
    volumes:
      - ./www:/var/www/html
      - ./db/sqlite:/var/roundcube/db
    environment:
      - ROUNDCUBEMAIL_DB_TYPE=sqlite
      - ROUNDCUBEMAIL_SKIN=elastic
      - ROUNDCUBEMAIL_DEFAULT_HOST=tls://mail.example.com
      - ROUNDCUBEMAIL_SMTP_SERVER=tls://mail.example.com
    ports:
      - 8080:80
  • 可以把8080 端口反向代理到 mail.example.com 域名
  • mail.example.com配置好SSL证书

访问 mail.example.com ,输入 账号密码 登录即可进行收发邮件了.

关于一些环境变量

# -----------------------------------------------
# --- Mailserver Environment Variables ----------
# -----------------------------------------------

# DOCUMENTATION FOR THESE VARIABLES IS FOUND UNDER
# https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/

# -----------------------------------------------
# --- General Section ---------------------------
# -----------------------------------------------

# empty => uses the `hostname` command to get the mail server's canonical hostname
# => Specify a fully-qualified domainname to serve mail for.  This is used for many of the config features so if you can't set your hostname (e.g. you're in a container platform that doesn't let you) specify it in this environment variable.
OVERRIDE_HOSTNAME=

# REMOVED in version v11.0.0! Use LOG_LEVEL instead.
DMS_DEBUG=0

# Set the log level for DMS.
# This is mostly relevant for container startup scripts and change detection event feedback.
#
# Valid values (in order of increasing verbosity) are: `error`, `warn`, `info`, `debug` and `trace`.
# The default log level is `info`.
LOG_LEVEL=info

# critical => Only show critical messages
# error => Only show erroneous output
# **warn** => Show warnings
# info => Normal informational output
# debug => Also show debug messages
SUPERVISOR_LOGLEVEL=

# 0 => mail state in default directories
# 1 => consolidate all states into a single directory (`/var/mail-state`) to allow persistence using docker volumes
ONE_DIR=1

# Support for deployment where these defaults are not compatible (eg: some NAS appliances):
# /var/mail vmail User ID (default: 5000)
DMS_VMAIL_UID=
# /var/mail vmail Group ID (default: 5000)
DMS_VMAIL_GID=

# **empty** => use FILE
# LDAP => use LDAP authentication
# OIDC => use OIDC authentication (not yet implemented)
# FILE => use local files (this is used as the default)
ACCOUNT_PROVISIONER=

# empty => postmaster@domain.com
# => Specify the postmaster address
POSTMASTER_ADDRESS=

# Check for updates on container start and then once a day
# If an update is available, a mail is sent to POSTMASTER_ADDRESS
# 0 => Update check disabled
# 1 => Update check enabled
ENABLE_UPDATE_CHECK=1

# Customize the update check interval.
# Number + Suffix. Suffix must be 's' for seconds, 'm' for minutes, 'h' for hours or 'd' for days.
UPDATE_CHECK_INTERVAL=1d

# Set different options for mynetworks option (can be overwrite in postfix-main.cf)
# **WARNING**: Adding the docker network's gateway to the list of trusted hosts, e.g. using the `network` or
# `connected-networks` option, can create an open relay
# https://github.com/docker-mailserver/docker-mailserver/issues/1405#issuecomment-590106498
# The same can happen for rootless podman. To prevent this, set the value to "none" or configure slirp4netns
# https://github.com/docker-mailserver/docker-mailserver/issues/2377
#
# none => Explicitly force authentication
# container => Container IP address only
# host => Add docker container network (ipv4 only)
# network => Add all docker container networks (ipv4 only)
# connected-networks => Add all connected docker networks (ipv4 only)
PERMIT_DOCKER=none

# Set the timezone. If this variable is unset, the container runtime will try to detect the time using
# `/etc/localtime`, which you can alternatively mount into the container. The value of this variable
# must follow the pattern `AREA/ZONE`, i.e. of you want to use Germany's time zone, use `Europe/Berlin`.
# You can lookup all available timezones here: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
TZ=

# In case you network interface differs from 'eth0', e.g. when you are using HostNetworking in Kubernetes,
# you can set NETWORK_INTERFACE to whatever interface you want. This interface will then be used.
#  - **empty** => eth0
NETWORK_INTERFACE=

# empty => modern
# modern => Enables TLSv1.2 and modern ciphers only. (default)
# intermediate => Enables TLSv1, TLSv1.1 and TLSv1.2 and broad compatibility ciphers.
TLS_LEVEL=

# Configures the handling of creating mails with forged sender addresses.
#
# **0** => (not recommended) Mail address spoofing allowed. Any logged in user may create email messages with a forged sender address (see also https://en.wikipedia.org/wiki/Email_spoofing).
# 1 => Mail spoofing denied. Each user may only send with his own or his alias addresses. Addresses with extension delimiters(http://www.postfix.org/postconf.5.html#recipient_delimiter) are not able to send messages.
SPOOF_PROTECTION=

# Enables the Sender Rewriting Scheme. SRS is needed if your mail server acts as forwarder. See [postsrsd](https://github.com/roehling/postsrsd/blob/master/README.md#sender-rewriting-scheme-crash-course) for further explanation.
#  - **0** => Disabled
#  - 1 => Enabled
ENABLE_SRS=0

# Enables the OpenDKIM service.
# **1** => Enabled
#   0   => Disabled
ENABLE_OPENDKIM=1

# Enables the OpenDMARC service.
# **1** => Enabled
#   0   => Disabled
ENABLE_OPENDMARC=1


# Enabled `policyd-spf` in Postfix's configuration. You will likely want to set this
# to `0` in case you're using Rspamd (`ENABLE_RSPAMD=1`).
#
# - 0     => Disabled
# - **1** => Enabled
ENABLE_POLICYD_SPF=1

# 1 => Enables POP3 service
# empty => disables POP3
ENABLE_POP3=

# Enables ClamAV, and anti-virus scanner.
#   1   => Enabled
# **0** => Disabled
ENABLE_CLAMAV=0

# Enables Rspamd
# **0** => Disabled
#   1   => Enabled
ENABLE_RSPAMD=0

# When `ENABLE_RSPAMD=1`, an internal Redis instance is enabled implicitly.
# This setting provides an opt-out to allow using an external instance instead.
# 0 => Disabled
# 1 => Enabled
ENABLE_RSPAMD_REDIS=

# When enabled,
#
# 1. the "[autolearning][rspamd-autolearn]" feature is turned on;
# 2. the Bayes classifier will be trained when moving mails from or to the Junk folder (with the help of Sieve scripts).
#
# **0** => disabled
# 1     => enabled
RSPAMD_LEARN=0

# This settings controls whether checks should be performed on emails coming
# from authenticated users (i.e. most likely outgoing emails). The default value
# is `0` in order to align better with SpamAssassin. We recommend reading
# through https://rspamd.com/doc/tutorials/scanning_outbound.html though to
# decide for yourself whether you need and want this feature.
RSPAMD_CHECK_AUTHENTICATED=0

# Controls whether the Rspamd Greylisting module is enabled.
# This module can further assist in avoiding spam emails by greylisting
# e-mails with a certain spam score.
#
# **0** => disabled
# 1     => enabled
RSPAMD_GREYLISTING=0

# Can be used to enable or disable the Hfilter group module.
#
# - 0     => Disabled
# - **1** => Enabled
RSPAMD_HFILTER=1

# Can be used to control the score when the HFILTER_HOSTNAME_UNKNOWN symbol applies. A higher score is more punishing. Setting it to 15 is equivalent to rejecting the email when the check fails.
#
# Default: 6
RSPAMD_HFILTER_HOSTNAME_UNKNOWN_SCORE=6

# Amavis content filter (used for ClamAV & SpamAssassin)
# 0 => Disabled
# 1 => Enabled
ENABLE_AMAVIS=1

# -1/-2/-3 => Only show errors
# **0**    => Show warnings
# 1/2      => Show default informational output
# 3/4/5    => log debug information (very verbose)
AMAVIS_LOGLEVEL=0

# This enables DNS block lists in Postscreen.
# Note: Emails will be rejected, if they don't pass the block list checks!
# **0** => DNS block lists are disabled
# 1     => DNS block lists are enabled
ENABLE_DNSBL=0

# If you enable Fail2Ban, don't forget to add the following lines to your `compose.yaml`:
#    cap_add:
#      - NET_ADMIN
# Otherwise, `nftables` won't be able to ban IPs.
ENABLE_FAIL2BAN=0

# Fail2Ban blocktype
# drop   => drop packet (send NO reply)
# reject => reject packet (send ICMP unreachable)
FAIL2BAN_BLOCKTYPE=drop

# 1 => Enables Managesieve on port 4190
# empty => disables Managesieve
ENABLE_MANAGESIEVE=

# **enforce** => Allow other tests to complete. Reject attempts to deliver mail with a 550 SMTP reply, and log the helo/sender/recipient information. Repeat this test the next time the client connects.
# drop => Drop the connection immediately with a 521 SMTP reply. Repeat this test the next time the client connects.
# ignore => Ignore the failure of this test. Allow other tests to complete. Repeat this test the next time the client connects. This option is useful for testing and collecting statistics without blocking mail.
POSTSCREEN_ACTION=enforce

# empty => all daemons start
# 1 => only launch postfix smtp
SMTP_ONLY=

# Please read [the SSL page in the documentation](https://docker-mailserver.github.io/docker-mailserver/latest/config/security/ssl) for more information.
#
# empty => SSL disabled
# letsencrypt => Enables Let's Encrypt certificates
# custom => Enables custom certificates
# manual => Let's you manually specify locations of your SSL certificates for non-standard cases
# self-signed => Enables self-signed certificates
SSL_TYPE=

# These are only supported with `SSL_TYPE=manual`.
# Provide the path to your cert and key files that you've mounted access to within the container.
SSL_CERT_PATH=
SSL_KEY_PATH=
# Optional: A 2nd certificate can be supported as fallback (dual cert support), eg ECDSA with an RSA fallback.
# Useful for additional compatibility with older MTA and MUA (eg pre-2015).
SSL_ALT_CERT_PATH=
SSL_ALT_KEY_PATH=

# Set how many days a virusmail will stay on the server before being deleted
# empty => 7 days
VIRUSMAILS_DELETE_DELAY=

# Configure Postfix `virtual_transport` to deliver mail to a different LMTP client (default is a dovecot socket).
# Provide any valid URI. Examples:
#
# empty => `lmtp:unix:/var/run/dovecot/lmtp` (default, configured in Postfix main.cf)
# `lmtp:unix:private/dovecot-lmtp` (use socket)
# `lmtps:inet:<host>:<port>` (secure lmtp with starttls)
# `lmtp:<kopano-host>:2003` (use kopano as mailstore)
POSTFIX_DAGENT=

# Set the mailbox size limit for all users. If set to zero, the size will be unlimited (default).
#
# empty => 0
POSTFIX_MAILBOX_SIZE_LIMIT=

# See https://docker-mailserver.github.io/docker-mailserver/edge/config/user-management/accounts/#notes
# 0 => Dovecot quota is disabled
# 1 => Dovecot quota is enabled
ENABLE_QUOTAS=1

# Set the message size limit for all users. If set to zero, the size will be unlimited (not recommended!)
#
# empty => 10240000 (~10 MB)
POSTFIX_MESSAGE_SIZE_LIMIT=

# Mails larger than this limit won't be scanned.
# ClamAV must be enabled (ENABLE_CLAMAV=1) for this.
#
# empty => 25M (25 MB)
CLAMAV_MESSAGE_SIZE_LIMIT=

# Enables regular pflogsumm mail reports.
# This is a new option. The old REPORT options are still supported for backwards compatibility. If this is not set and reports are enabled with the old options, logrotate will be used.
#
# not set => No report
# daily_cron => Daily report for the previous day
# logrotate => Full report based on the mail log when it is rotated
PFLOGSUMM_TRIGGER=

# Recipient address for pflogsumm reports.
#
# not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS
# => Specify the recipient address(es)
PFLOGSUMM_RECIPIENT=

# Sender address (`FROM`) for pflogsumm reports if pflogsumm reports are enabled.
#
# not set => Use REPORT_SENDER
# => Specify the sender address
PFLOGSUMM_SENDER=

# Interval for logwatch report.
#
# none => No report is generated
# daily => Send a daily report
# weekly => Send a report every week
LOGWATCH_INTERVAL=

# Recipient address for logwatch reports if they are enabled.
#
# not set => Use REPORT_RECIPIENT or POSTMASTER_ADDRESS
# => Specify the recipient address(es)
LOGWATCH_RECIPIENT=

# Sender address (`FROM`) for logwatch reports if logwatch reports are enabled.
#
# not set => Use REPORT_SENDER
# => Specify the sender address
LOGWATCH_SENDER=

# Defines who receives reports if they are enabled.
# **empty** => ${POSTMASTER_ADDRESS}
# => Specify the recipient address
REPORT_RECIPIENT=

# Defines who sends reports if they are enabled.
# **empty** => mailserver-report@${DOMAINNAME}
# => Specify the sender address
REPORT_SENDER=

# Changes the interval in which log files are rotated
# **weekly** => Rotate log files weekly
# daily => Rotate log files daily
# monthly => Rotate log files monthly
#
# Note: This Variable actually controls logrotate inside the container
# and rotates the log files depending on this setting. The main log output is
# still available in its entirety via `docker logs mail` (Or your
# respective container name). If you want to control logrotation for
# the Docker-generated logfile see:
# https://docs.docker.com/config/containers/logging/configure/
#
# Note: This variable can also determine the interval for Postfix's log summary reports, see [`PFLOGSUMM_TRIGGER`](#pflogsumm_trigger).
LOGROTATE_INTERVAL=weekly


# If enabled, employs `reject_unknown_client_hostname` to sender restrictions in Postfix's configuration.
#
# - **0** => Disabled
# - 1 => Enabled
POSTFIX_REJECT_UNKNOWN_CLIENT_HOSTNAME=0

# Choose TCP/IP protocols for postfix to use
# **all** => All possible protocols.
# ipv4 => Use only IPv4 traffic. Most likely you want this behind Docker.
# ipv6 => Use only IPv6 traffic.
#
# Note: More details at http://www.postfix.org/postconf.5.html#inet_protocols
POSTFIX_INET_PROTOCOLS=all

# Choose TCP/IP protocols for dovecot to use
# **all** => Listen on all interfaces
# ipv4 => Listen only on IPv4 interfaces. Most likely you want this behind Docker.
# ipv6 => Listen only on IPv6 interfaces.
#
# Note: More information at https://dovecot.org/doc/dovecot-example.conf
DOVECOT_INET_PROTOCOLS=all

# -----------------------------------------------
# --- SpamAssassin Section ----------------------
# -----------------------------------------------

ENABLE_SPAMASSASSIN=0

# deliver spam messages in the inbox (eventually tagged using SA_SPAM_SUBJECT)
SPAMASSASSIN_SPAM_TO_INBOX=1

# KAM is a 3rd party SpamAssassin ruleset, provided by the McGrail Foundation.
# If SpamAssassin is enabled, KAM can be used in addition to the default ruleset.
# - **0** => KAM disabled
# - 1 => KAM enabled
#
# Note: only has an effect if `ENABLE_SPAMASSASSIN=1`
ENABLE_SPAMASSASSIN_KAM=0

# spam messages will be moved in the Junk folder (SPAMASSASSIN_SPAM_TO_INBOX=1 required)
MOVE_SPAM_TO_JUNK=1

# spam messages wil be marked as read
MARK_SPAM_AS_READ=0

# add spam info headers if at, or above that level:
SA_TAG=2.0

# add 'spam detected' headers at that level
SA_TAG2=6.31

# triggers spam evasive actions
SA_KILL=10.0

# add tag to subject if spam detected
SA_SPAM_SUBJECT=***SPAM*****

# -----------------------------------------------
# --- Fetchmail Section -------------------------
# -----------------------------------------------

ENABLE_FETCHMAIL=0

# The interval to fetch mail in seconds
FETCHMAIL_POLL=300

# Enable or disable `getmail`.
#
# - **0** => Disabled
# - 1 => Enabled
ENABLE_GETMAIL=0

# The number of minutes for the interval. Min: 1; Max: 30.
GETMAIL_POLL=5

# -----------------------------------------------
# --- LDAP Section ------------------------------
# -----------------------------------------------

# A second container for the ldap service is necessary (i.e. https://hub.docker.com/r/bitnami/openldap/)

# empty => no
# yes => LDAP over TLS enabled for Postfix
LDAP_START_TLS=

# empty => mail.example.com
# Specify the `<dns-name>` / `<ip-address>` where the LDAP server is reachable via a URI like: `ldaps://mail.example.com`.
# Note: You must include the desired URI scheme (`ldap://`, `ldaps://`, `ldapi://`).
LDAP_SERVER_HOST=

# empty => ou=people,dc=domain,dc=com
# => e.g. LDAP_SEARCH_BASE=dc=mydomain,dc=local
LDAP_SEARCH_BASE=

# empty => cn=admin,dc=domain,dc=com
# => take a look at examples of SASL_LDAP_BIND_DN
LDAP_BIND_DN=

# empty** => admin
# => Specify the password to bind against ldap
LDAP_BIND_PW=

# e.g. `"(&(mail=%s)(mailEnabled=TRUE))"`
# => Specify how ldap should be asked for users
LDAP_QUERY_FILTER_USER=

# e.g. `"(&(mailGroupMember=%s)(mailEnabled=TRUE))"`
# => Specify how ldap should be asked for groups
LDAP_QUERY_FILTER_GROUP=

# e.g. `"(&(mailAlias=%s)(mailEnabled=TRUE))"`
# => Specify how ldap should be asked for aliases
LDAP_QUERY_FILTER_ALIAS=

# e.g. `"(&(|(mail=*@%s)(mailalias=*@%s)(mailGroupMember=*@%s))(mailEnabled=TRUE))"`
# => Specify how ldap should be asked for domains
LDAP_QUERY_FILTER_DOMAIN=

# -----------------------------------------------
# --- Dovecot Section ---------------------------
# -----------------------------------------------

# empty => no
# yes => LDAP over TLS enabled for Dovecot
DOVECOT_TLS=

# e.g. `"(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))"`
DOVECOT_USER_FILTER=

# e.g. `"(&(objectClass=PostfixBookMailAccount)(uniqueIdentifier=%n))"`
DOVECOT_PASS_FILTER=

# Define the mailbox format to be used
# default is maildir, supported values are: sdbox, mdbox, maildir
DOVECOT_MAILBOX_FORMAT=maildir

# empty => no
# yes => Allow bind authentication for LDAP
# https://wiki.dovecot.org/AuthDatabase/LDAP/AuthBinds
DOVECOT_AUTH_BIND=

# -----------------------------------------------
# --- Postgrey Section --------------------------
# -----------------------------------------------

ENABLE_POSTGREY=0
# greylist for N seconds
POSTGREY_DELAY=300
# delete entries older than N days since the last time that they have been seen
POSTGREY_MAX_AGE=35
# response when a mail is greylisted
POSTGREY_TEXT="Delayed by Postgrey"
# whitelist host after N successful deliveries (N=0 to disable whitelisting)
POSTGREY_AUTO_WHITELIST_CLIENTS=5

# -----------------------------------------------
# --- SASL Section ------------------------------
# -----------------------------------------------

ENABLE_SASLAUTHD=0

# empty => pam
# `ldap` => authenticate against ldap server
# `shadow` => authenticate against local user db
# `mysql` => authenticate against mysql db
# `rimap` => authenticate against imap server
# Note: can be a list of mechanisms like pam ldap shadow
SASLAUTHD_MECHANISMS=

# empty => None
# e.g. with SASLAUTHD_MECHANISMS rimap you need to specify the ip-address/servername of the imap server  ==> xxx.xxx.xxx.xxx
SASLAUTHD_MECH_OPTIONS=

# empty => Use value of LDAP_SERVER_HOST
# Note: You must include the desired URI scheme (`ldap://`, `ldaps://`, `ldapi://`).
SASLAUTHD_LDAP_SERVER=

# empty => Use value of LDAP_BIND_DN
# specify an object with privileges to search the directory tree
# e.g. active directory: SASLAUTHD_LDAP_BIND_DN=cn=Administrator,cn=Users,dc=mydomain,dc=net
# e.g. openldap: SASLAUTHD_LDAP_BIND_DN=cn=admin,dc=mydomain,dc=net
SASLAUTHD_LDAP_BIND_DN=

# empty => Use value of LDAP_BIND_PW
SASLAUTHD_LDAP_PASSWORD=

# empty => Use value of LDAP_SEARCH_BASE
# specify the search base
SASLAUTHD_LDAP_SEARCH_BASE=

# empty => default filter `(&(uniqueIdentifier=%u)(mailEnabled=TRUE))`
# e.g. for active directory: `(&(sAMAccountName=%U)(objectClass=person))`
# e.g. for openldap: `(&(uid=%U)(objectClass=person))`
SASLAUTHD_LDAP_FILTER=

# empty => no
# yes => LDAP over TLS enabled for SASL
# If set to yes, the protocol in SASLAUTHD_LDAP_SERVER must be ldap:// or missing.
SASLAUTHD_LDAP_START_TLS=

# empty => no
# yes => Require and verify server certificate
# If yes you must/could specify SASLAUTHD_LDAP_TLS_CACERT_FILE or SASLAUTHD_LDAP_TLS_CACERT_DIR.
SASLAUTHD_LDAP_TLS_CHECK_PEER=

# File containing CA (Certificate Authority) certificate(s).
# empty => Nothing is added to the configuration
# Any value => Fills the `ldap_tls_cacert_file` option
SASLAUTHD_LDAP_TLS_CACERT_FILE=

# Path to directory with CA (Certificate Authority) certificates.
# empty => Nothing is added to the configuration
# Any value => Fills the `ldap_tls_cacert_dir` option
SASLAUTHD_LDAP_TLS_CACERT_DIR=

# Specify what password attribute to use for password verification.
# empty => Nothing is added to the configuration but the documentation says it is `userPassword` by default.
# Any value => Fills the `ldap_password_attr` option
SASLAUTHD_LDAP_PASSWORD_ATTR=

# empty => `bind` will be used as a default value
# `fastbind` => The fastbind method is used
# `custom` => The custom method uses userPassword attribute to verify the password
SASLAUTHD_LDAP_AUTH_METHOD=

# Specify the authentication mechanism for SASL bind
# empty => Nothing is added to the configuration
# Any value => Fills the `ldap_mech` option
SASLAUTHD_LDAP_MECH=

# -----------------------------------------------
# --- SRS Section -------------------------------
# -----------------------------------------------

# envelope_sender => Rewrite only envelope sender address (default)
# header_sender => Rewrite only header sender (not recommended)
# envelope_sender,header_sender => Rewrite both senders
# An email has an "envelope" sender (indicating the sending server) and a
# "header" sender (indicating who sent it). More strict SPF policies may require
# you to replace both instead of just the envelope sender.
SRS_SENDER_CLASSES=envelope_sender

# empty => Envelope sender will be rewritten for all domains
# provide comma separated list of domains to exclude from rewriting
SRS_EXCLUDE_DOMAINS=

# empty => generated when the image is built
# provide a secret to use in base64
# you may specify multiple keys, comma separated. the first one is used for
# signing and the remaining will be used for verification. this is how you
# rotate and expire keys
SRS_SECRET=

# -----------------------------------------------
# --- Default Relay Host Section ----------------
# -----------------------------------------------

# Setup relaying all mail through a default relay host
#
# empty => don't configure default relay host
# default host and optional port to relay all mail through
DEFAULT_RELAY_HOST=

# -----------------------------------------------
# --- Multi-Domain Relay Section ----------------
# -----------------------------------------------

# Setup relaying for multiple domains based on the domain name of the sender
# optionally uses usernames and passwords in postfix-sasl-password.cf and relay host mappings in postfix-relaymap.cf
#
# empty => don't configure relay host
# default host to relay mail through
RELAY_HOST=

# empty => 25
# default port to relay mail
RELAY_PORT=25

# empty => no default
# default relay username (if no specific entry exists in postfix-sasl-password.cf)
RELAY_USER=

# empty => no default
# password for default relay user
RELAY_PASSWORD=

APU 黑苹果支持列表

2025年3月5日 10:51
得益于“NootedRed”驱动,目前部分AMD核显,即APU,包括桌面和移动端的处理器,可以实现黑苹果系统的安装。
https://chefkissinc.github.io/applehax/nootedred/

根据NootedRed官网的描述如下:支持Ryzen 1xxx (Athlon Silver/Gold) 到 5xxx,另外还有7x30系列。

综上来讲,核显主要以下几种的,可以安装黑苹果:

Radeon Graphics
Vega 3
Vega 8
Vega 11
Radeon Graphics (Vega)
RX Vega 11
RX Vega 10
Vega 9
Vega 6
GCN 5th gen(iGPU)
Radeon RX Vega 11

Typecho博客同步插件FediverseSync

2025年2月19日 12:08

介绍

这是一个在发布文章时自动同步到Fediverse(Mastodon/Gotosocial)的插件
在发布文章时触发

更新

后台增加同步管理的面板

已知问题

在使用Mastodon实例时,对嘟文的回复无法保存为评论.

功能

很简单的功能实现,使用AI编写

实现的功能

  • 使用gts作为博客的评论
  • 在发布文章时自动同步到Mastodon/Gotosocial的插件

使用

2025-03-03T11:56:46.png
2025-03-03T11:56:46.png

在后台设置填入实例的地址和token.
可选择摘要的字数.
默认禁用插件时不删除同步数据.
Token的获取可参照

下载地址

https://github.com/jkjoy/FediverseSyncForTypecho

Gotosocial 通过API获取用户Timeline

2025年2月17日 10:14

在测试Gotosocial 时发现
通过链接${Host}/api/v1/accounts/${userId}/statuses?limit=${limit}&exclude_replies=true&only_public=true
访问不到数据,提示Unauthorized: token not supplied需要提供token.

mastodonpleroma 还是可以正常访问的.

为了更好的解决这个问题(其实之前通过cloudflare workers已经可以解决了)
参见 https://www.imsun.org/archives/1643.html#%E9%80%9A%E8%BF%87Cloudflare-Workers%E8%8E%B7%E5%8F%96json%E6%95%B0%E6%8D%AE

这次使用vercel拿到数据.

https://github.com/jkjoy/Gotosocial-API-Vercel

主要代码是python

直接部署在vercel

环境变量

HOST=Gotosocial实例地址
USER_ID=Gotosocial用户ID
TOKEN=access_token

演示 https://gts.ima.cm

Gotosocial 的分域部署

2025年2月16日 09:29

我因为使用mastodon比较多,最近在与新关注的好友交流时发现他使用的GTS实例账号是顶级域名,访问时的网页地址却是二级域名,通过他我才知道GTS更新了很多有意思的功能

在最新的0.18.0 版本中新增了可信代理 trusted-proxies

跟我当时使用的版本相比,已经增加了许多功能

譬如 开放了注册页面, 譬如 分域部署
需要注意的是 这种部署布局的配置必须在第一次启动 GoToSocial 前完成。

主要是通过重定向来实现.把原本请求到账户所属域名的流量转发到真实的实例地址上去.

server {
  server_name ima.cm;                                                      # account-domain

  location /.well-known/webfinger {
    rewrite ^.*$ https://social.ima.cm/.well-known/webfinger permanent;    # host
  }

  location /.well-known/host-meta {
    rewrite ^.*$ https://social.ima.cm/.well-known/host-meta permanent;  # host
  }

  location /.well-known/nodeinfo {
    rewrite ^.*$ https://social.ima.cm/.well-known/nodeinfo permanent;   # host
  }
}

如此就需要部署一个新的GTS实例
修改一下docker-compose.yaml中的配置

services:
  gotosocial:
    image: superseriousbusiness/gotosocial:latest
    container_name: gotosocial
    user: 1000:1000
    networks:
      - gotosocial
    pull_policy: always  
    environment:
      GTS_HOST: social.ima.cm #实例地址
      GTS_ACCOUNT_DOMAIN: ima.cm #用户账户所属的域名
      GTS_DB_TYPE: sqlite #使用sqlite数据库
      GTS_DB_ADDRESS: /gotosocial/storage/sqlite.db
      GTS_STORAGE_BACKEND: s3 # 使用S3存储 ,如果不需要可以删除包含STORAGE的环境变量
      GTS_STORAGE_S3_BUCKET: user #桶名
      GTS_STORAGE_S3_ENDPOINT: s3.bitiful.net  #S3端点
      GTS_STORAGE_S3_ACCESS_KEY: kmX5VsV8cB4ma8jeAg #
      GTS_STORAGE_S3_SECRET_KEY: DJ9qG7pNAZy9 #密钥
      GTS_STORAGE_S3_PROXY: true #代理S3,不会显示S3地址
      GTS_ACCOUNTS_ALLOW_CUSTOM_CSS: true #允许自定义CSS
      TZ: Asia/Chongqing #时区
      GTS_SMTP_HOST: mail.cock.li #smtp服务器
      GTS_SMTP_PORT: 587 #必须使用TLS
      GTS_SMTP_USERNAME: admin@cock.li #用户名
      GTS_SMTP_PASSWORD: ******  #密码
      GTS_SMTP_FROM: admin@cock.li #邮箱地址
      GTS_INSTANCE_LANGUAGES: zh #中文
      GTS_ACCOUNTS_REGISTRATION_OPEN: true #开放注册
      GTS_TRUSTED_PROXIES: 172.18.0.1/16 #可信代理
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - ./data:/gotosocial/storage
    restart: "always"

networks:
  gotosocial:
    ipam:
      driver: default
      config:
        - subnet: "172.18.0.0/16"
          gateway: "172.18.0.1"

访问https://social.ima.cm/@m
显示的账户所属的域名是ima.cm
mastodon 可以通过 @m@ima.cm 来添加好友

使用Github Action自动生成静态相册

2024年12月28日 09:48

前言

使用memos调用的相册虽然方便,但是也有一些痛点无法解决

  1. 在CDN被刷流量之后,我已经几乎关闭所有的国内CDN服务,小水管服务器也无法满足大量图片同时加载,那龟速谁用谁知道.
  2. S3存储太贵,在COS被刷了流量之后(没错,我就是这么倒霉),我决定多地备份,主要上传在github,利用Cloudflare+ vercel+github pages+ 其他SaaS. 这些服务的优点就是免费.
  3. 模板来源于网络

Deepseek

主要使用AI来解决主要功能代码,主打一个能用就行.至于有什么bug,一个简单的网页生成 能有什么逆天bug也没关系的....吧
主要代码是 Python

功能

  • 我想要的功能
  1. 上传图片到Github 仓库,触发 Actions 自动生成Html页面
  2. 相册的缩略图需要压缩,点击显示原图,缓解小水管压力(没错,我会定时使用git拉取到国内服务器,主打一个多地都能访问)
  3. 根据图片名称自动生成标题
  • 更新
    2024.12.29
    又找了一个模板,自己觉得还可以
    加了进去
    在workflow里设置需要执行的脚本
    两个脚本 分别为 times.pylens.py 对应着两个模板

演示地址

lens模板 https://photo.asbid.cn
times模板 https://photo.sgcd.net

部署在Github Pages

使用

项目模板

https://github.com/jkjoy/generate-albums

设置

在自己仓库的Settings中找到

指示
指示

TOKEN为你的 Github token

REPO为你想要生成相册的仓库名称 如username/repo

设置
设置

上传规则

相册内容上传到 photos 这个目录下

photos 根目录下的照片默认标题为分享生活

新建文件夹, 该文件夹名称为此目录下所有图片的标题

  • 照片同名txt中的文本为描述说明 最高优先级
    1.jpg 1.txt 则使用1.txt中的文本为描述说明
  • 目录下描述.txt为此目录下所有图片的描述说明 第二优先级
  • 如果两者都没有则使用照片文件名为描述说明

其他部分

可以根据需求修改 template目录下对应模板的index.html文件 中的布局和内容.

每次修改仓库会自动触发Action 生成HTML到目标仓库,目标仓库可以使用Github Pages,也可以部署在Vercel,这里就不多做说明

演示

https://photos-jkjkjoy.vercel.app/

总结

AI真好用!!
https://movie.douban.com/subject/2136204/

Tianji - 一个集成 网站分析 + 上线监控 + 服务器状态 的应用

2024年12月27日 18:48

简介

一个All-in-one的项目,集成了uptime-kuma+ nezha + umami 的功能的超强应用.

项目地址

https://github.com/msgbyte/tianji

预览

2025-03-16T08:08:01.png
2025-03-16T08:08:01.png

部署

使用 docker compose 的方式来部署

mkdir /home/tianji
cd /home/tianji
vi docker-compose.yaml

docker-compose.yaml的内容如下

services:
  tianji:
    image: moonrailgun/tianji
    ports:
      - "12345:12345"
    environment:
      DATABASE_URL: postgresql://tianji:tianji@postgres:5432/tianji
      JWT_SECRET: replace-me-with-a-random-string
      ALLOW_REGISTER: "false"
      ALLOW_OPENAPI: "true"
    depends_on:
      - postgres
    restart: always
  postgres:
    image: postgres:15.4-alpine
    environment:
      POSTGRES_DB: tianji
      POSTGRES_USER: tianji
      POSTGRES_PASSWORD: tianji
    volumes:
      - ./tianji-db-data:/var/lib/postgresql/data
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5

运行容器

docker compose up -d

默认密码

Url: http://<your-ip>:12345
Default username: admin
Default password: admin

监控

https://0tz.top/status/imsun
可以使用一个小徽章显示状态

遥测

在页面插入一张图片就可以侦测到访问量,但是只能显示 pv 浏览量

多用户

没错,Tianji是支持多用户使用的,若是有想体验的朋友可以留言,我可以发送邀请.

通知

可以支持多种通知方式

访客统计

从木木老师那里得到灵感,使用API获取网站的统计数据并使用js 调用,替代Umami.

在后台获取token websiteID workspaceID

然后使用Cloudflare Workers来保护token.
新建一个workers,填入以下代码(需要修改的部分已经标注出来)

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

const tianji = 'https://tianji.top'; #自行修改
const token = 'sk_cf38cfb0bcb22d****'; #自行修改
const workspaceID = 'clnzoxcy10001vy2ohi4obbi0'; #自行修改
const websiteID = 'cm3sj40cs00018n2ezrgbdl5w'; #自行修改

async function handleRequest(request) {
  const url = `${tianji}/open/workspace/${workspaceID}/website/${websiteID}/stats`;

  // 获取当前时间的 Unix 时间戳(以毫秒为单位)
  const now = Date.now();

  // 设置查询参数
  const params = new URLSearchParams({
    startAt: '1704067200000', // 2024年1月1日的 Unix 时间戳(毫秒)
    endAt: now.toString()  
  });

  const fullUrl = `${url}?${params.toString()}`;

  const headers = new Headers({
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  });

  const response = await fetch(fullUrl, {
    method: 'GET',
    headers: headers
  });

  const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization'
  };

  if (request.method === 'OPTIONS') {
    return new Response(null, {
      headers: corsHeaders
    });
  }

  if (response.ok) {
    const data = await response.json();
    return new Response(JSON.stringify(data), {
      headers: {
        'Content-Type': 'application/json',
        ...corsHeaders
      }
    });
  } else {
    return new Response(`Failed to fetch data: ${response.status} ${response.statusText}`, {
      status: response.status,
      headers: corsHeaders
    });
  }
}

startAt 的值 是某时间的unix时间戳 可以自行设置查询范围

调用

使用html+js调用

👣<p>本站到访<span id="uniques">0</span>位朋友.</p><p>共浏览页面<span id="pageviews">0</span>次</p>
<script>
// 定义 API URL
const apiUrl = 'https://tj.imsun.org';

fetch(apiUrl)
    .then(response => {
        if (!response.ok) {
            throw new Error('error');
        }
        return response.json();
    })
    .then(data => {
        const pageviewsElement = document.getElementById('pageviews');
        const uniquesElement = document.getElementById('uniques');
        pageviewsElement.textContent = data.pageviews.value;
        uniquesElement.textContent = data.uniques.value;
    })
    .catch(error => {
        console.error('获取数据时出现问题:', error);
    });
</script>

其他

还有其他很多功能,等待摸索

使用 NeoDB API 构建观影页面

2024年12月26日 08:19

前言

使用 neodb.social 的API算是多一种选择.
豆瓣数据的获取还是不太方便

获取neodb的token

使用mastodon账号登录 https://neodb.social/
在右上角头像点击- 设置 - 找到更多设置

2024-12-26T00:09:57.png
2024-12-26T00:09:57.png

点击 查看已授权的应用程序

2024-12-26T00:11:01.png
2024-12-26T00:11:01.png

生成一个token即可

获取neodb API

在此使用项目
https://github.com/Lyunvy/neodb-shelf-api

可以部署在vercel上,过程就不赘述了

调用

在本主题的基础上修改

JS

class NeoDB {
    constructor(config) {
        this.container = config.container;
        this.types = config.types ?? ["book", "movie", "tv", "music", "game", "podcast"];
        this.baseAPI = config.baseAPI;
        this.type = "movie";
        this.status = "complete";
        this.finished = false;
        this.paged = 1;
        this.subjects = [];
        this._create();
    }

    on(event, element, callback) {
        const nodeList = document.querySelectorAll(element);
        nodeList.forEach((item) => {
            item.addEventListener(event, callback);
        });
    }

    _handleTypeClick() {
        this.on("click", ".neodb-navItem", (t) => {
            const self = t.currentTarget;
            if (self.classList.contains("current")) return;
            this.type = self.dataset.type;
            document.querySelector(".neodb-list").innerHTML = "";
            document.querySelector(".lds-ripple").classList.remove("u-hide");
            document.querySelector(".neodb-navItem.current").classList.remove("current");
            self.classList.add("current");
            this.paged = 1;
            this.finished = false;
            this.subjects = [];
            this._fetchData();
        });
    }

    _renderTypes() {
        document.querySelector(".neodb-nav").innerHTML = this.types
            .map((item) => {
                return `<span class="neodb-navItem${
                    this.type == item ? " current" : ""
                }" data-type="${item}">${item}</span>`;
            })
            .join("");
        this._handleTypeClick();
    }

    _fetchData() {
        const params = new URLSearchParams({
            type: "complete",
            category: this.type,
            page: this.paged.toString(),
        });
    
        return fetch(this.baseAPI + "?" + params.toString())
            .then((response) => response.json())
            .then((data) => {
                if (data.length) {
                    // 过滤重复项
                    data = data.filter(item => !this.subjects.some(existing => existing.item.id === item.item.id));
                    
                    if (data.length) {
                        this.subjects = [...this.subjects, ...data];
                        this._renderListTemplate();
                    }
    
                    document.querySelector(".lds-ripple").classList.add("u-hide");
                } else {
                    this.finished = true; // 没有更多数据
                    document.querySelector(".lds-ripple").classList.add("u-hide");
                }
            });
    }

    _renderListTemplate() {
        document.querySelector(".neodb-list").innerHTML = this.subjects
            .map((item) => {
                const coverImage = item.item.cover_image_url;
                const title = item.item.title;
                const rating = item.item.rating;
                const link = item.item.id;

                return `<div class="neodb-item">
                    <img src="${coverImage}" referrerpolicy="no-referrer" class="neodb-image">
                    <div class="neodb-score">
                        ${rating ? `<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 20.1l5.82 3.682c1.066.675 2.37-.322 2.09-1.584l-1.543-6.926 5.146-4.667c.94-.85.435-2.465-.799-2.567l-6.773-.602L13.29.89a1.38 1.38 0 0 0-2.581 0l-2.65 6.53-6.774.602C.052 8.126-.453 9.74.486 10.59l5.147 4.666-1.542 6.926c-.28 1.262 1.023 2.26 2.09 1.585L12 20.099z"></path></svg>${rating}` : ""}
                    </div>
                    <div class="neodb-title">
                        <a href="${link}" target="_blank">${title}</a>
                    </div>
                    
                </div>`;
            })
            .join("");
    }

    _handleScroll() {
        let isLoading = false; // 标志位,表示是否正在加载数据
        let lastScrollTop = 0; // 上一次的滚动位置
    
        window.addEventListener("scroll", () => {
            const scrollY = window.scrollY || window.pageYOffset;
            const moreElement = document.querySelector(".block-more");
    
            // 检查滚动到底部的条件
            if (
                moreElement.offsetTop + moreElement.clientHeight <= scrollY + window.innerHeight &&
                document.querySelector(".lds-ripple").classList.contains("u-hide") &&
                !this.finished &&
                !isLoading // 确保没有正在加载数据
            ) {
                isLoading = true; // 设置标志位为 true,表示正在加载数据
                document.querySelector(".lds-ripple").classList.remove("u-hide");
                this.paged++;
                this._fetchData().finally(() => {
                    isLoading = false; // 数据加载完成后,重置标志位
                });
            }
    
            // 更新上一次的滚动位置
            lastScrollTop = scrollY;
        });
    }

    _create() {
        if (document.querySelector(".neodb-container")) {
            const container = document.querySelector(this.container);
            if (!container) return;
            container.innerHTML = `
                <nav class="neodb-nav"></nav>
                <div class="neodb-list"></div>
                <div class="block-more block-more__centered">
                    <div class="lds-ripple"></div>
                </div>
            `;
            this._renderTypes();
            this._fetchData();
            this._handleScroll();
        }
    }
}

CSS

.neodb-container{--db-item-width:150px;--db-item-height:180px;--db-music-width:150px;--db-music-height:150px;--db-primary-color:var(--farallon-hover-color);--db-background-white:var(--farallon-background-white);--db-background-gray:var(--farallon-background-gray);--db-border-color:var(--farallon-border-color);--db-text-light:var(--farallon-text-light);}.neodb-nav{padding:30px 0 20px;display:flex;align-items:center;flex-wrap:wrap;}.neodb-navItem{font-size:20px;cursor:pointer;border-bottom:1px solid rgba(0,0,0,0);transition:0.5s border-color;display:flex;align-items:center;text-transform:capitalize;}.neodb-navItem.current,.neodb-navItem:hover{border-color:inherit;}.neodb-navItem{margin-right:20px;}.neodb-score svg{fill:#f5c518;margin-right:5px;}.neodb-list{display:flex;align-items:flex-start;flex-wrap:wrap;}.neodb-image{width:var(--db-item-width);height:var(--db-item-height);object-fit:cover;border-radius:4px;}.neodb-image:hover{box-shadow:0 0 10px var(--db-border-color);}.neodb-title{margin-top:2px;font-size:14px;line-height:1.4;}.neodb-title a:hover{color:var(--db-primary-color);text-decoration:underline;}.neodb-genreItem{background:var(--db-background-gray);font-size:12px;padding:5px 12px;border-radius:4px;margin-right:6px;margin-bottom:10px;line-height:1.4;cursor:pointer;}.neodb-genreItem.is-active,.neodb-genreItem:hover{background-color:var(--db-primary-color);color:var(--db-background-white);}.neodb-genres{padding-bottom:15px;display:flex;flex-wrap:wrap;}.neodb-genres.u-hide + .neodb-list{padding-top:10px;}.neodb-score{display:flex;align-items:center;font-size:14px;color:var(--db-text-light);}.neodb-item{width:var(--db-item-width);margin-right:20px;margin-bottom:20px;position:relative;}.neodb-item__music img{width:var(--db-music-width);height:var(--db-music-height);object-fit:cover;}.neodb-date{position:relative;font-size:20px;color:var(--farallon-text-light);font-weight:900;line-height:1;}.neodb-date::before{content:"";position:absolute;top:0.5em;bottom:-2px;left:-10px;width:3.4em;z-index:-1;background:var(--farallon-hover-color);opacity:0.3;transform:skew(-35deg);transition:opacity 0.2s ease;border-radius:3px 8px 10px 6px;}.neodb-date{margin-top:30px;margin-bottom:10px;}.neodb-dateList{padding-left:15px;padding-top:5px;padding-right:15px;}.neodb-card__list{display:flex;align-items:center;padding:15px 0;border-bottom:1px dotted var(--farallon-border-color);font-size:14px;color:rgba(0,0,0,0.55);}.neodb-card__list:last-child{border-bottom:0;}.neodb-card__list .title{font-size:18px;margin-bottom:5px;}.neodb-card__list .rating{margin:0 0 0px;font-size:14px;line-height:1;display:flex;align-items:center;}.neodb-card__list .rating .allstardark{position:relative;color:#f99b01;height:16px;width:80px;background-repeat:repeat;background-image:url("../images/star.svg");background-size:auto 100%;margin-right:5px;}.neodb-card__list .rating .allstarlight{position:absolute;left:0;color:#f99b01;height:16px;overflow:hidden;background-repeat:repeat;background-image:url("../images/star-fill.svg");background-size:auto 100%;}.neodb-card__list img{width:80px;border-radius:4px;height:80px;object-fit:cover;flex:0 0 auto;margin-right:15px;}.neodb-titleDate{display:flex;flex-direction:column;line-height:1.1;margin-bottom:10px;flex:0 0 auto;margin-right:15px;align-items:center;}.neodb-titleDate__day{font-weight:900;font-size:44px;}.neodb-titleDate__month{font-size:14px;color:var(--farallon-text-light);font-weight:900;}.neodb-list__card{display:block;}.neodb-dateList__card{display:flex;flex-wrap:wrap;align-items:flex-start;}.neodb-listBydate{display:flex;align-items:flex-start;margin-top:15px;}@media (max-width:600px){.neodb-listBydate{flex-direction:column;}}

HTML

    <div class="neodb-container"></div>
<script>
const neodb = new NeoDB({
    container: ".neodb-container",
    baseAPI: "https://neodb.imsun.org/api",
    types: ["book", "movie", "tv", "music", "game"],
});    
</script>

其中https://neodb.imsun.org/为 部署在 vercel 的绑定域名,可自行更改

再见百度SEO

2024年12月14日 10:52

无意中看到一个关于SEO的文章,说现在的SEO都改行了.
最主要的原因是百度SEO不好做了.

事实上百度搜索基本上已经看不到个人网站的身影了.
大部分只有寥寥几页的收录,无备案的域名更惨,几乎找不到踪迹.

国内的网络环境如此,不存在所谓的自由.

况且移动互联网时代搜索引擎就变得可有可无了,因为太多的APP形成的垂直搜索替代了综合性的搜索引擎

就好像你想看短视频 很多人就会去抖音搜索,
想旅游找攻略 很多人会去小红书搜索
想了解什么新闻,很多人会选择在微博搜索.
想购物会去淘宝 京东这些商城搜索.

在这个时代必定是移动互联网的时代,因为用户基数庞大到了无以复加的地步.

这也导致了PC端的网民大多数始终是2000年左右那一批

还有一些百度自身的原因,譬如竞价排名这些让人无力吐槽的玩意儿.
前几页的搜索结果就没有什么有价值的内容.
这也是用户流失的主要原因,丝毫不在乎用户体验的产品就应该有这样的结果.

使用PHP获取 Mastodon API数据

2024年12月12日 08:09

根据 Cloudflare Workers 获取API 拉取的时候有些慢,所以使用php 获取到json 数据并保存在本地,通过计划任务定时生成.

<?php
$userId = '110711427149362311'; //改为自己的
$instance = 'jiong.us'; //改为自己的
$baseUrl = 'https://' . $instance . '/api/v1/accounts/' . $userId . '/statuses';
$limit = 20; // Maximum limit per page
$toots = [];

for ($i = 0; $i < 25; $i++) { // 25 pages * 40 toots per page = 1000 toots
    $ch = curl_init();
    $url = $baseUrl . '?limit=' . $limit;

    if (isset($lastId)) {
        $url .= '&max_id=' . $lastId;
    }

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    //curl_setopt($ch, CURLOPT_HTTPHEADER, [
    //    'Authorization: Bearer ' . $accessToken
    //]);

    $response = curl_exec($ch);
    curl_close($ch);

    $data = json_decode($response, true);

    if (empty($data)) {
        break;
    }

    foreach ($data as $toot) {
        if (!isset($toot['reblog']) && !isset($toot['in_reply_to_id'])) {
            $toots[] = $toot;
        }
    }

    $lastId = end($data)['id'];
}

// Now $toots contains up to 1000 toots
$jsonData = json_encode($toots, JSON_PRETTY_PRINT);
file_put_contents('toot.json', $jsonData);
?>

保存为toot.php, 访问 toot.php 则会在同级目录生成 toot.json.

创建一个定时任务定时访问toot.php

我发现

2024年12月9日 11:57
  • 我发现Wordpress之前中毒大概率是被SQL注入了.
    我在宝塔的WAF防护看到Wordpress站点各种拦截的各种注入.
    宝塔的WAF可以免费用,1panel的就需要收费.这大概也是用Wordpress中招的原因之一吧.
    而Typecho就相对好一些.
  • 我发现人不能分心,特别是专心做一件的事情的时候一分心,再回过头来就不知道自己做了什么,接下来要做什么.
    会懵逼一大段时间.
  • 闲着没事干的时候学着写Dockerfile 构建了两个镜像 分别是 jkjoy/typechojkjoy/wordpress .
    都是使用Sqlite3 数据库.
  • 我发现人的购买欲望是一直都存在的,无论男女.或许是男女侧重的方向不同.就如同我又没忍住剁手,买了几个VPS.明明已经立了FLag.
  • 我发现最好别立Flag. 会脸疼.
  • 出了一个3杂的CN米. 怒赚360RMB
  • 把之前移植的BearBLOG的主题 又拿出来, 套上官网的style,还挺像那么回事.
  • 买了十年的 imsun.pw的域名,花费188RMB.
    用上了 刚买的 VPS 12刀.
    套上了bearblog的皮.
  • 看到群里有人出CC的VPS, 原价 25.25刀 2G/2H/100GB SSD/ 4TB 流量 还有7个月 70元.
    等手里16刀的到期 不续费了 ,换这个.

胖东来为何会被口诛笔伐

2024年11月27日 11:17
AI摘要DeepSeek
胖东来因要求员工结婚不付彩礼并采用家长制管理而受到批评,被指复辟旧社会体制。然而,胖东来试图解决高彩礼问题,提供稳定收入和良好工作环境,减少家庭矛盾。批评者认为其管理缺乏边界感,但支持者认为这是企业文化的一部分,员工自由选择是否接受。胖东来的管理模式被视为夹在旧制度和西方管理之间,难以长期发展。

现在的微信公众号清一色的都在讨论胖东来.

其中有一篇名为"胖东来,复辟旧社会体制"的文章,其标题简直媲美UC震惊部.

其开篇就是唱衰胖东来

胖东来必定走向衰落
胖东来要求员工结婚不许要或付彩礼,做不到将取消一切福利。
胖东来是典型的中式管理企业,也就是家长制管理,于东来,给自己的人设就是老东家,老族长,老掌柜的封建形象。像不想《白银谷》《胡雪岩》《乔家大院》。而判断一个中式企业是否走向衰落,就看他的管理是否有边界感,管理层越过了边界,进入了员工的私人领域。同理,很多家庭危机,多是老人没有边界感,老人插手子女的家庭,本着我为你好的原则制造家庭矛盾。

我曾在微博上因为彩礼问题与人激情对线过,很多女同志的说法出奇的一致,彩礼不能不收,因为彩礼是婚后的保障,是女性生育的"奖励".她们很多人都认为这是他们应得的.

那么,问题在胖东来被解决了.
在胖东来你不会觉得被压榨你有充分的时间来谈恋爱,照顾情侣,在生活上你有稳定且不菲的收入来源来保障你未来的生活.
这些不比做牛马快乐?这些不比借债来的彩礼更有安全感?
即完美解决了很多女性口中彩礼存在的意义.
又能让家庭不会因为彩礼而产生隔阂,何乐而不为.

至于被口诛笔伐,频频戴高帽吗?

胖东来企图解决的是目前高彩礼的社会问题,而不是一味的让人听从顺从,他毕竟是一家企业,若是不认同他的企业文化你完全可以去其他的企业. 你是自由的人.
留下来的自然也是衡量过利弊.觉得利大于弊.这才是问题的关键,而不是靠着自己的想象就开始高潮了,脑补了一场封建主义复辟的大戏.

胖东来,复辟旧社会体制胖东来是成功是短暂的,他最多能支撑一个企业的生命周期,无法长此发展,更走不出河南。这样的人治企业,难以复制,也难找到合适的接班人,即使二代能找到接班人,到第三代又面临同样的问题。企业是有生命周期的,学过市场营销的小伙伴一定很清楚,产品生命周期:投入期,成长期,成熟期,衰退期如果你喜欢读经管类书籍,例如吴晓波在他的《激荡三十年》,张磊《价值》,包括西方管理类书籍书中列举的那些曾经辉煌过的企业,如今仍然在世的不多。这就是企业生命周:发展、成长、成熟、衰退四个阶段胖东来的管理,是现代社会当下中国企业特别时期,出现的一种产物。没有接轨西方先进管理制度,又没有发展一套中国的管理制度,夹在旧社会制度和西方先进管理制度中间,杂交出来地 一种不中不洋的产物。西方的现代管理学诞生再二战期间,管理学之父彼得·德鲁克提出的,西方的管理制度源自卢梭的《社会契约论》,所以西方企业讲契约精神。中国管理沿用的是儒家思想,讲究仁义礼智信,看重的是员工忠诚,懂感恩,懂报答…… 这套思想可行吗?在历史的一定时期是可行的,例如山西晋商,徽商,浙商,福建闵商,广东潮商…… 他们的都曾经辉煌过。为什么说是在一定的历史时期呢?旧社会,人们受教育水平普遍不高,人们依赖家族和血缘关系,近代商帮的成功都是靠强大的血缘关系维系的。社会是发展的,从民国开始西方思想传入中国,新中国践行了马克思和列宁主义,可以说是建立《资本论》基础上的,传统宗族社会被打破,新的制度至今还在摸索。这造成了中国企业家,既想,又想的思维。既想用传统思想管理员工,让员工忠诚,感恩,又想用西方思想,实现契约,绩效管理,末位淘汰,灵活雇佣。人一旦上了年纪就容易糊涂,胖东来这种制度下,就像你们的父母,开始插手你的家庭,尤其是不争气的孩子,吃父母,住父母的,父母更有话语权了。你吃我、喝我、住我的,你就要听我的。

看看这人都在说什么..
一个给你吃给你住的人你不感恩戴德你想干什么? 父母的感受就无所谓了? 父母的做法就完全都是错误的吗?
这种就是自私小人的论调! 且狂悖自大!

一个让利给员工给社会的良心企业能与资本联系起来,这危言耸听的做派,我都怀疑其人是资本的走狗了.


经济没有独立,思想怎可能独立!


  • 关于越界
    有人说胖东来的做法是越界,说于东来是爹味.

从这个论点来说,于东来倡议员工拒绝彩礼,发放员工福利.听起来是有点干涉个人情感生活的意思.

在大多数企业还在压榨你的剩余价值,让996成为福报的时候,没人谈及个人生活,认为做牛马是应该的.
现在胖东来给出了高于其他企业的福利与薪酬的时候,社会上很多人却觉得干涉了私人生活. 其员工都没觉得有什么不对, 轮得到其他人置喙.

敢情自愿做牛马与被动接受正向的倡议就是爹味啊?

我拒绝天价彩礼,因为这才是真正的糟粕,是买卖的婚姻的变种,是包办婚姻的延续.


有人评价说一家企业不能不遵守契约精神云云.

只要不违背公序良俗,你管人家公司规定是什么啊? 现在很多企业的离谱规定难道少了?

双标都不带这么玩


其实我更想吐槽现今很多小企业都是加私人微信,创建N个群,下班还有N多消息!!这难道不是越界吗?为什么没有人去抨击去大肆报导呢?因为这件事常见到没有任何新鲜感了.算不得新闻,赚不来流量.

Typecho Farallon - 说说页面Memos 0.22-0.23

2024年11月5日 09:04
AI摘要:DeepSeek AI摘要 DeepSeek
本教程介绍了如何在Typecho中使用Typecho Farallon主题的Memos API来动态获取说说页面的数据。首先,在主题目录下新建一个`bb.php`文件,并在其中设置Memos的相关参数。然后,根据自己的Memos参数进行相应的更改。最后,在Typecho后台新增一个独立页面,并选择自定义模板为`哔哔页面`。通过这些步骤,就可以在Typecho中实现动态获取说说页面的功能。

Memos

一款由go语言开发的开源项目,有丰富的API.

支持 v0.20 - v0.23

说说页面的数据使用Memos 的 API 通过js动态获取

步骤

1.在主题目录下 新建 一个 bb.php

内容如下

<?php 
/**
 * 哔哔页面
 *
 * @package custom
 */
 if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
<?php $this->need('header.php'); ?>
<section class="site--main">
<header class="archive--header">
<h1 class="post--single__title"><?php $this->title() ?></h1>
</header>
    <article class="post--single">
        <div id="talk"></div>
<script>
        const memos = {
            host: 'https://memos.ee/', //更改memos 地址
            limit: '1000', //拉取memos数量
            creatorId: '1', //用户ID
            domId: '#talk', //默认
        };
</script>
    <script src="<?php $this->options->themeUrl('/dist/js/marked.min.js'); ?>"></script>
    <script src="<?php $this->options->themeUrl('/dist/js/view-image.min.js'); ?>"></script>
    <script src="https://jkjoycn.github.io/assets/js/bb.js"></script>
</article>
</section>
<?php $this->need('footer.php'); ?>

2.自行更改

        const memos = {
            host: 'https://memos.ee/', //更改memos 地址
            limit: '1000', //拉取memos数量
            creatorId: '1', //用户ID
            domId: '#talk', //默认
        };

之中的参数为自己的Memos参数

页面内的头像会拉取用户memos设置的头像

3.在Typecho后台新增独立页面

在自定义模板中选择哔哔页面

演示

https://www.imsun.org/bb/

如何使用Docker快速部署mastodon实例

2024年10月28日 09:17
AI摘要:DeepSeek AI摘要 DeepSeek
本文介绍了如何使用Docker快速部署Mastodon实例。首先需要创建目录并拉取镜像,然后修改docker compose配置文件并初始化PostgreSQL。接下来需要配置Mastodon,包括创建空白的.env.production文件并运行引导。最后启动Mastodon并创建管理员账号。完成以上步骤后,即可成功部署Mastodon实例。

Mastodon 是什么?

是自己也是全世界.

它可以私有化部署,所以说他是自己.

它可以连通世界,与世界各地的实例进行交互.所以也可以说是全世界.

它之所以被称之为联邦宇宙,自然与联邦政府类似,各自为政,但是相互连接.

相关文章



如何部署

直接使用docker compose部署是不可行的,需要按照步骤进行

创建目录

mkdir -p /home/mastodon/mastodon

进入目录

cd /home/mastodon/mastodon

拉取镜像

docker pull ghcr.io/mastodon/mastodon

修改docker compose配置文件

wget https://raw.githubusercontent.com/mastodon/mastodon/main/docker-compose.yml

修改docker compose文件中的版本号

初始化PostgreSQL

  • 重要!!!!!
docker run --name postgres14 -v /home/mastodon/mastodon/postgres14:/var/lib/postgresql/data -e   POSTGRES_PASSWORD=设置数据库管理员密码 --rm -d postgres:14-alpine

进入数据库

docker exec -it postgres14 psql -U postgres

创建用户名mastodon的密码

CREATE USER mastodon WITH PASSWORD '数据库密码(最好和数据库管理员密码不一样)' CREATEDB;

停止docker

docker stop postgres14

配置Mastodon

/home/mastodon/mastodon根文件夹中创建空白.env.production文件

cd /home/mastodon/mastodon
touch .env.production

运行引导

docker-compose run --rm web bundle exec rake mastodon:setup

按照提示进行操作

Below is your configuration, save it to an .env.production file outside Docker:之后会出现配置文件的数据,复制下来

写入.env.production

启动Mastodon

docker-compose down
docker-compose up -d

文件夹赋权

chown 991:991 -R ./public
chown -R 70:70 ./postgres14
docker-compose down
docker-compose up -d

创建管理员

docker exec mastodon-web-1 tootctl accounts create USERNAME --email EMAIL --confirmed --role Owner

至此完成

Follow 使用体验

2024年10月12日 09:17
AI摘要:DeepSeek AI摘要 DeepSeek
文章介绍了作者对于使用RSS的体验和喜好,以及介绍了一个名为"Follow"的工具,该工具具有互动属性,可以让每个人看到哪些人订阅了哪篇文章,从而形成一种新型的社交。作者还提到了使用RSSHUB关注小红书账户时遇到的问题,以及在创建新列表时出现的白屏情况。

RSS其实对于我而言是一种比较少的行为
是一种使用频率较少的工具,我更喜欢在各个独立的博客中游走,看到喜欢的评论就会点进去看看他的博客

Follow 就完美的加入了互动的属性,每个人都可以看到哪篇文章被哪些人订阅,这是一种新型的社交吧.

目前遇到的问题

通过RSSHUB关注小红书的账户会遇到403的情况,貌似是开启了防盗链?
2024-10-12T01:14:56.png
设置-列表创建新列表的时候会白屏

原来是旧版本的BUG,刚刚更新了最新的0.0.1-alpha.19版本 好像已经修复了这个问题

认证

通过添加

This message is used to verify that this feed (feedId:61724628700647450) belongs to me (userId:56257056869285888). Join me in enjoying the next generation information browser https://follow.is.

订阅

我的mastodon https://jiong.us/@sun.rss

Dokploy 一款好用的开源VPS面板

2024年10月11日 11:41
AI摘要:DeepSeek AI摘要 DeepSeek
Dokploy是一款稳定、易于使用的部署解决方案,旨在简化应用程序管理流程。它可以作为Heroku、Vercel和Netlify等平台的免费可托管替代品,使用Docker和Traefik构建。Dokploy具有S3备份、自动续签证书和随GitHub仓库自动更新部署等功能。它可以完成大多数项目的管理,而且安装也非常方便。此外,Dokploy还支持设置集群和多用户使用。

之前我介绍过easypanel,但是它是大部分功能免费使用,部分功能是收费使用的

这次介绍的 Dokploy 是一款稳定、易于使用的部署解决方案,旨在简化应用程序管理流程。 可将 Dokploy 视为 Heroku、Vercel 和 Netliify 等平台的免费可托管替代品, 使用稳定的 Docker 和灵活的 Traefik 构建.

它有S3备份,自动续签证书,还可以设置集群,多用户使用.等等.

官网中文网站

https://dokploy.com/zh-Hans

官方中文文档

https://docs.dokploy.com/cn/docs/core/get-started/installation

使用

Dokploy的安装部署也十分方便,只需要在纯净的系统中执行命令

curl -sSL https://dokploy.com/install.sh | sh

即可运行.

访问 ip:3000 先创建一个管理员账号

可以在后台设置中绑定一个域名来访问面板
1728618429226.png

这里我们来做一个基本的演示,部署一个memos服务
首先创建一个项目 点击Project-Create Project-Add a project

1728618533812.png

创建好之后进入该项目
1728618676972.png

部署数据库

点击Create Service
1728618718046.png

先创建一个database
1728618830205.png
1728618883740.png

点击Deploy部署.

成功后可以点击复制此处的数据库链接
1728618968708.png

创建应用

再新建一个Application
1728619030565.png
点击进入详情
2024-10-11T03:58:17.png
填入镜像ghcr.io/usememos/memos

环境变量

Environment Settings
填入

MEMOS_DRIVER=postgres
MEMOS_DSN=获取到的数据库链接

映射目录

Advanced-Volumes中设置 映射目录
Add Volumes-Volumes / Mounts
Volume Name 随便填写
Mount Path (In the container)填入/var/opt/memos

绑定域名

Domains
2024-10-11T05:40:25.png
如此添加即可,最好提前解析

完成所有设置
点击Deploy部署完成
等待片刻,打开绑定的域名查看结果.

快速部署一个发布memos的QQ机器人

2024年10月4日 19:15
AI摘要:DeepSeek AI摘要 DeepSeek
本文介绍了如何部署一个转发memos的QQ机器人。首先使用docker-compose部署,创建一个docker-compose.yaml文件,并在其中配置相关参数。然后登录napcat的webui,填写反向WS地址为ws://memos:8080/onebot/v11/ws即可。

根据上文

使用docker-compose部署
执行

mkdir qq2memos
cd qq2memos
vim docker-compose.yaml

输入以下内容

services:
  napcat:
    container_name: napcat
    mac_address: 02:42:ac:11:00:91 #自己修改
    environment:
      - ACCOUNT=3319693101 #QQ机器人号码
      - WSR_ENABLE=true
      - WS_URLS=["ws://memos:8080/onebot/v11/ws"]
      - NAPCAT_UID=0
      - NAPCAT_GID=0
    ports:
      - 6099:6099
      - 3000:3000
    restart: always
    image: mlikiowa/napcat-docker:latest
    volumes:
      - "./QQ:/app/.config/QQ"
      - "./config:/app/napcat/config"
    networks: 
      - memos
  memos:
    container_name: memos
    environment:
      - MEMOS_API=https://memos.imsun.org/api/v1/memo ##自己修改
    image: jkjoy/qq2memos:latest  
    volumes:  
      - "./data:/app/data"  
    restart: always
    networks: 
      - memos
networks:
  memos:

执行

docker-compose up -d

登录napcat的webui
ip:6099/webui/login.html

填写反向WS地址为

ws://memos:8080/onebot/v11/ws

即可

在群晖部署可使用
2024-10-04T11:22:28.png
提前在/docker/qqbot路径下 创建 data QQ config三个文件夹以免权限不足构建失败

使用Docker快速部署一个QQ机器人

2024年9月12日 08:50
AI摘要:DeepSeek AI摘要 DeepSeek
本文介绍了如何使用Docker快速部署一个QQ机器人,并通过对接Nonebot实现Memos机器人的功能。文章详细介绍了在Windows和Linux系统下的部署步骤,并提供了相应的代码示例。通过绑定Memos账号和转发消息发送到Memos,实现了将QQ机器人的消息转发到Memos的功能。最后,文章提供了一个QQ机器人的账号供读者尝试使用。

简介

介绍如何使用Docker快速部署一个QQ机器人并对接Nonebot实现Memos机器人的功能:

  • 绑定memos账号
  • 转发消息发送到memos

步骤

部署QQ机器人

这里使用的项目是基于QQNT的无头机器人方案,使用webui登录,相对于之前我部署的Go-cqhttp的方案的好处是不会被风控掉线.
稳定性很nice

使用的项目地址
使用的项目文档: https://llonebot.github.io/zh-CN/guide/getting-started

Windows系统

在windows下非常简单,下载QQNT版本的QQ,登录你的QQ机器人账号

https://github.com/super1207/install_llob/releases

下载 exe,双击运行即可,之后打开 QQ 的设置,看到了 LLOneBot 就代表安装成功了。

Linux系统

在linux下 我选择使用 的项目 NapCatQQ
地址 : https://github.com/NapNeko/NapCatQQ

使用Docker部署
docker-compose.yaml内容如下

services:
  napcat:
    environment:
      - ACCOUNT=153985848 #QQ机器人号码
      - WS_ENABLE=true
      - NAPCAT_UID=0
      - NAPCAT_GID=0
    ports:
      - 3001:3001 #上传端口
      - 6099:6099 #webui端口
      - 3000:3000 #http端口 
    restart: always
    image: mlikiowa/napcat-docker:latest
    volumes:
      - "./napcat/app/.config/QQ:/app/.config/QQ"
      - "./napcat/app/napcat/config:/app/napcat/config"
    network_mode: host #使用host的原因是为了方便对接宿主机的nonebot框架

启动

docker-compose up -d

访问 http://ip:6099/webui/login.html

注意 : 登录所使用的 token 在docker-compose.yaml 所在目录下的
/napcat/app/napcat/config中的webui.json
QQ20240912-083331.png

扫码登录

在设置页面中添加反向 WS 地址,地址为 ws://127.0.0.1:8080/onebot/v11/ws, 这里的 8080 是 NoneBot 输出的端口号,/onebot/v11/ws 是 NoneBot onebot 适配器默认的路径
2024-09-12T00:36:18.png

部署nonebot

要求环境​Python 版本 >= 3.9

按照文档操作 https://llonebot.github.io/zh-CN/guide/nonebot2

Memos转发机器人的实现

在nonebot 项目中

新建 bot.py 内容为

import nonebot
from nonebot.adapters.onebot.v11 import Adapter as ONEBOT_V11Adapter

# 初始化 NoneBot
nonebot.init()

# 注册适配器
driver = nonebot.get_driver()
driver.register_adapter(ONEBOT_V11Adapter)

# 在这里加载插件
nonebot.load_builtin_plugins("echo")  # 加载内置插件
nonebot.load_from_toml("pyproject.toml")  # 从 toml 文件加载插件

# 如果有额外的插件目录,可以这样加载
# nonebot.load_plugins("src/plugins")

if __name__ == "__main__":
    nonebot.run()

新建 memos/plugins 文件夹 , 在其下创建 qq_to_memos.py 内容为

from nonebot import on_command, on_message, get_driver
from nonebot.rule import to_me
from nonebot.adapters.onebot.v11 import Bot, Event, Message
from nonebot.params import CommandArg
import json
import os
import httpx
from typing import Dict, Any
import logging

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    filename='memos_bot.log',
    filemode='a'
)
logger = logging.getLogger(__name__)

# 文件路径
JSON_FILE = "users_data.json"

# 读取 JSON 数据
def read_json() -> Dict[str, Any]:
    if os.path.exists(JSON_FILE):
        with open(JSON_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)
    return {}

# 写入 JSON 数据
def write_json(data: Dict[str, Any]):
    with open(JSON_FILE, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=4)

# 初始化函数
async def init():
    if not os.path.exists(JSON_FILE):
        write_json({})
        logger.info(f"Created new JSON file: {JSON_FILE}")

# 注册命令
start = on_command("start", rule=to_me(), priority=5)

@start.handle()
async def handle_start(bot: Bot, event: Event, args: Message = CommandArg()):
    user_id = event.get_user_id()
    token = args.extract_plain_text().strip()
    if not token:
        await start.finish(" 请提供 Token,格式:/start <token>")
        logger.warning(f"User {user_id} failed to start due to missing token")
        return

    users_data = read_json()
    users_data[user_id] = {"token": token}
    write_json(users_data)

    logger.info(f"User {user_id} started successfully")
    await start.finish(" 绑定成功!现在您可以直接发送消息,我会将其保存到 Memos。")

# 处理所有消息
memo = on_message(priority=5)

@memo.handle()
async def handle_memo(bot: Bot, event: Event):
    user_id = event.get_user_id()
    message = event.get_message()

    users_data = read_json()
    user_info = users_data.get(user_id)

    if not user_info:
        await memo.finish(" 您还未绑定,请先使用 /start <token> 命令绑定。")
        logger.warning(f"Unstarted user {user_id} attempted to send a memo")
        return

    token = user_info["token"]

    text_content = message.extract_plain_text()

    # 如果消息为空,不处理
    if not text_content.strip():
        return

    # 发送到 Memos
    async with httpx.AsyncClient() as client:
        try:
            payload = {
                "content": text_content.strip(),
                "visibility": "PUBLIC"
            }
            response = await client.post(
                "https://memos.ee/api/v1/memos",
                json=payload,
                headers={"Authorization": f"Bearer {token}"}
            )
            response.raise_for_status()
            logger.info(f"Memo sent successfully for user {user_id}")

        except httpx.HTTPStatusError as e:
            logger.error(f"HTTP error occurred for user {user_id}: {e}")
            logger.error(f"Response content: {e.response.text}")
            await memo.finish(f" 发送失败,错误代码:{e.response.status_code},请检查您的 Token 和网络连接。")
            return
        except Exception as e:
            logger.error(f"Unexpected error occurred for user {user_id}: {e}")
            await memo.finish(" 发送过程中发生意外错误,请稍后重试。")
            return

    await memo.finish(" 已成功发送到 Memos!")

# 获取驱动器并注册启动事件
driver = get_driver()
driver.on_startup(init)

logger.info("Memos bot plugin initialized")

API 端点按自己需求更改即可.

在 pyproject.toml 中加入

plugins = []
plugin_dirs = ["memos/plugins"]

运行机器人

nb run

使用机器人

在聊天界面 使用命令

/start token

token 为 Memos 后台获取的对应的token
绑定成功后如下图效果
2024-09-12T00:44:49.png

然后直接发送消息

此时发送消息即可转发到 memos. 且默认为公开的 memos.

如需默认为其他状态 需修改 qq_to_memos.py 中 "visibility" 的 值 .

结尾

如果你想尝试一下此功能可以添加 QQ 机器人

153985848
需添加为好友且 在 https://memos.ee 注册获取 token 便可以使用.

使用Docker 快速部署 Pleroma 之 更新

2024年8月31日 09:52
AI摘要:本文介绍了如何使用Docker快速部署和更新Pleroma社交网络服务,支持最新2.7.0版本。步骤包括克隆仓库、编辑配置文件、启动容器以及创建管理员账户。通过Docker Compose工具,简化了Pleroma的部署流程。

前言

之前的docker 我测试仅仅支持我构建的2.4.2版本

这次支持最新的2.7.0版本,所以写了这次的更新

步骤

克隆仓库

git clone https://git.ima.cm/jkjoy/pleroma-docker-compose.git
cd pleroma-docker-compose

编辑配置

注意:
你需要编辑./environments/pleroma/pleroma.env 其中的 ops.pleroma.social 为你自己的域名

启动容器

执行

docker-compuse up -d

在初始化之后反向代理4000端口即可.

创建管理员

docker exec -it pleroma sh ~/bin/./pleroma_ctl user new admin admin@ow3.cn --admin

通过脚本获取Relay中继服务器的成员列表

2024年8月21日 20:00
AI摘要:本文介绍了如何通过脚本获取Relay中继服务器的成员列表。作者提供了一个Python脚本和一个Bash脚本,用于生成成员列表的JSON数据。Python脚本通过读取Redis中的键值对来获取中继服务器的域名,然后使用HTTP请求获取服务器的统计信息,并将这些信息保存为JSON格式的数据。Bash脚本用于在Docker容器中运行Python脚本,并将生成的JSON数据保存到文件中。最后,作者介绍了如何使用定时任务来定期更新成员列表。

后续

如何获取relay中继服务器的列表呢
参考项目 https://github.com/dragonfly-club/dragon-relay
这个项目呢 是把列表生成自定义的html页面,不够灵活,所以我改了一下
用以生成json数据

我原本的设想是通过dockerfile重新构建一个docker镜像,但是一想又嫌麻烦,所以只好通过曲线救国了///

使用方法

python

gen-member-list.py的内容

#!/usr/bin/python3

import logging
import requests
import base64
import json
from collections import Counter
from subprocess import Popen, PIPE
import shutil

outfile = 'output.json'
stats_file = 'stats.json'
USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0 (https://relay.jiong.us)'
TIMEOUT = 4

instance_ids = set()

def setup_logging():
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.INFO)
    log_handler = logging.FileHandler('gen-member-list.log')
    log_handler.setLevel(logging.INFO)
    log_format = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
    log_handler.setFormatter(log_format)
    logger.addHandler(log_handler)
    return logger

logger = setup_logging()

def get_redis_cli_path():
    redis_cli = shutil.which('redis-cli')
    if redis_cli:
        return redis_cli
    else:
        raise FileNotFoundError("redis-cli not found in PATH")

def read_redis_keys():
    redis_cli = get_redis_cli_path()
    cmd = [redis_cli]
    cmdin = 'KEYS relay:subscription:*'.encode('utf-8')
    p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
    return p.communicate(input=cmdin)[0].decode('utf-8')

def generate_instance_id(page):
    uid = []
    fields = ['uri', 'email', 'name', 'hcaptchaSiteKey']
    for field in fields:
        try:
            uid.append(str(page.get(field, '')))
        except AttributeError:
            pass

    try:
        if page.get('contact_account'):
            uid.append(str(page['contact_account'].get('id', '')))
            uid.append(str(page['contact_account'].get('username', '')))
    except AttributeError:
        pass

    return '_'.join(filter(None, uid))

def fetch_favicon(domain):
    try:
        favicon_url = f"https://{domain}/favicon.ico"
        response = requests.get(favicon_url, timeout=TIMEOUT)
        if response.status_code == 200:
            return base64.b64encode(response.content).decode('utf-8')
    except Exception as e:
        logger.warning(f"Failed to fetch favicon for {domain}: {str(e)}")
    return ""

def try_nodeinfo(headers, domain, timeout):
    nodeinfo_url = f"https://{domain}/.well-known/nodeinfo"
    response = requests.get(nodeinfo_url, headers=headers, timeout=timeout)
    nodeinfo_link = response.json()['links'][0]['href']

    response = requests.get(nodeinfo_link, headers=headers, timeout=timeout)
    nodeinfo = response.json()

    software = nodeinfo['software']['name']
    version = nodeinfo['software']['version']
    stats = nodeinfo['usage']

    fav_md = fetch_favicon(domain)
    title = nodeinfo["metadata"].get("nodeName", domain.split('.')[0].capitalize())

    uid = generate_instance_id(nodeinfo)

    json_line = {
        'favicon': fav_md,
        'title': title,
        'domain': domain,
        'users': stats['users']['total'],
        'posts': stats.get('localPosts', 0),
        'software': software,
        'version': version,
        'instances': nodeinfo.get('metadata', {}).get('federation', {}).get('domainCount', 0)
    }

    return json_line, uid

def try_mastodon(headers, domain, timeout):
    instance_url = f"https://{domain}/api/v1/instance"
    response = requests.get(instance_url, headers=headers, timeout=timeout)
    instance_info = response.json()

    stats_url = f"https://{domain}/api/v1/instance/peers"
    response = requests.get(stats_url, headers=headers, timeout=timeout)
    peers = response.json()

    fav_md = fetch_favicon(domain)
    title = instance_info['title']
    version = instance_info['version']

    uid = generate_instance_id(instance_info)

    json_line = {
        'favicon': fav_md,
        'title': title,
        'domain': domain,
        'users': instance_info['stats']['user_count'],
        'statuses': instance_info['stats']['status_count'],
        'instances': len(peers),
        'version': version,
        'software': 'mastodon'
    }

    return json_line, uid

def try_misskey(headers, domain, timeout):
    meta_url = f"https://{domain}/api/meta"
    response = requests.post(meta_url, headers=headers, timeout=timeout)
    meta_info = response.json()

    stats_url = f"https://{domain}/api/stats"
    response = requests.post(stats_url, headers=headers, timeout=timeout)
    stats = response.json()

    fav_md = fetch_favicon(domain)
    title = meta_info['name']
    version = meta_info['version']

    uid = generate_instance_id(meta_info)

    json_line = {
        'favicon': fav_md,
        'title': title,
        'domain': domain,
        'users': stats['originalUsersCount'],
        'notes': stats['originalNotesCount'],
        'instances': stats['instances'],
        'version': version,
        'software': 'misskey'
    }

    return json_line, uid

def generate_list():
    json_list = []
    all_domains = [line.split('subscription:')[-1] for line in read_redis_keys().split('\n') if line and 'subscription' in line]
    logger.info(f"Total domains from Redis: {len(all_domains)}")

    success_count = 0
    failure_count = 0
    software_counter = Counter()
    interaction_stats = {}

    for domain in all_domains:
        logger.info(f"Processing domain: {domain}")

        headers = {
            'User-Agent': USER_AGENT
        }

        json_line = {'domain': domain, 'status': 'Stats Unavailable'}
        uid = None
        success = False

        for try_function in [try_mastodon, try_misskey, try_nodeinfo]:
            try:
                json_line, uid = try_function(headers, domain, TIMEOUT)
                logger.info(f"Successfully fetched stats for {domain} using {try_function.__name__}")
                success = True
                break
            except Exception as e:
                logger.warning(f"Failed to fetch stats for {domain} using {try_function.__name__}: {str(e)}")

        if success:
            success_count += 1
            software_counter[json_line.get('software', 'Unknown')] += 1

            interaction_count = json_line.get('statuses', 0) or json_line.get('notes', 0) or json_line.get('posts', 0)
            interaction_stats[domain] = interaction_count

            logger.info(f"Instances count for {domain}: {json_line.get('instances', 0)}")
        else:
            failure_count += 1

        if uid and uid in instance_ids:
            logger.info(f"Skipped duplicate domain {domain} with uid {uid}")
            continue

        if uid:
            instance_ids.add(uid)
        json_list.append(json_line)
        logger.info(f"Added {domain} to the list")

    logger.info(f"Total instances processed: {len(json_list)}")
    logger.info(f"Successful instances: {success_count}")
    logger.info(f"Failed instances: {failure_count}")
    logger.info(f"Software distribution: {dict(software_counter)}")

    json_list.sort(key=lambda x: x.get('users', 0), reverse=True)

    stats = {
        "total_instances": len(json_list),
        "successful_instances": success_count,
        "failed_instances": failure_count,
        "software_distribution": dict(software_counter),
        "interaction_stats": interaction_stats
    }

    with open(stats_file, 'w') as f:
        json.dump(stats, f, indent=2)

    return json_list

if __name__ == "__main__":
    logger.info('Started generating member list.')
    sub_list = generate_list()
    with open(outfile, 'w') as f:
        json.dump(sub_list, f, indent=2)
    logger.info('Write new page template done.')

bash脚本

update-list.sh的内容

#!/bin/sh

if [ ! -f /tmp/setup_done ]; then #如果存在安装缓存则跳过,重启容器会重新安装依赖
    apk add python3 py3-pip py3-requests #安装python
    apk add tzdata #修正时区
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
    echo "Asia/Shanghai" > /etc/timezone
    touch /tmp/setup_done
fi

cd /relay/

./gen-member-list.py || exit 1;

exit 0;

修改Dockercompose.yaml

这一步就是映射本地脚本到docker容器中

services:
  redis:
    restart: always
    image: redis:alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
    volumes:
      - "./redisdata:/data"
      - "./relay:/relay"

  worker:
    container_name: worker
    build: .
    image: yukimochi/activity-relay
    working_dir: /var/lib/relay
    restart: always
    init: true
    command: relay worker
    volumes:
      - "./actor.pem:/var/lib/relay/actor.pem"
      - "./config.yml:/var/lib/relay/config.yml"
    depends_on:
      - redis

  server:
    container_name: relay
    build: .
    image: yukimochi/activity-relay
    working_dir: /var/lib/relay
    restart: always
    init: true
    ports:
      - "8080:8080"
    command: relay server
    volumes:
      - "./actor.pem:/var/lib/relay/actor.pem"
      - "./config.yml:/var/lib/relay/config.yml"
    depends_on:
      - redis

然后把上面两个脚本都丢进relay的文件夹

定时任务

使用宝塔或者系统的计划任务
activity-relay-redis-1为redis的容器名,执行bash

docker exec  activity-relay-redis-1  /bin/sh /relay/update-list.sh

演示

https://relay.jiong.us

Docker部署Activity-Relay服务

2024年8月21日 11:50
AI摘要:本文介绍了使用Docker部署Activity-Relay服务的具体步骤。首先,通过git克隆仓库并编辑配置文件。然后,生成actor RSA证书并设置权限。接下来,配置docker-compose文件以开放端口并构建镜像并运行服务。最后,通过反向代理将服务添加到mastodon的中继服务中。完成以上步骤后,即可使用Docker部署Activity-Relay服务。

前言

最初部署的Activity-Relay服务由于是在宿主机上工作,前段时间由于宿主机机房重启了机器 使Relay服务莫名其妙的不能启动

于是这次使用便携易用的Docker来部署服务,记录一下以备用

具体步骤

拉取仓库

git clone https://github.com/yukimochi/Activity-Relay.git -b v2.0.0

编辑配置

进入Activity-Relay目录

cd Activity-Relay
cp config.yml.example config.yml

编辑config.yml修改相关配置

生成actor RSA 证书

ubuntu使用

openssl genrsa -traditional | tee actor.pem

centos使用

openssl genrsa -out actor.pem 1024 | tee actor.pem

赋予权限600

chmod 600 actor.pem

docker-compose配置

这里需开放端口用以反向代理

services:
  redis:
    restart: always
    image: redis:alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
    volumes:
      - "./redisdata:/data"

  worker:
    container_name: worker
    build: .
    image: yukimochi/activity-relay
    working_dir: /var/lib/relay
    restart: always
    init: true
    command: relay worker
    volumes:
      - "./actor.pem:/var/lib/relay/actor.pem"
      - "./config.yml:/var/lib/relay/config.yml"
    depends_on:
      - redis

  server:
    container_name: relay
    build: .
    image: yukimochi/activity-relay
    working_dir: /var/lib/relay
    restart: always
    init: true
    ports:
      - "8080:8080"
    command: relay server
    volumes:
      - "./actor.pem:/var/lib/relay/actor.pem"
      - "./config.yml:/var/lib/relay/config.yml"
    depends_on:
      - redis

构建镜像与运行服务

docker-compose build
docker-compose up -d

查看容器运行状态

docker-compose ps

停止服务

docker-compose down

反向代理

    location = /inbox {
        proxy_pass http://127.0.0.1:8080; 
        proxy_pass_request_headers on; 
        proxy_set_header Host $http_host; 
    }
    location = /actor {
        proxy_pass http://127.0.0.1:8080; 
        proxy_pass_request_headers on; 
        proxy_set_header Host $http_host; 
    }

完成

https://relay.jiong.us/inbox
加入mastodon 的中继服务即可

痛风患者饮食指南

2024年8月13日 09:10
AI摘要:痛风患者饮食应遵循“四避五益”。避免摄入高嘌呤食物、酒类和含酒精饮料、含糖饮料和甜点。而对痛风有益的食物包括蔬菜和水果、咖啡、奶制品、维生素C和多喝水。蔬菜食用越多,血尿酸水平越低;每天饮用约250毫升的咖啡可以降低痛风发生的风险;经常食用奶制品的人痛风发病率下降44%;每天口服500毫克维生素C可降低血尿酸20微摩尔/升;每天尿量超过2000毫升能降低尿液中尿酸浓度和尿酸性肾结石的风险。

最近痛风复发,从网络上找到了一些饮食应当注意的事项

还是锻炼才是王道.

痛风患者饮食应遵循“四避五益”

痛风患者避免摄入这四类食物

  1. 避免高嘌呤食物:生活中我们常接触的高嘌呤食物有动物内脏如肝、肾、脑、肠、舌、胰、脾等;海鲜类如贝类、海鱼、鱼卵、虾蟹、紫菜等;肉汤和肉汁(尤其是火锅汤和老火汤);新鲜蘑菇和豆芽菜等。这些食物痛风患者应避而远之。
  2. 避免酒类和含酒精饮料:不同酒类引起的痛风风险不一样,从大到小依次为:啤酒、陈年黄酒>烈酒>干红葡萄酒。啤酒、黄酒等嘌呤含量较高,喝得越多痛风风险越高。
  3. 避免含糖饮料:研究发现,饮用含糖饮料越多,发生痛风的风险越高。
  4. 避免甜点:甜点中果糖含量较高,会增加尿酸产生;人造奶油制作的甜点所含的反式脂肪酸也会促使痛风发作。另外,甜点热量较高,吃太多易引起肥胖和高胰岛素血症,抑制尿酸的排泄从而使体内尿酸升高。

这五类食物对痛风有益

  1. 蔬菜和水果有益:蔬菜食用越多,血尿酸水平越低。平时可多吃莲藕、茭白、芋头、土豆、淮山、慈姑等根茎类蔬菜。
  2. 咖啡有益:多项研究显示,每天饮用约250毫升的咖啡(包括清咖、奶咖、花式咖啡等)可以降低痛风发生的风险。
  3. 奶制品有益:与不吃奶制品人的相比,经常食用奶制品的人痛风发病率下降44%。但全脂奶脂肪含量较高,可能会提高痛风风险,建议患者多喝低脂奶。
  4. 维生素C有益:每天口服500毫克维生素C可降低血尿酸20微摩尔/升,对预防痛风有显著效果。
  5. 多喝水有益:对痛风患者来说,每天尿量超过2000毫升能降低尿液中尿酸浓度和尿酸性肾结石的风险。患者每日可喝足量的水保证尿量。

Typecho S3存储插件 - S3Upload

2024年7月29日 11:56
AI摘要:这篇文章介绍了一款名为S3Upload的Typecho S3存储插件,该插件是通过AI辅助开发的,目前还不清楚是否存在BUG。之前使用的插件只能上传图片,对其他类型的文件支持不理想,因此作者尝试使用AI开发了这个支持S3兼容的插件。文章还提到了后台设置的截图,并建议只在缤纷云上进行测试。

前言

用AI辅助写的插件,至于有没有什么BUG咱也不知道

鉴于之前用的这个插件只能上传图片,对于其他类型的文件支持不是很理想,


所以想借着AI尝试一下是否可以写一个支持S3兼容的插件

于是有了该插件

预览

后台设置

QQ20240729-115456.png

使用须知

需关闭php的display_errors

仅测试缤纷云,R2
php版本8.3
其他请自测

下载地址: 本地下载
S3下载:S3Upload.zip
项目地址 https://git.ima.cm/jkjoy/S3upload

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

2024年7月28日 15:45
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>

Windows 如何设置动态壁纸

2024年7月18日 13:30
AI摘要:本文介绍了在Windows系统中如何设置动态壁纸的方法。由于Windows本身没有开发动态壁纸功能,但可以通过下载名为Lively Wallpaper的软件来实现。安装好软件后,选择添加壁纸并选择要设置的视频文件,即可在桌面上看到自己的动态壁纸。需要注意的是,壁纸需要在Lively Wallpaper软件打开并在后台运行才能正常使用。另外,不建议在低配电脑上使用动态壁纸,以免影响电脑的流畅度。如果无法打开Microsoft Store,可以尝试使用代理软件或搜索第三方提供的安装包。

由于Windows本身并未开发动态壁纸功能,而许多动态壁纸软件都需要收费,在这里提供一个Win10/11免费将视频文件设置为动态壁纸的方法(操作很简单):

在电脑自带的微软应用商店下载名为Lively Wallpaper的软件,或直接打开https://www.microsoft.com/store/productId/9NTM2QC6QWS7跳转到该应用的微软商店下载页。

安装好软件后打开,选择右上角的“添加壁纸”,然后选择Browse,在弹出的窗口内找到要设置成壁纸的视频,然后在添加页面点“好”,之后就可以在桌面看到自己的动态壁纸啦。

如果壁纸并未成功使用,请检查是否使用了不支持动态壁纸的第三方桌面,或者查看动态壁纸是否在活动状态。另外壁纸需要在Lively Wallpaper软件打开并且在后台(一般不会占用太多内存)情况下才能正常使用。大多数情况下,重启电脑软件会自动开启(也可以手动设置)。

另外,不建议太低配电脑使用动态壁纸,会对电脑使用流畅度有一定影响。

Microsoft Store打不开可能是以下原因:使用或使用过代理软件,Windows升级等功能被关闭...(请参考网上的各种教程)

或者也可以在网上搜索第三方提供的该软件的安装包。

Stack 又一款Typecho 主题

2024年7月16日 08:40
AI摘要:这是一款从Hugo主题`Stack`移植而来的Typecho主题。主题提供了一些自定义选项,包括站点LOGO地址、Favicon地址、归档页面地址、链接页面地址、关于页面地址、自定义菜单等。用户还可以选择是否在侧边栏显示按日期归档,是否使用魔改风格,以及使用第三方评论系统等。此外,用户还可以在Header和Footer中插入自定义代码。项目地址:https://github.com/jkjoy/Typecho-Theme-Stack。

前言

从Hugo主题Stack移植而来.

https://github.com/CaiJimmy/hugo-theme-stack

演示

https://wanne.cn

使用

站点 LOGO 地址

为左侧边栏头像

站点 Favicon 地址

Favicon

归档页面地址

创建归档页面后,在此填入

链接页面地址

使用links插件

创建链接页面后,在此填入

关于页面地址

创建关于页面后,在此填入

自定义菜单

        <li >
            <a href='/' >
                <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-home" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z"/><polyline points="5 12 3 12 12 3 21 12 19 12" /><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" /><path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" /></svg>
                <span>首页</span>
            </a>
        </li>

按照此格式填入

是否在侧边栏显示按日期归档

由于日期归档过多,可以选择是否显示

是否使用魔改风格

mod风格来自其他Stack用户

分类图片目录

按照分类的 mid 以jpg的格式 存放的目录
譬如本地目录 或者 CDN 等,用于匹配归档页面的分类图片

使用第三方评论

可以选择使用第三方的评论系统 如 twikoo 等

Header代码

用于DIY CSS 或 身份验证 等

Footer代码

用于插入备案号码 或者 统计代码等

项目地址

https://github.com/jkjoy/Typecho-Theme-Stack

给Typecho添加 有评论通知到QQ 的功能

2024年7月3日 19:25
AI摘要:该文章介绍了如何给Typecho添加评论通知到QQ的功能。作者使用了基于GO-CQhttp项目的方法来实现。在使用过程中,需要修改主题的`functions.php`文件,并在其中添加相应的代码。代码中包括了获取配置中的QQ机器人API地址和QQ号码,并构建消息内容。最后通过Curl请求将消息发送给QQ机器人。完成设置后,可以在主题设置处设置QQ机器人的API和QQ号码。

简介

前文有

个人看IM的时间比邮件的时间长,所以就还是使用QQ接收通知
基于 GO-CQhttp 项目
也可以使用插件实现

使用

修改主题 functions.phpfunction themeConfig($form) {下添加

    $qqboturl = new Typecho_Widget_Helper_Form_Element_Text('qqboturl', NULL, 'https://bot.asbid.cn', _t('QQ机器人API,保持默认则需添加 2280858259 为好友'), _t('基于cqhttp,有评论时QQ通知'));
    $form->addInput($qqboturl);
    $qqnum = new Typecho_Widget_Helper_Form_Element_Text('qqnum', NULL, '80116747', _t('QQ号码'), _t('用于接收QQ通知的号码'));
    $form->addInput($qqnum);

最后添加以下代码

function notifyQQBot($comment) {
    $options = Helper::options();
    // 检查评论是否已经审核通过
    if ($comment->status != "approved") {
        error_log('Comment is not approved.');
        return;
    } 
    // 获取配置中的QQ机器人API地址
    $cq_url = $options->qqboturl;
    // 检查API地址是否为空
    if (empty($cq_url)) {
        error_log('QQ Bot URL is empty. Using default URL.');
        $cq_url = 'https://bot.asbid.cn';
    }
    // 获取QQ号码
    $qqnum = $options->qqnum;
    // 检查QQ号码是否为空
    if (empty($qqnum)) {
        error_log('QQ number is empty.');
        return;
    }
    // 如果是管理员自己发的评论则不发送通知
    if ($comment->authorId === $comment->ownerId) {
        error_log('This comment is by the post owner.');
        return;
    }
    // 构建消息内容
    $msg = '「' . $comment->author . '」在文章《' . $comment->title . '》中发表了评论!';
    $msg .= "\n评论内容:\n{$comment->text}\n永久链接地址:{$comment->permalink}";
    // 准备发送消息的数据
    $_message_data_ = [
        'user_id' => (int) trim($qqnum),
        'message' => str_replace(["\r\n", "\r", "\n"], "\r\n", htmlspecialchars_decode(strip_tags($msg)))
    ];
    // 输出调试信息
    error_log('Sending message to QQ Bot: ' . print_r($_message_data_, true));
    // 初始化Curl请求
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => "{$cq_url}/send_msg?" . http_build_query($_message_data_, '', '&'),
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => false,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => 0
    ]);
    $response = curl_exec($ch);
    if (curl_errno($ch)) {
        error_log('Curl error: ' . curl_error($ch));
    } else {
        error_log('Response: ' . $response);
    }
    curl_close($ch);
}
Typecho_Plugin::factory('Widget_Feedback')->finishComment = 'notifyQQBot';

完成之后在主题设置处设置QQ机器人的API 和 QQ号码

Space 又一个typecho主题

2024年6月13日 19:55
AI摘要:Space是一个移植自Gridea主题的Typecho主题。它增加了友情链接页面,并需要与插件"links"配合使用。该主题的预览图可以在文章中找到,演示地址为https://space.imsun.org/,下载地址为https://github.com/jkjoy/typecho-theme-space。

说明

移植自 Gridea 主题 Space

增加友情链接页面 需要配合插件links使用

预览

预览图

演示

https://space.imsun.org/

下载地址

https://github.com/jkjoy/typecho-theme-space

Smartisan 又一款Typecho主题

2024年6月1日 15:27
AI摘要:本文介绍了一款Smartisan的Typecho主题,该主题是从Gridea主题移植而来。作者提供了原项目的GitHub链接,并附上了预览截图。然而,暂时没有提供演示地址。

感谢

这次移植的是一款Gridea主题
原项目
https://github.com/fullstack-kingj/gridea-theme-smartisan

预览

screenshot.png

演示地址

https://blog.wenxs.cn/

下载

https://github.com/jkjoy/typecho-themes-Smartisan

❌
❌