悪霊にさいなまれる世界 -The Demon-Haunted World

悪霊にさいなまれる世界 -The Demon-Haunted World

30代独身機械系技術者が雑記/セキュリティ/資産運用なんかを書きます/PGP・GPG Key:BC884A1C8202081B19A7A9B8AB8B419682B02FF8

RaspberryPiを使ってBlueskyに切り取り線を投稿するPythonのスクリプトを作った

Twitterにあって便利だったbotはできるだけBlueskyに作っておこうという試みである。そう言うわけでネタ元はこれ。1時間おきに切り取り線を呟いてくれるbot、結構便利であるのでBlueskyに作っておく。

https://x.com/kiri_tory

とは言えBlueskyはそんなにタイムラインの流れが速くないので、1時間おきに線を引く必要も無かろう、と思って6時間おきでポストするようにした。完成品がこちら。

bsky.app人によってはそれでも1時間おきがいいとか要望があるかもしれないので欲しい人が適当に作って欲しい。そう言う訳でスクリプトを公開しておく。適宜弄って流用して結構であるが、多分質問とかには答えないし無保証である。

まず基本事項は以下の通り。

  1. PythonのatprotoでBlueskyに書き込む。スクリプトはsystemdに登録して常時起動させておく。
  2. ファイルはkiritori.py(本体)、kiritori.service(systemd用)、kiritori_auth.json(ログイン情報が入っている設定ファイル)、kiritori.log(ログファイル)からなる。
  3. RaspberryPi OSはpipが使えないのでvenvで仮想環境を作る。仮想環境のディレクトリは/home/pi/bluesky/とする。このフォルダに前記のファイル(kiritori.service)以外を保存する。

詳細な解説はしない。これを読むような人は自力でなんとかできると信じてる。と言うか面倒臭い。サクサクとディレクトリと仮想環境を作り、atprotoモジュールをインストールする。足りないものがあればエラーを見て追加すれば良い。

mkdir bluesky
python3 -m venv bluesky
source bluesky/bin/activate
pip install atproto

本体のスクリプト:kiritori.pyはこれ。もったいぶらずドンと貼る

import sys
import os
import time
import json
import logging
from atproto import Client, models
import argparse
from datetime import datetime, timedelta

# ログの設定
log_file = '/home/pi/bluesky/kiritori.log'
logging.basicConfig(
    filename=log_file,
    level=logging.INFO,  # INFOレベルでログを記録(DEBUGレベルから変更)
    format='%(asctime)s - %(levelname)s - %(message)s'
)

# コマンドライン引数の設定
def parse_args():
    parser = argparse.ArgumentParser(description="Bluesky Kiritori Sen")
    parser.add_argument('--debug', action='store_true', help="デバッグモードでデバッグ書き込み")
    return parser.parse_args()

# 引数を解析
args = parse_args()

# 仮想環境のパスをsys.pathに追加
venv_path = '/home/pi/bluesky'
sys.path.insert(0, os.path.join(venv_path, 'lib', 'python3.11', 'site-packages'))

# 設定ファイルのパス
CONFIG_FILE_PATH = '/home/pi/bluesky/kiritori_auth.json'

# 設定ファイルを読み込む関数
def load_config():
    try:
        with open(CONFIG_FILE_PATH, 'r', encoding='utf-8') as file:
            return json.load(file)
    except FileNotFoundError:
        logging.error("設定ファイルが見つかりません。")
        return None
    except json.JSONDecodeError:
        logging.error("設定ファイルの形式にエラーがあります。")
        return None

# 設定を読み込み
config = load_config()

if config:
    username = config.get('username')
    password = config.get('password')
    logging.info("ユーザー名とパスワードを正常に読み込みました。")
else:
    logging.error("設定の読み込みに失敗しました。")

# 日本語の曜日を手動で定義
weekdays_ja = ['月', '火', '水', '木', '金', '土', '日']

