0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

おひとりさまインスタンスにログを出力する Fediverse (4) Advent Calendar 2023

Last updated at Posted at 2023-12-21

これは,Fediverse (4) Advent Calendar 2023 の 12 月 22 日の記事です。21日目の記事は 💙mostrで近づいた、Fediverseとの距離 でした。

本稿の趣旨

おひとりさまインスタンスで自分宛にダイレクトメッセージを送ったら,自分だけが見られるロガーとして使えるんじゃない?

おひとりさまインスタンス

Mastodon はジャック・ドーシーの横暴に耐えかね,古き良き Twitter を実現するために生まれました。Twitter が抱える問題の原因は,ただひとりの支配者が SNS のすべてを掌握できてしまうからだと考えたのです。そのことは後にカレン氏によって実証され,人類は再び邪悪な支配者の前に破れることになります。Fediverse は分散化することによって,この問題を解決しようとしています。

2023-12-22_01-18-06.png

新参者の私は Mastodon 以外の Fediverse プラットフォームについてはよくわかりませんが,Misskey や Pleroma など,Open Source 実装が公開されているシステムの場合,自前のサーバにインストールすることができます。自分だけがアカウントを作成できるように設定したインスタンスのことは,おひとりさまインスタンスと呼ばれます。

Logging モジュール

処理状況を追跡し動作検証するには記録を取ることが大切です。Python にはこの目的で作られた Logging モジュールがあります。例えば,以下のように使います。

import logging
logging.debug('足袋に玉を入れもうして,くるまを打ち付けるで候')

実際には getLogger を使うべきとか作法がありますが,設定ファイルから書式を読み込めるなど機能も十分にあり,いざ実装するとめんどうなロガーを手軽に使うことができます。

PPAP

おひとりさまインスタンスには自分しかいません。その自分にダイレクトメッセージを送ると,他のサーバへ通知されることはありません。Mastodon は End-to-End Encryption ではないため,送受信者とその経路は暗号化されていません。しかし,おひとりさまインスタンス内であれば,自分以外にアクセスできないため,Slack に通知していたような機密性の水準の投稿はできそうです。

準備するもの

  • おひとりさまインスタンス
  • 投稿用 Access Token

おひとりさまインスタンスはいくつかの方法で準備できます。本稿をここまで読んだ方であれば,VPS をいくつか運用しているでしょうから,Docker Compose を使って稼働させるのがお手軽かもしれません。日本語のサポートがあるホスティングサービス(Hostdon 等)を利用する方法もあります。直近の Advent Calendar 19日目 おひとり様 Mastodon サーバーを支える技術 などをご参照ください。私は固定 IP アドレスを振った自宅 Linux サーバ上で,他のサービスとともに Docker コンテナで動作させています。

投稿用 Access Token は,Preferences | Development | New application から,write にだけチェックを入れて作成します。

2023-12-22_02-42-31.png

すると,このような雰囲気のものが生成されます。必要になるのは Your access token とあるものだけです。

Client keyClient secret は API を利用するツールそれぞれに対応する,いわゆる ID/パスワードに相当する情報です。これらによりクライアントが認証されると,API リクエストに使うための Access Token を生成することができます。今回は Mastodon のウェブインターフェイスを利用したため,これら 3 つが一度に生成されました。

ログの投稿方法

cURL を使う

以下のように curl からインスタンスへアクセスすることで,Mastodon API を利用できます。

curl -X POST \
    -H 'Authorization: Bearer {ACCESS_TOKEN}' \
    -d 'visibility=direct' \
    -d 'status={MESSAGE}'
    https://{INSTANCE_NAME}/api/v1/statuses

ただし,

  • {INSTANCE_NAME}: おひとりさまインスタンス 例:mstdn.jp
  • {ACCESS_TOKEN}: 上記の準備で得たアクセストークン
  • {MESSAGE}: 投稿するメッセージ @自分のID を前置する

例えば,以下のようなリクエストを投げると,こんな感じで Private mention が自分に飛んできます。

curl -X POST \
    -H 'Authorization: Bearer vvjPKgFtlbCsVVZqKzRWMk_jTOBEuc4urvX2on41tyw' \
    -d 'visibility=direct' \
    -d 'status=@rino 裏金がバレてしまった!' \
    https://hello.world/api/v1/statuses

2023-12-22_03-12-41.png

ダイレクトメッセージは Private mentions のページで一覧することができます。

2023-12-22_03-16-06.png

私が使用している Ivory では,目立った感じの音と色で知らせてくれます。

Mastodon.py を使う

Python から投稿するのであれば,次のような雑ロガーを実装しておくと便利です。

requirements.txt
mastodon.py
python-dotenv
.env
ACCESS_TOKEN=
BASE_URL=https://
DESTINATION=@
mlogging.py
#!/usr/bin/env python3

