星期日, 四月 26, 2026

195 Telegram 的极简网页版 mpgram-web

2026 年 4 月, 机场经历了巨大的动荡, 大量中转机场的国内中转出口被清退、通报, 这里我吐槽那些还在用 SS、Trojan 协议的 SB 机场, 我认为这些能被识别的过时协议和国内出口清退通报有很大关联, 而且从中转迁移到直连还在用这些 SB 协议的 SB 机场, 后续会继续祸害直连. 

为了防止可能到来的 2026 年 5 月或者 6 月直连断网, 以及之后可能彻底和 Telegram 失去联系, 我做了个最终应急方案, esim 海外流量套餐 + 省流量的极简 Telegram 网页版 mpgram-web, 手机和电脑都可以使用. 

提一嘴 esim, 卡是骗多多上卖的二十几块的白卡, LPA(写入工具)走大部分安卓自带的 OMAPI 协议, 安卓上用闭源 Nekoko2(可刷 magisk 给 apk 提权成系统应用, 以支持 telephony api, 此软件功能多些) 或者开源 EasyEUICC (https://easyeuicc.org https://gitea.angry.im/PeterCxy/OpenEUICC/releases 这两个是同一个东西), 这两 LPA 只写卡, 二十几块的白卡自带 stk (SIM 卡工具箱) 切卡删卡功能. 流量买 eskimo 的, 走 aff 新注册有 500MB Global, 随后购买 10GB China 折扣成半价 $13.5, 再购买一次折扣成 $1 的 1GB China, 这是两个券的最优用法, 写卡连上信号后有效期两年. 如果你没见到券或者不是这个价格, 不要购买, 买最低的保卡就行. 

一些入门教程: 

https://www.leavesongs.com/THINK/using-euicc-card-to-support-esim.html

https://euicc-manual.osmocom.org/

https://iecho.cc/posts/convert-esim-to-physical-sim

https://www.muooy.com/417.html


下面是正文.  

项目地址: https://github.com/shinovon/mpgram-web

大概就是 3GQQ 那样, 这是纯网页版, 作者还有一个 j2me 的客户端, 你可以打开 api 功能, 和客户端 (https://github.com/shinovon/mpgram-client)一起使用, 也可以单使用网页版. 


 
图片来自 http://nokiahacking.pl/mpgram-web-telegram-w-kazdej-przegladarce-vt35348.htm

没错, 你没看错, 基于 java 的 j2me 的客户端, 也就是二十年前功能机使用的 jar, 截至发文的 20260426, 依旧在维护更新. 此作者似乎是 j2me 的忠实粉丝, 还接手了早已遁入虚空的 kemulator 项目继续维护 (https://github.com/shinovon/KEmulator/releases)...

本文讲的是自建 mpgram-web, 而不是使用公共服务器, 一是之前有一定数量的封号(All public instances are closed because many people getting their accounts banned by telegram flood prevention system), 二是自己的账号用别人的服务器也不安全. 

这套东西的文档做得其实很烂, 踩了很多坑, 下面是自己倒腾出来的经验. 

1, 安装 docker 并查看 docker compose 的版本号: 

curl -fsSL https://get.docker.com | bash -s docker
docker --version
docker compose version

我本来想自己编译个命令行的版本, 但折腾了几个小时都失败了, 于是就用作者带的 docker-compose.yml 了. 

2, 安装好 docker 之后, 按照教程所说, 编辑 api_values.php 和 config.php 两个文件, 如果你没有申请过 Telegram apps (api_id 和 api_hash), 那么需要打开 https://my.telegram.org/apps 来申请. 将其填入 api_values.php, 下面讲解 config.php 主要的配置及含义. 

define('sessionspath', 'session_dir/'); //和 locale img 这些目录平级, 设置一个保存登录数据的路径
define('PNG_STICKERS', true); //透明贴纸, 用不上
define('FORCE_HTTPS', false); // http 重定向到 https, 有 traefik 等反向代理, 用不上, 127.0.0.1 反代就行
define('CHROME_HTTPS', false); //
当识别到 chrome user-agent 请求头的时候, http 重定向到 https, 用不上, 同上, 走反代
define('CONVERT_VOICE_MESSAGES', false);
define('VOICE_TMP_DIR', sys_get_temp_dir().'/mp/');
define('FFMPEG_DIR', '');  //这三项和语音有关, 需要下载 ffmpeg 可执行文件, 平时我根本用不到语音, 所以跳过了
define('LOGIN_CAPTCHA', false); //登录时输入验证码, 在执行网页登录的时候, 多一步输入验证码的操作, 但我实际测试发现设置为 0 验证码照样出现... 
define('INSTANCE_PASSWORD', 'admin_password'); //登录时输入密码, 如果你的自建 mpgram-web 暴露在公网, 设置这个以防被他人滥用
define('FILE_REWRITE', false); //获取图片等文件时使用目录, 用不上, 我看默认都是 file.php?abc=def 这样请求图片或文件
define('CONVERT_TGS_STICKERS', false);
define('LOTTIE_DIR', '');
define('TGS_TMP_DIR', sys_get_temp_dir().'/mp/');
define('LOTTIE_TO_GIF', true); //和贴纸有关, 用不上, 没开. 这东西需要引入其它 GitHub 项目, 懒得折腾了
define('ENABLE_API', false);
define('ENABLE_LOGIN_API', false); //没折腾这个, 应该是开放给 j2me 客户端 mpgram-client 来使用的
define('DOWNLOAD_SIZE_LIMIT', 1024 * 1024 * 1024);
define('IMAGE_SIZE_LIMIT', 30 * 1024 * 1024);
define('UPLOAD_SIZE_LIMIT', 20 * 1024 * 1024); //限制 file.php 最大获取的大小, 保持默认了
define('ENABLE_PEER_DB', false); //设置为 false 可 
sessionspath 中的登录数据文件大小, 但无法显示好友名称, 会显示 deleted account, 如果你的号聊天特别多, 建议关闭成 false 状态, 不然 sessionspath 底下的 safe.php 文件可以来到 170MB+ 的大小, 这会造成 vendor 也就是 MadelineProto 会抛出 Amp\TimeoutException: Timeout while waiting for help.getConfig 超时报错的异常

3, 改好这两个文件之后, 开始使用 git clone 命令下载源码: 

git clone https://github.com/shinovon/mpgram-web.git

4, 随后进入项目的 docker 文件夹并执行一键编译:  

cd docker

docker compose up --build -d

5, 编译完成之后, 就可以把这几个 container 和 image 都删了, 我用的 lazydocker 在图形界面终端操作的, 如果需要命令删除自己查一下删除 container 和 image 的命令. 

git clone 的目录不要删, 留下, 因为刚才它在 compose 编译的时候引入了另一个项目 MadelineProto 放在了 vendor 目录下, 并且对这个引入的项目打了文件补丁, 所以整个克隆的目录不要动. 

6, php-cli 的 docker, 和 traefik (反向代理)

php-cli 有一些依赖要启用, 所以我重做了 dockerfile, 整合了 mpgram-web 所需的一些东西, 重新编译出一个 image 了. 具体操作如下: 

新建一个 dockerfile 空文件, 里面写入: 

FROM php:8.5.5-cli

# =========================
# 系统依赖
# =========================
RUN apt-get update && apt-get install -y --no-install-recommends \
        libyaml-dev \
        libpng-dev \
        libjpeg-dev \
        libfreetype6-dev \
        libgmp-dev \
        libffi-dev \
        libxml2-dev \
        libonig-dev \
        curl \
        procps \
        vim \
        ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# =========================
# GD 扩展配置
# =========================
RUN docker-php-ext-configure gd \
        --with-freetype \
        --with-jpeg

# =========================
# PHP 内置扩展
# =========================
RUN docker-php-ext-install -j$(nproc) \
        gd \
        mbstring \
        ffi \
        gmp \
        xml \
        fileinfo

# =========================
# PECL yaml 扩展, 这段可以不要, mpgram-web 用不到
# =========================
RUN pecl install yaml \
    && docker-php-ext-enable yaml

# =========================
# Browscap, 这是 mpgram-web 里的功能
# =========================
RUN curl -fsSL \
    http://browscap.org/stream?q=Lite_PHP_BrowsCapINI \
    -o /usr/local/etc/browscap.ini

# =========================
# 自定义 php.ini, 
mpgram-web 自带的 php.ini, 我以 conf.d 的形式加载了
# =========================
RUN curl -fsSL \
    https://raw.githubusercontent.com/shinovon/mpgram-web/refs/heads/master/docker/mpgram_web/php.ini \
    -o /usr/local/etc/php/conf.d/90-php.ini 

7, 随后使用命令编译成 image, 注意末尾有个小数点:

docker build --no-cache -t php-cli:8.5.5 .

再清理编译缓存: 

docker builder prune

最后运行, 注意指定正确的路径, 我是 /root/www: 

docker run -d -p 127.0.0.1:9000:80 --name php-cli --restart=unless-stopped -v /root/www:/var/www/html php-cli:8.5.5 php -S 0.0.0.0:80 -t /var/www/html

8, traefik 的配置, 之前自己写的 index.yaml 配置, 放在 config 文件夹下面自动加载的那种, 访问 mpgram-web 的时候总是卡住很长时间, 我判断是 http 请求被 Traefik 强行重定向到 https 了(127.0.0.1 内部), 所以我就让 ChatGPT 重写了一版, 以下是 index.yaml: 

http:
  routers:

    # =========================
    # ① HTTP → HTTPS 重定向
    # =========================
    us-http:
      entryPoints:
        - "http"
      rule: "Host(`你的网站域名, 如abc.com`)"
      middlewares:
        - redirect-https
      service: noop

    # =========================
    # ② HTTPS 正式入口
    # =========================
    us-https:
      entryPoints:
        - "https"
      rule: "Host(`你的网站域名, 如abc.com`) && PathPrefix(`/`)"
      tls:
        certResolver: "letsencrypt"
      service: us
      middlewares:
        - adrules-cors

  services:
    us:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:9000" #这里是 php-cli 开放的 9000 端口

    # 必须存在的“虚拟 service”(用于 redirect)
    noop:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1"

  middlewares:

    # =========================
    # HTTPS 强制跳转
    # =========================
    redirect-https:
      redirectScheme:
        scheme: https
        permanent: true

    # =========================
    # CORS
    # =========================
    adrules-cors:
      headers:
        accessControlAllowOriginList:
          - "*"
        accessControlAllowHeaders:
          - "*"

以上是 Traefik 的 php-cli 反代配置, 如果你用 nginx 或者 apache 都可以参考. 

至于你的 mpgram-web, 建议放在一个含有随机字符的路径, 如 abc_4138u90i9tqwkrioew3894A, 这样就能防止爬虫遍历目录, 这种东西漏洞应该不少. 暴露在公网的服务器可以访问: https://abc.com/abc_4138u90i9tqwkrioew3894A/ 


这套源码跑起来是没问题, 号码登录可能不行, 使用二维码登录. 收发消息就靠刷新网页, 完完全全就是当年 3GQQ 的体验, 而且我在火狐上都没有自动刷新这一说. 

不过有一个很大的问题, 它是和进程交互的, 当一段时间没有访问后, 再去唤醒进程就会超时一次, 等待时间非常长, 可能达三~五分钟之久, 所以我想到了一个保活方法, 就是定时访问自身来保活, 也就是在 crontab -e 中添加任务. 

首先你要保证浏览器正常登录过一次, 后端才能在 sessionspath 中保存数据, 可以看到文件夹类似: 

qr_942cbc3ef5..........e5bfec.madeline

把 .madeline 之前的文件夹名复制出来. qr_942cbc3ef5..........e5bfec

构造请求. 在 crontab -e 中添加: 

*/5 * * * * /usr/bin/curl 'https://你的域名/abc_4138u90i9tqwkrioew3894A你的mpgram-web路径/chats.php?lang=en&timeoff=-18000&theme=6主题ID自己试去,或者在网页设置好去cookies里找&updint=10&user=qr_942cbc3ef5..........e5bfec你刚才复制的文件夹名' -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) Gecko/20100101 Firefox/150.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Connection: keep-alive'

如果有多个需要保活, 就添加多行. 如果需要直接编辑 crontab 文件, 去 /var/spool/cron/crontabs 编辑文件. 


至此, 一整套 mpgram-web 就搭好了, 你可以直接使用上面 crontab -e 里的链接访问(前提是必须在此浏览器登录过, 后端存有 session, 浏览器存有 cookies 才行), 哈哈, 是不是有 3GQQ sid 的感觉了? 也不用怕长时间不用下次唤醒超时报错了. 用起来其实还是挺麻烦的, 但这屎山折腾好了以后就会非常舒服, 你甚至还可以使用作者写的 j2me 客户端 mpgram-client 在安卓 j2me loader 模拟器, 或电脑的 kemulator 模拟器中使用. 

星期五, 四月 17, 2026

194 bitwarden 转 vaultwarden 自建后, creationDate 和 revisionDate 不能写入数据库的 created_at 和 updated_at 问题处理

bitwarden 简称 bw, vaultwarden 简称 vw

思路:  

1, 先将 bw 的数据导出成 bw.json. 

2, 将 bw.json 导入你自建的 vw. 

3,  将你自建的 vw 也导出一个 vw.json

4, 把 vaultwarden 的 docker / 主程序 停止, 启动, 再停止, 以保证 sqlite wal 全部写入数据库 db.sqlite3 文件.  

5, 可以把 db.sqlite3 文件下载到本地, 用 navicat 或者 dbeaver 等工具打开, 这时你看到 vw 的 sqlite 数据中,  created_at updated_at 全部变成了清一色导入的时间, 奇葩行为..

5, 使用 ai 生成代码, 寻找两个 json 中 id 的对应关系, id 是不同的, 如何一一对应呢?
我使用的是 name + username 的组合对应关系. 除此之外, 当没有 username 的时候(银行卡类型), 用 name + notes 来组合, 以防止重复的情况. 

也就是: 

两个不同的数据库 json 导出文件内容相同, id 不同, bw.json 的两个时间 creationDate 和 revisionDate 是对的, vw.json 中的两个时间是错的. 通过 name+username (没有 username 就用 name+notes) 来找到两个 json 的 "id" 对应关系. 

随后根据这个 id 的对应关系, 将 bw.json 中的 creationDate 和 revisionDate 读取出来, 写到数据库对应 id 中的 created_at 和 updated_at 中去. 

6, 我使用 ChatGPT 生成的代码如下: 

<?php

$bwFile = 'bw.json';
$vwFile = 'vw.json';
$dbFile = 'db.sqlite3';

$bwJson = json_decode(file_get_contents($bwFile), true);
$vwJson = json_decode(file_get_contents($vwFile), true);

if (!$bwJson || !$vwJson) {
    die("JSON 解析失败\n");
}

$bwItems = $bwJson['items'] ?? [];
$vwItems = $vwJson['items'] ?? [];

/**
 * key = name + username/notes
 */
function buildKey($item) {
    $name = $item['name'] ?? '';

    $username = $item['login']['username'] ?? null;
    $notes = $item['notes'] ?? '';

    $second = $username;
    if ($second === null || $second === '') {
        $second = $notes;
    }

    return $name . '|' . $second;
}

/**
 * SQLite connect
 */
$pdo = new PDO("sqlite:" . $dbFile);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

/**
 * =======================
 * 1. BW index
 * =======================
 */
$bwMap = [];

foreach ($bwItems as $item) {
    if (!isset($item['name'])) continue;

    $key = buildKey($item);

    $bwMap[$key] = [
        'creationDate' => $item['creationDate'] ?? null,
        'revisionDate' => $item['revisionDate'] ?? null,
    ];
}

/**
 * =======================
 * 2. Update DB
 * =======================
 */
$updateStmt = $pdo->prepare("
    UPDATE ciphers
    SET created_at = :created_at,
        updated_at = :updated_at
    WHERE uuid = :uuid
");

$updated = 0;
$skipped = 0;
$skippedList = [];

foreach ($vwItems as $item) {

    if (!isset($item['name'], $item['id'])) {
        $skipped++;
        $skippedList[] = [
            'reason' => 'missing name or id',
            'item' => $item
        ];
        continue;
    }

    $key = buildKey($item);

    if (!isset($bwMap[$key])) {
        $skipped++;
        $skippedList[] = [
            'reason' => 'bw not found',
            'id' => $item['id'],
            'key' => $key
        ];
        continue;
    }

    $bw = $bwMap[$key];

    if (empty($bw['creationDate']) || empty($bw['revisionDate'])) {
        $skipped++;
        $skippedList[] = [
            'reason' => 'missing bw dates',
            'id' => $item['id'],
            'key' => $key,
            'bw' => $bw
        ];
        continue;
    }

    try {
        $updateStmt->execute([
            ':created_at' => $bw['creationDate'],
            ':updated_at' => $bw['revisionDate'],
            ':uuid' => $item['id'],
        ]);

        $updated++;

    } catch (Exception $e) {
        $skipped++;
        $skippedList[] = [
            'reason' => 'sql error',
            'id' => $item['id'],
            'error' => $e->getMessage()
        ];
    }
}

/**
 * =======================
 * 3. Output
 * =======================
 */
echo "UPDATED: {$updated}\n";
echo "SKIPPED: {$skipped}\n";

echo "\n=== SKIPPED DETAILS ===\n";

if (empty($skippedList)) {
    echo "NONE\n";
} else {
    foreach ($skippedList as $s) {
        echo json_encode($s, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
    }


运行代码之前, 要把 php.ini 中的两个插件启用, 也就是把这两行前面的分号去掉, 然后重启 php: 

extension=pdo_sqlite
extension=sqlite3 

如果你的 extensions 里面甚至都没有这两个 dll, 那么改 php.ini 也没有用了, 推荐使用类似 wamp 的一键端, 它有很多名字: uniform server, unicontroller, miniserver, 都是同一个软件. 内含 apache mysql php, 只启用 php 就行. 链接: https://sourceforge.net/projects/miniserver/


最终输出如下: 

UPDATED: 成功的数量 SKIPPED: 1 === SKIPPED DETAILS === { "reason": "bw not found", "id": "这里是uuid序号", "key": "这里是json里的name字段|这里是json里的username字段" }  

没找到就说明 bw 里面没有, 这是你自建了以后新增的记录. 

再去看看数据库里面的 created_at 和 updated_at 已经写好了, 但是格式不对, 简单替换下, sql 执行: 

UPDATE ciphers
SET
  created_at = REPLACE(REPLACE(created_at, 'T', ' '), 'Z', ''),
  updated_at = REPLACE(REPLACE(updated_at, 'T', ' '), 'Z', '');


* 如果你需要 备份/同步 数据库, 如使用 mega sync 同步 .db 文件而不同步 wal, 那需要在 crontab -e 中加上: 

0 5 * * * /usr/bin/docker restart vaultwarden

来保证数据库 wal 能自动写入 .db, 而触发同步. 

星期二, 二月 24, 2026

193 docker 迁移目录和数据 包括 docker 和 containerd | 解决迁移之后依旧存储在原目录的问题

首先来讲 docker 和 containerd, docker 是个第三方软件, 它依赖于系统的 containerd, 类似于早些年的套壳 ie 浏览器, ie 坏了, 套壳浏览器也会打不开. 这两块的数据也是分开的, 也就是按照很多网上教程设置完 docker 的 json 之后, docker pull 依旧被拉取到了原目录的根本原因. 

docker 配置路径文件:  /etc/docker/daemon.json

docker 默认数据目录: /var/lib/docker/

containerd 配置路径文件: /etc/containerd/config.toml

containerd 默认数据目录: /var/lib/containerd 

所以这两个配置文件, json 和 toml 要配置好, 再把数据目录复制或移动走, docker 才能完整地运行起来


之前干了件蠢事, 由于小鸡只给了 10GB 的固态硬盘空间, 所以我把 docker 迁移到机械盘了, 机械盘很大, 有 1000GB, 但迁移过去后悔了, 在机器重启之后, 从 docker 服务开始启动到正常运行, 花了至少五分钟, 这还是只有三个容器的情况下, 如果再跑大型的 浏览器微信 qq 客户端 docker 那得花十分钟以上, 重启容器也要花费大量时间, 后来 也不需要挂着微信和 qq 了, 就打算迁移回来. 10GB 完全够用. 


现在, docker 是运行中的状态. 目前的状态: 

docker 数据: /mnt/hdd/root/docker 

containerd 数据: /mnt/hdd/root/docker/containerd

我想要迁移回 /var/lib/ 也就是系统默认的地方去. 

从这一步开始, 我详细讲解 docker 带着数据迁移的方法. 大家如果是从默认目录迁移至别处, 注意目录的颜色, 蓝色字体是 原目录, 红色字体是 目标目录, 要迁移到的地方. 

1, 确认目录位置:

docker info | grep "Docker Root Dir"

2, 停止 service 和 socket

systemctl stop docker.socket
systemctl stop docker.service
service containerd stop

3, 复制 docker 文件, 这里排除了 containerd, 因为我放到一起了: 

rsync -a \
 --exclude='containerd' \
 /mnt/hdd/root/docker/ \
 /var/lib/docker/

复制 containerd 中的文件, 由于 containerd 文件极多, rsync 效率会很低, 使用内存中转的方式在后台运行, 这种操作不会在磁盘上生成多余文件, 注意 tar -xf - -C 后面跟的是上级目录, 如果目标已存在不用的 containerd 记得删除: 

nohup bash -c 'cd /mnt/hdd/root/docker && tar -cf - containerd | tar -xf - -C /var/lib' > /root/containerd.log 2>&1 &

再使用命令对比目录大小: 

nohup bash -c 'du -sh /mnt/hdd/root/docker/containerd/ > /root/mnt.txt 2>&1' &
du -sh /var/lib/containerd/

4, 修改 docker 配置文件: 

echo '{"data-root": "/var/lib/docker"}' | sudo tee /etc/docker/daemon.json
cat /etc/docker/daemon.json

5, 修改 containerd 配置文件, 打开 /etc/containerd 文件夹, 修改 config.toml: 

root = "/var/lib/containerd"

再看一眼是否改好: 

cat /etc/containerd/config.toml

6, 先启动 containerd 再启动 docker

service restart containerd
service restart docker

7, 删除旧的 docker 文件夹和 containerd 文件夹.  

 

如果你在启动 docker 之后, 看不到 images, 看到 containers 变成了类似 md5 的名字, 那就是 containerd 的目录没设置对, 检查 toml 配置文件和实际存放的位置是不是一致, 如果你用 tar -xf - -C 一定要注意后面跟上级目录. 不然会变成 /var/lib/containerd/containerd, 变成这样也没关系, 移动一下就行. 

我没有使用 docker compose, 如果你使用它, 迁移方法可能会有不同.