# Blueskyに投稿
def post_to_bluesky(client):
    logging.info("Blueskyへの投稿を開始します。")
    try:
        # 現在の時刻を取得
        now = datetime.now()

        # 0時の場合
        if now.hour == 0:
            # 日付と日本語曜日を表示
            weekday_ja = weekdays_ja[now.weekday()]  # 日本語の曜日を取得
            post_text = f"✄-------{now.strftime('%Y-%m-%d')} ({weekday_ja})------✄"
        else:
            # それ以外の時刻(通常は時:分の形式)
            post_text = f"✄-------{now.strftime('%H:%M')}------✄"

        client.send_post(post_text)
        logging.info(f"投稿成功: {post_text}")

    except Exception as e:
        logging.error(f"Blueskyへの投稿中にエラーが発生しました: {e}")

# 次の投稿時刻まで待機する関数
def wait_until_next_run():
    # 現在の時刻を取得
    now = datetime.now()

    # 0時、6時、12時、18時のいずれかの時刻までの秒数を計算
    next_run_time = None
    for hour in [0, 6, 12, 18]:
        if now.hour < hour:
            next_run_time = datetime(now.year, now.month, now.day, hour, 0, 0)
            break
    if next_run_time is None:
        # 今日の18時を過ぎている場合、次は明日の0時
        next_run_time = datetime(now.year, now.month, now.day, 0, 0, 0) + timedelta(days=1)

    # 次回実行までの待機時間(秒)
    time_to_wait = (next_run_time - now).total_seconds()

    logging.info(f"次回の実行まで {time_to_wait / 60:.2f} 分です。")
    time.sleep(time_to_wait)

# メイン処理
if __name__ == "__main__":
    # Clientのインスタンスを1回だけ作成
    client = Client()
    client.login(username, password)  # ログイン

    if args.debug:
        logging.info("デバッグモード: 即時チェックを実行します。")
        post_to_bluesky(client)
    else:
        logging.info("スケジュールモード: 0時、6時、12時、18時に更新チェックを実行します。")
        while True:
            post_to_bluesky(client)
            wait_until_next_run()  # 次回の実行まで指定された時刻まで待機

次は設定ファイルのkiritori_auth.json。アカウント名とBlueskyの「プライバシーとセキュリティ」から作れるアプリパスワードを保存するので、スクリプト本体とは別に作り、chomodでパーミッションを600にして所有者のみ読み書き専用にする。

{
    "username": "kiri-tori.bsky.social",
    "password": "****-****-****-****"
}

作るファイルとしては次で最後、kiritori.serviceであるが、これは/etc/systemd/system/に作る。

[Unit]
Description=Kiritorisen Service
After=network.target

[Service]
ExecStart=/home/pi/bluesky/bin/python /home/pi/bluesky/kiritori.py
WorkingDirectory=/home/pi
StandardOutput=journal
StandardError=journal
Restart=on-failure
User=pi
Group=pi
Environment="PATH=/home/pi/bluesky/bin:$PATH"

[Install]
WantedBy=multi-user.target

これでよし。kiritori.logはない場合の動作が特に定義してないのでエラーが出るかもしれない。不安なら

touch /home/pi/bluesky/kiritori.log

で作っておけばよい。

動作確認はデバッグモードがある。仮想環境で動かすことを忘れないように。

source /home/pi/bluesky/bin/activate
python3 /home/pi/bluesky/kiritori.py --debug

とオプションを付けて実行すると現在の時刻が書き込まれるはずである。設定ファイルが正しく設定されるか等の確認に使える。仮想環境から抜けるときは

deactivate

で抜けられる。あとは普通にサービスに登録すれば良い。

sudo systemctl daemon-reload
sudo systemctl enable kiritori.service
sudo systemctl start kiritori.service
sudo systemctl status kiritori.service

これで完了。問題が生じたらログファイルを読むなどして適当に対応してほしい。