# logging for mastodon

import datetime
from mastodon import Mastodon
import time

class MLogging:
  # Logging levels
  Levels = {
    'EMERG' : 80, 'EMERGENCY' : 80,
    'ALERT' : 70,
    'CRIT'  : 60, 'CRITICAL'  : 60,
    'FATAL' : 60,
    'ERROR' : 50,
    'WARN'  : 40, 'WARNING'   : 40,
    'NOTE'  : 30, 'NOTICE'    : 30,
    'INFO'  : 20,
    'DEBUG' : 10,
  }

  # Constructer
  # api_base_url  url of the mastodon instance
  # access_token  api access token
  # destination   username to be sent (@user for local, @user@instance for remote)
  # level         minimum logging level
  def __init__(self, api_base_url, access_token, destination, level='WARN', time_format='%Y-%m-%d %H:%M:%S'):
    # Store given information to local variables
    self.api_base_url = api_base_url
    self.access_token = access_token
    self.destination = destination
    self.level = level if level.upper() in self.Levels else 'WARN'
    self.time_format = time_format
    self.retry_count = 10
    self.retry_interval = 2

    # Create mastodon object
    self.mastodon = Mastodon(
      api_base_url = self.api_base_url,
      access_token = self.access_token,
    )

  # Send status directly to a user
  # level   logging level
  # status  message
  # media   list of images (specify empty list if no image)
  def __send_direct(self, level, status, media=[]):
    # Set level to WARNING if the given level is not in the predefined list
    if not level.upper() in self.Levels:
      level = 'WARN'

    # Send status only if the given logging level is above than criteria
    if self.Levels[self.level] <= self.Levels[level.upper()]:
      # Obtain current time and format
      now = datetime.datetime.now()
      dt = now.strftime(self.time_format)

      # Upload media if media files are specified
      media_files = []
      if media:
        for m in media:
          count = 0
          d = self.mastodon.media_post(m)
          while count < self.retry_count and d['url'] is None:
            count += 1
            d = self.mastodon.media_update(id=d['id'])
            time.sleep(self.retry_interval)
          media_files.append(d)

      # Send status directly to the user
      self.mastodon.status_post(
        '%s [%s][%s] %s' % (self.destination, level, dt, status),
        visibility = 'direct',
        media_ids = media_files if media else None,
      )

      # Wait
      time.sleep(1)

  def basicConfig(self, level='WARN', time_format='%Y-%m-%d %H:%M:%S'):
    if level in self.Levels:
      self.level = level
    self.time_format = time_format

  def emergency(self, status, media=[]):
    self.__send_direct(level='EMERG', status=status, media=media)

  def alert(self, status, media=[]):
    self.__send_direct(level='ALERT', status=status, media=media)

  def critical(self, status, media=[]):
    self.__send_direct(level='CRIT', status=status, media=media)

  def fatal(self, status, media=[]):
    self.__send_direct(level='FATAL', status=status, media=media)

  def error(self, status, media=[]):
    self.__send_direct(level='ERROR', status=status, media=media)

  def warning(self, status, media=[]):
    self.__send_direct(level='WARN', status=status, media=media)

  def notice(self, status, media=[]):
    self.__send_direct(level='NOTE', status=status, media=media)

  def info(self, status, media=[]):
    self.__send_direct(level='INFO', status=status, media=media)

  def debug(self, status, media=[]):
    self.__send_direct(level='DEBUG', status=status, media=media)

def main():
  from dotenv import load_dotenv
  import os

  # Load the environment variables from .env file
  load_dotenv()

  mlog = MLogging(
    api_base_url = os.getenv('BASE_URL'),
    access_token = os.getenv('ACCESS_TOKEN'),
    destination = os.getenv('DESTINATION'),
  )

  mlog.basicConfig(level='DEBUG')

  mlog.critical('CRITICAL MESSAGE')
  mlog.error('file not found', media=['./image1.png', './image2.png'])
  mlog.warning('WARNING MESSAGE')
  mlog.info('INFO MESSAGE')
  mlog.debug('DEBUG MESSAGE')
  return 0

if __name__ == "__main__":
  main()

2023-12-22_03-30-48.png

まとめ

いくつか Bot を稼働させているのですが(例えば 今年の残り日数),失敗したときなどに,ローカルのログへエラーを書き出しただけではいまいち気づかず,ホームタイムラインへ通知されたら便利だなあと思って試してみた次第です。

うちには爬虫類のペットがおり,一年中 28 ℃くらいに維持しているんですが,空調の停止によりかわいい爬虫類が苦しんでしまうなど耐えがたいため,室温変動を警告するといった用途でも使えそうです。

参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?