RaspberryPi 3Bにdockerでnginxをインストールして良い感じのdocker-compose.ymlを作った

エンジンつながりで適当なエンジン画像。初見ではエンジンエックスなんて絶対読めない。wkpdより https://commons.wikimedia.org/wiki/File:Nissan_RB26DETT_Engine_-_Front_Side.jpg

ほとんどタイトルオンリー。普通に

docker pull nginx

でプルしたのはいいが、検索したところ丁度良いdocker-compose.ymlが無かったので自作した。引っかかったところも含めてもメモしておく。

/home/pi以下に/nginxディレクトリを作り、その中でさらにボリュームとしてマウントするconfigディレクトリとhtmlディレクトリを作る。

sudo mkdir nginx
cd nginx
sudo mkdir config
sudo mkdir html

nginxディレクトリにいれるdocker-compose.ymlは以下の通り。ログが溜まりすぎないように最大10MBで3ファイルのローテートするように設定した。

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    ports:
      - "80:80"
    restart: always
    volumes:
      - /home/pi/nginx/html:/usr/share/nginx/html
      - /home/pi/nginx/config:/etc/nginx
    logging:
      driver: "json-file" # defaults if not specified
      options:
        max-size: "10m"
        max-file: "3"

なお/nginx/config/ディレクトリにnginx.confファイルがないとエラーが出る(ここでハマった)ので最低限度作ってやる。RaspberryPi3Bを想定しているのでwoker_processesはコア数と同じ4にした。

worker_processes 4;

events {
    worker_connections 1024;
}

http {
    server {
		# 80番ポート(http)でリクエストを受け付ける
    listen       80;
		# ドメイン名の指定
    server_name  localhost;

    location / {
        # リクエストされた際にドキュメントがある場所を指定
        root   /usr/share/nginx/html;
        # リクエストを受けた際に提供するファイル名
        index  index.html index.htm;
    }

    # エラーコードが発生した際に表示するURIの指定
    error_page   500 502 503 504  /50x.html;
    # "/50x.html"ページへ内部リダイレクト
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
	}
}

以上。

RaspberryPi 3BにdockerでRadicaleをインストールしてCloudflaredを使い予定表を共有する(その2:Cloudflaredを設定する)

と言うわけで続いてCloudflaredをインストールする。これを入れるとルータのポート開放など面倒な手続きが一切無しで外部にサービスが公開できる。しかも無料。ドメイン名の維持費だけは掛かるが21世紀も4分の1が終わるのだから個人で1つや2つはドメイン名を持っているのは普通なので実質無料と考えれば良い。

サクサクとCloudflareにアカウントを作る。

www.cloudflare.com私はもう登録してしまったので特に登録までに細かい話は出来ないししない。引っかかるようなところはないので問題ないと思う。「ドメイン登録」から「ドメインの登録」を選び、良い感じのドメイン名を登録する。

TLDも好きに選べば良いが、お安いのはorgドメインなのでお手頃なのを選ぼう。前述の通り、ここだけはおカネがかかるのでクレジットカードが必要になる。登録すれば下のようにアクティブなステータスのドメインが追加されるはずだ。

おもむろに左ペインから"Zero Trust"を選択する。赤で囲んで矢印を付けたところである。3種類ぐらいプランが出てくるが、無料のZero Trust Freeプランで良い。

ネットワーク→Tunnelsを選び、「+トンネルの作成」を選ぶ。トンネルの種類はCloudflaredを選び、トンネル名を付ける。トンネルを保存したら「コネクタをインストールして実行」で「Debian」→「arm64-bit」を選択する。

下に出てくるコマンドをRaspberryPiで実行するとCloudflaredがインストールされ、トンネルが設定される。正常にインストールされれば画面下の方のConnectorsペインにコネクタIDやステータス、Cloudflaredのバージョンが表示される。「次へ」へ進もう。

ここでさっき登録したドメイン名を使い、サブドメインを設定する。

この図の例ではradicale.[ドメイン名]をパブリックホストとし、サービスにRadicaleが動作しているlocalhost:5323を指定してやる。これでトンネルを保存をする。問題なければこれで外部からアクセスできるはずだ。試しにブラウザでアクセスしてみる。こんな感じでアクセスできるはずだ。

ここまで来ればあとは如何様にでもなる。今回はCalDAVを設定したのでThunderbirdのカレンダー機能に登録しても良いし、iOS付属のカレンダーにアカウントを追加しても良い。

 

ただし、ドメイン名を登録したからには最低限の設定をしておいた方が良い。具体的にはSPAMの送信ドメインに使われないように、DNS周りを設定しておく。

ダッシュボードのトップ画面から「Webサイト」を選択し、DNSレコードを下記のように設定する。

上から順にNULL MX、DMARC、DKIMSPFの設定である。これで自分が登録したドメインの名義でSPAMが送られる可能性が低くなる。詳細は適宜調べてほしい、と言うかCloudflareが解説をしているので読んでおこう。

www.cloudflare.com大体こんな感じである。余らせてるRaspberryPiがあったら適当にサービスを立ち上げてみよう。

RaspberryPi 3BにdockerでRadicaleをセルフホストしてCloudflaredを使い予定表を共有する(その1:Rasicaleのdockerを使ったインストールと実行)

Twitter(世界最後の一人になるまでこの名前で呼び続ける)でもはちょっと前に書いたが最近結婚した。となると家族間で予定表を共有したいという願望が出てくる。もちろんiCloudのカレンダーや予定表共有サービスはあるが、「クラウドなどというものはない。単なる他人のコンピュータだ」というモットーを掲げているので出来れば自前でサーバを立ち上げたい。ついでに余ってるRaspberryPiの使い道にもなりそう。

と言うわけで、どこのご家庭でも余ってるRaspberryPiにCalDAVサーバであるRadicaleをインストールしてセルフホスティング、Cloudflaredを使って誰でも1つは持っている独自ドメインからアクセスできるようにする。

radicale.org

"docker Radicale"で検索して出てくるQiitaの記事のはバージョンアップされてないので使わない。dockerはインストールされているものとして話を進める。

今回使うのはこれ。

https://hub.docker.com/r/tomsquest/docker-radicale/

横にもあるとおり、下記のコマンドでdockerhubからpullする。

docker pull tomsquest/docker-radicale

Radicale用のディレクトリを作ってその中にconfigディレクトリとdataディレクトリを作る。さらに起動用のdocker-compose.ymlを作成。上記のサイトではなぜかリンクが切れているので、githubから直接拾ってくる。

mkdir radicale-docker
cd ./radicale-docker/
mkdir data
mkdir config
wget https://raw.githubusercontent.com/tomsquest/docker-radicale/refs/heads/master/docker-compose.yml

github.comこれを元に少々弄ってやる。まずポートは127.0.0.1から0.0.0.0に(この変更をしないとローカルホスト以外からアクセスできない)。またRaspberryPiの環境ではリソース制限周りが動かない(コンテナ自体は動くが起動時にアラートが出る)のと、30秒ごとに死活監視をするとログが埋まるのでコメントアウトする。また、設定ファイルや認証ファイルを入れるconfigディレクトリをボリュームにマウントさせる。変更したdocker-compose.ymlが以下の通り。

services:
  radicale:
    image: tomsquest/docker-radicale
    container_name: radicale
    ports:
      - 0.0.0.0:5232:5232
    init: true
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - SETUID
      - SETGID
      - CHOWN
      - KILL
    #    deploy:
    #    resources:
    # limits:
    #  memory: 256M
    #  pids: 50
    #healthcheck:
    #  test: curl -f http://127.0.0.1:5232 || exit 1
    #  interval: 30s
    #  retries: 3
    restart: unless-stopped
    volumes:
      - ./data:/data
      - ./config/:/config

あとはコンフィグとユーザ認証周りを行えばいい。ユーザ認証のベストプラクティスはbcryptであるが、デフォルトのpython3だと別にパッケージが必要なので次善策としてソルト付きSHA-512を選ぶ。configディレクトリに移動し、configファイルを作成する。

cd config
sudo vim config

内容は下記の通り。serverのhostsオプションで0.0.0.0を指定することにより、ローカルホスト以外からのアクセスを許可する。authはパスワードの形式と保存場所、storageはデータの保存場所の指定。

[server]
hosts = 0.0.0.0:5232

[auth]
type = htpasswd
htpasswd_filename = /config/users
htpasswd_encryption = sha512

[storage]
filesystem_folder = /data/collections

次にユーザ認証である。上に設定したとおり、htpasswd形式のusersというファイルをこのディレクトリに作る。わざわざApacheをインストールするのも面倒だし、かと言って検索すると出てくるオンラインのhtpasswdファイル作成ページを使うのも怪しいので、デフォルトで入っているopensslを使おう。ソルトも乱数生成コマンドを使えば自動で作ることが出来る。

echo "USER:$(openssl passwd -6 -salt $(openssl rand -base64 6) PASSWORD)" > users

このコマンドのUSERとPASSWORDを適当に設定してやれば良い。なお、当然このコマンドは平文のパスワードを含んだまま.bash_historyにそのまま残るのでちゃんと削除して置くこと。

それではradicaleディレクトリに戻ってdocker-compose.ymlを起動してやる。

cd /home/pi/radicale-docker/
sudo docker compose up -d

正常に動いているか見てみる。

docker ps -a

終了したければ下記コマンドを指示する。

sudo docker compose down

これで動作しているコンテナが一覧表示される。問題があればそれらしいことが書いてあるはずだ。問題が無ければ母艦PCから192.168.11.4:5323へアクセスすると下に示すようなログインページが開く。

ここに上で設定したユーザ名とパスワードを入力すればログインできる。最初の状態では何もないので、右下の+ボタンを押して新規にカレンダーを作成する。

これでサーバ側の設定は終わり。このままルータを弄ってポートを空けてやる、等すれば外部からも繋げるが、前述の通りCloudflaredを使って保有するドメイン名から簡単にアクセスするように出来るので次の記事でそれを紹介しよう。

【令和最新版】RaspberryPi でIPアドレスを固定する設定【2024年8月最新版】

ed25519鍵だと例よりかなり短くなるが問題ない


いつの間にかRaspbianからRaspberryPiOSに名前が変わっていたRaspberryPiのOS、Debian bookwormベースの最新版ではもはやdhcpdは使用されなくなったため、昔のセットアップ手順に書いてあるdhcpd.confでは駄目になっていた。

science-as-a-candle-in-the-dark.hatenablog.com勝手に変更しないで欲しいが、変更されてものは仕方あるまい。しかし新しい方法はなんぞや、と検索しても古い情報も混ざって引っかかるため記録のために残すことにした。

なお使用環境はRaspberryPi Imagerで最新版(Debian 12 bookworm)のRaspberryPi OS Liteを、無線LANの設定を入れてインストールしたものとする。

  1. TeraTermでraspberry.local:22にアクセスする。
  2. sudo nmcli connection showでネットワークの状態を確認する。
    NAME                UUID                                  TYPE      DEVICE
    preconfigured       hogehoge-hoge-hoge-hoge-hogehogehoge  wifi      wlan0
    lo                  hogehoge-hoge-hoge-hoge-hogehogehoge  loopback  lo
    Wired connection 1  hogehoge-hoge-hoge-hoge-hogehogehoge  ethernet  --
    と表示されるはずである。
  3. sudo vim /etc/NetworkManager/system-connections/preconfigured.nmconnectionで設定ファイルを開く。
  4. [ipv4]の項ではmethod=autoの一行のはずなので、下記のように書き換える。前半が指定するIPアドレス、後半がデフォルトゲートウエイである。なんとなく/24は後半につけたくなるが間違えないように。
    [ipv4]
    method=manual
    address1=192.168.11.4/24,192.168.11.1
    
  5. sudo systemctl restart NetworkManager.serviceでサービスを再起動。

これで行ける。ネットワーク周りがdhcpdからNetworkManagerなるサービスに変更されたせいらしい。

 

Thunderbirdを使っていたらOutlookから「アクションが必要です」とメールが来たので対応した。

  • TL;DR. アカウントの設定でサーバの認証を"OAuth2"にして認証アプリで認証し、Thunderbirdを登録すればいい。

 

基本的に使ってないMicrosoftアカウント用のOutlookのメールにこんなメールが来た。

言うまでもないがこんなもんフィッシングをまず疑うべきで、不用意にリンクをクリックしたりしてはいけない。同様の例がないか検索してみる。

と、どうやら本当に、OAuth2なる新しい認証方式にしないといけないらしいので下記サイトを参考に変更した。

https://www.g-ipc.shimane-u.ac.jp/_files/00220781/office365_mfa_set_thunderbird_modern.pdf

内容は簡単である。ThunderbirdOutlookのアカウント設定を開き、セキュリティ設定の認証方式を「OAuth2」にする。するとMicrosoftアカウントのログイン画面が出るのでログインして登録する。

このときにThunderbirdCookieを受け入れない設定にしていたのでエラーが出た。設定画面で「サイトから送られてきたCookieを保存する」にチェックを入れる。普通なデフォルトでチェックが入ってると思う。

これで「アクセスが許可されているアプリとサイト」にThunderbirdが追加され今後も使えるようになる。

 

以上。記録のために。

 

「マリウポリの20日間」を見てきた

synca.jp見てきた。とにかく見に行けという言葉しかない。

screenonline.jp内容に関してはこの声明文に書いてあることがほとんどだが、それを動画で見るかそうでないかは天と地ほどの差がある。

唯一残った記者が戦火の中、産科病院への攻撃を撮影し、命懸けで携帯の繋がるポイントに出向いて本社に送った動画をフェイク扱いされる。一体どういうことなんだ…と思ったが、逆に言えばロシア側からは「フェイクだと主張しなければならない」ほど重要な報道だったという証左だと気付いた。

ともすればマスコミは「写真や動画なんか撮ってないで助ける手助けをしろよ!」とか「いるだけで負担がかかるんだよ!」と批判されがちだし、実際そう言う側面もあるんだろうけど、「現地で何が起きてるかを伝える」と言う使命の重要さを再確認させられる。

取材していた病院が包囲されたとき、「捕まったらあの動画はフェイクだと認めされられるぞ」と言い、特殊部隊まで呼んで包囲下からギリギリで脱出させた警官の有能さには頭が下がる。

しかもこの警察官は産科病院への攻撃の直後、破壊された病院を背景に「これはロシアによる明確な戦争犯罪だ」というステートメントを撮影させてたのだが、上手とは言えないがはっきりとした英語で発表していて、「世界に知らせる」ことの重要さをよく理解していた。

 

かつてEEtimesJapanで「「英語に愛されないエンジニア」のための新行動論」というコラムを書かれていた江端氏がこの戦争を受けて、

「侵略者の非道」を語る時に、世界に対して、事実上の世界言語である「英語」を使って語れるかどうかは、国防上、重要な事項ではないか、と思います。 

www.kobore.net

eetimes.itmedia.co.jpと書いていたが、日本人も「ここ○○は民間施設であり、××国による攻撃は明確な戦争犯罪だ」と稚拙でも堂々とした宣言できるほどの英語力を持つようになって欲しい。もちろんそんな宣言が出るような未来があっては困るのだが…

とにかく、上映館は決して多くはないが無理をしてでも見に行くべきである。