Skip to content
Open

set #72

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 46 additions & 41 deletions CURRENT_STATUS.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,55 @@
# 褒めたもん(metamon_code)現状整理メモ

最終更新: 2026-02-15(Codex調査
最終更新: 2026-02-15(Codex更新

## 1. 目的と実行の流れ
- `main/src/hometamon.py` の `main()` がエントリポイント
- Home Timeline を取得し、条件に応じて以下を実行する構成
- エントリポイントは `main/src/hometamon.py` の `main()`。
- Home Timeline を読み、条件に応じて以下を実行
- 挨拶リプ(おはよう / おやすみ)
- 褒めリプ(キーワードベース)
- 15時のおやつツイート
- フォローバック管理
- 実行件数のDMレポート送信
- Docker運用時は `main/crontab` で **6分ごと** に `python3 /src/hometamon.py` を叩く設計。

## 2. 現在のランタイム/依存の特徴
- READMEの依存記載が古め(Django など現状未使用の記載が残る)。
- 実際のコンテナ依存は `main/Dockerfile` 側がソースオブトゥルースに近い。
- `python-dotenv==1.0.0`
- `tweepy==3.8.0`(v1.1 API時代)
- `pytest==5.4.1`, `pytest-mock==3.1.0`
- ローカル直実行では `dotenv` など未導入環境だとテスト収集時に失敗する。

## 3. CI/CD の状態
- GitHub Actions が2本。
- `ci.yml`: push時に docker-compose build → コンテナ内pytest
- `linter.yml`: PR時に black + reviewdog
- 直近履歴でも CI / linter の調整コミットが入っており、保守は継続されていた形跡あり。

## 4. いま見えている技術的リスク
- `main/src/hometamon.py` の `main()` 内で `hometamon.tweet_linestamp()` を呼んでいるが、定義されているのは `test_tweet_linestamp()`。
- 指定日時に到達すると `AttributeError` になる可能性が高い。
- Twitter APIが古い実装(`tweepy==3.8.0` + v1.1想定)で、現行運用時は権限・API仕様差分を要確認。
- 除外ワードや分類はハードコード中心で、運用ルール変更時にコード修正が必要。
- READMEの手順・依存情報と実態の乖離あり(再起動時のオンボーディング負荷が高い)。

## 5. テスト現状(この調査での実行)
- `pytest -q` は、環境に `python-dotenv` が無いため import error で停止。
- Docker経由テストは、この実行環境に Docker コマンドが無く未実施。

## 6. リリース再開に向けた優先TODO(提案)
1. **クリティカルバグ修正**: `tweet_linestamp` 呼び出し不整合を解消。
2. **Secrets/Env棚卸し**: `.env` 必須値(APIキー、管理者ID等)を再確認。
3. **実行確認**: `--test` 相当の安全モード(投稿しないdry-run)を用意して疎通確認。
4. **README更新**: 実行手順を Docker中心に一本化、依存記載を最新化。
5. **段階リリース**: まず cron 無効 + 手動単発、次に cron 有効化。

## 7. 現状理解サマリ(短く)
- Botの基本機能は保たれているが、**そのまま本番再開はやや危険**。
- 特に `tweet_linestamp` 名称不整合は先に潰すべき。
- 依存と運用手順の再整備をした上で、段階的に復帰するのが安全。
- 本番コンテナ(`prd`)では `main/crontab` により 6分ごとに `python3 /src/hometamon.py` を実行する設計。

## 2. ランタイム/依存の現状
- Docker Python は `3.13-alpine`。
- 依存管理は `requirements.txt` から `pyproject.toml` + `uv.lock` へ移行済み。
- `main/Dockerfile` は `uv sync --frozen` で `.venv` を作成し、`/app/.venv/bin` を `PATH` に追加。
- direct dependency(2026-02-15時点)
- `black>=26.1.0`
- `pykakasi>=2.3.0`
- `pytest>=9.0.2`
- `pytest-mock>=3.15.1`
- `python-dotenv>=1.2.1`
- `tqdm>=4.67.3`
- `tweepy>=4.16.0`

## 3. Docker/開発運用
- `main/Dockerfile` は multi-stage 構成。
- `dev`: `CMD ["sh"]`
- `prd`: `crond` 常駐
- `docker-compose.yml` は本番相当定義、`docker-compose.override.yml` で開発用差分(`target: dev`、volume mount、`command: sh`)を上書き。
- `Makefile` は `docker compose` コマンドへ統一済み。`--no-cache` は通常運用から削除済み。

## 4. CI/CD の現状
- ワークフローは2本(どちらも `pull_request` トリガー)。
- `ci.yml`: Docker Buildx + GitHub Actions cache(`type=gha`)で `dev` イメージをビルドし、`pytest tests` を実行。
- `linter.yml`: `reviewdog/action-black` で black結果をPRコメントとして指摘(warning、failしない設定)。
- CI高速化として Docker layer cache を導入済み。

## 5. テスト現状(最新確認)
- Docker内で `pytest tests` 実行済み。
- 結果: **62 passed**(警告のみ)。
- `tweepy` 更新に伴い `OAuthHandler` の deprecation warning は出るが、現時点ではテスト成功。

## 6. 残課題/リスク
- `main()` で `hometamon.tweet_linestamp()` を呼んでいる一方、実装は `test_tweet_linestamp()` 名のまま。
- 条件ヒット時に `AttributeError` となるリスクが継続。
- `OAuthHandler` は非推奨警告が出るため、`OAuth1UserHandler` への移行を検討したい。
- Bot判定ロジック(除外ワード等)はハードコード中心で、運用ルール変更時はコード変更が必要。

## 7. 次にやると良いこと
1. `tweet_linestamp` 呼び出し不整合を修正。
2. Tweepy認証実装を `OAuth1UserHandler` へ移行。
3. `.env` 必須キーのドキュメント化と起動時バリデーション追加。
2 changes: 1 addition & 1 deletion main/crontab
Original file line number Diff line number Diff line change
@@ -1 +1 @@
*/6 * * * * python3 /src/hometamon.py
*/6 * * * * cd /app && python3 /app/src/hometamon.py
140 changes: 118 additions & 22 deletions main/src/hometamon.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import random
import datetime as dt
import unicodedata
from types import SimpleNamespace

from dotenv import load_dotenv

Expand All @@ -16,11 +17,18 @@

class Hometamon:
def __init__(self):
if os.path.exists("/env/.env"):
load_dotenv("/env/.env")
elif os.path.exists("env/.env"):
load_dotenv("env/.env")
else:
env_candidates = [
"/app/env/.env",
"/env/.env",
"env/.env",
]
loaded = False
for env_path in env_candidates:
if os.path.exists(env_path):
load_dotenv(env_path)
loaded = True
break
if not loaded:
print("error doesn't exist .env path")

if os.path.dirname("images"):
Expand All @@ -33,11 +41,20 @@ def __init__(self):
access_token = os.environ.get("ACCESS_TOKEN") or ""
token_secret = os.environ.get("TOKEN_SECRET") or ""

auth = tweepy.OAuthHandler(
auth = tweepy.OAuth1UserHandler(
consumer_key=consumer_key, consumer_secret=consumer_secret
)
auth.set_access_token(key=access_token, secret=token_secret)
self.api = tweepy.API(auth, wait_on_rate_limit=True)
self.client = None
if consumer_key and consumer_secret and access_token and token_secret:
self.client = tweepy.Client(
consumer_key=consumer_key,
consumer_secret=consumer_secret,
access_token=access_token,
access_token_secret=token_secret,
wait_on_rate_limit=True,
)
self.my_twitter_user_id = os.environ.get("TWITTER_USER_ID")
self.manuscript = meta_manuscript.Manuscript()
JST = dt.timezone(dt.timedelta(hours=+9), "JST")
Expand Down Expand Up @@ -188,8 +205,83 @@ def __init__(self):
}

def get_tweets(self):
if self.client:
if not self.my_twitter_user_id:
raise RuntimeError("TWITTER_USER_ID is required when using v2 client")
response = self.client.get_home_timeline(
id=self.my_twitter_user_id,
max_results=100,
expansions=["author_id"],
tweet_fields=["author_id"],
user_fields=["username", "name", "description"],
user_auth=True,
)
tweets = response.data or []
users = {}
if response.includes and "users" in response.includes:
users = {str(user.id): user for user in response.includes["users"]}
legacy_like_tweets = []
for tweet in tweets:
user = users.get(str(tweet.author_id))
legacy_like_tweets.append(
SimpleNamespace(
id=tweet.id,
text=tweet.text,
favorited=False,
user=SimpleNamespace(
id=user.id if user else 0,
name=user.name if user else "",
screen_name=user.username if user else "",
description=user.description if user else "",
),
)
)
return legacy_like_tweets
return self.api.home_timeline(count=100, since_id=None)

def _tweet(self, status, in_reply_to_status_id=None, image_file=None):
if self.client:
if image_file:
media = self.api.media_upload(filename=image_file)
kwargs = {"text": status, "media_ids": [media.media_id]}
if in_reply_to_status_id:
kwargs["in_reply_to_tweet_id"] = in_reply_to_status_id
self.client.create_tweet(user_auth=True, **kwargs)
else:
kwargs = {"text": status}
if in_reply_to_status_id:
kwargs["in_reply_to_tweet_id"] = in_reply_to_status_id
self.client.create_tweet(user_auth=True, **kwargs)
return
if image_file:
if in_reply_to_status_id is None:
self.api.update_with_media(
filename=image_file,
status=status,
)
else:
self.api.update_with_media(
filename=image_file,
status=status,
in_reply_to_status_id=in_reply_to_status_id,
)
else:
if in_reply_to_status_id is None:
self.api.update_status(status=status)
else:
self.api.update_status(
status=status,
in_reply_to_status_id=in_reply_to_status_id,
)

def _favorite(self, tweet_id):
if self.client:
if not self.my_twitter_user_id:
raise RuntimeError("TWITTER_USER_ID is required when using v2 client")
self.client.like(self.my_twitter_user_id, tweet_id, user_auth=True)
return
self.api.create_favorite(tweet_id)

def user_name_changer(self, user_name):
# 正規化
normalize_user_name = unicodedata.normalize("NFKC", user_name)
Expand All @@ -210,8 +302,8 @@ def good_morning(self, tweet):
# if random.random() < image_ratio:
# pass
# else:
self.api.update_status(status=reply, in_reply_to_status_id=tweet.id)
self.api.create_favorite(tweet.id)
self._tweet(status=reply, in_reply_to_status_id=tweet.id)
self._favorite(tweet.id)
return reply

def good_night(self, tweet, image_ratio=0.2):
Expand All @@ -225,12 +317,12 @@ def good_night(self, tweet, image_ratio=0.2):
self.counts["good_night"] += 1
if random.random() < image_ratio:
image_file = os.path.join(self.image_dir, "oyasumi_w_newtext.png")
self.api.update_with_media(
filename=image_file, status=reply, in_reply_to_status_id=tweet.id
self._tweet(
status=reply, in_reply_to_status_id=tweet.id, image_file=image_file
)
else:
self.api.update_status(status=reply, in_reply_to_status_id=tweet.id)
self.api.create_favorite(tweet.id)
self._tweet(status=reply, in_reply_to_status_id=tweet.id)
self._favorite(tweet.id)
return reply

def choose_image_by_reply(self, reply: str) -> str:
Expand All @@ -256,12 +348,12 @@ def praise(self, tweet, image_ratio=0.2):
if random.random() < image_ratio:
image_name = self.choose_image_by_reply(reply)
image_file = os.path.join(self.image_dir, image_name)
self.api.update_with_media(
filename=image_file, status=reply, in_reply_to_status_id=tweet.id
self._tweet(
status=reply, in_reply_to_status_id=tweet.id, image_file=image_file
)
else:
self.api.update_status(status=reply, in_reply_to_status_id=tweet.id)
self.api.create_favorite(tweet.id)
self._tweet(status=reply, in_reply_to_status_id=tweet.id)
self._favorite(tweet.id)
return reply

def tweet_sweet(self):
Expand All @@ -270,20 +362,24 @@ def tweet_sweet(self):
"\n⊂・ー・つ" + chr(int(random.choice(self.manuscript.sweets), 16)) + "\n"
) # 16進数から変換
status += random.choice(self.manuscript.sweet_tweet_after)
self.api.update_status(status=status)
self._tweet(status=status)

def test_tweet_linestamp(self):
def tweet_linestamp(self):
reply = "ぼくのLINEスタンプがでたもん!!!ぼくのかわりにみんなをほめてほしいもん!!よろしくもん!!\nhttps://store.line.me/stickershop/product/17652748"
image_file = os.path.join(self.image_dir, "stamp", "all.png")
self.api.update_with_media(filename=image_file, status=reply)
self._tweet(status=reply, image_file=image_file)

# Backward compatibility for existing tests/calls.
def test_tweet_linestamp(self):
self.tweet_linestamp()

def test_tweet(self, image_flg=False):
status = "起きてるもん!\n⊂・ー・つ"
if image_flg:
image_file = os.path.join(self.image_dir, "icon.jpg")
self.api.update_with_media(filename=image_file, status=status)
self._tweet(status=status, image_file=image_file)
else:
self.api.update_status(status=status)
self._tweet(status=status)
self.counts["test"] += 1
return status

Expand All @@ -300,7 +396,7 @@ def check_exclude(self, tweet): # 除外するかどうかcheck
if self.set_task_words[0] in tweet.text:
return True
else: # 自分に向けてのtweetかつ,設定が入っていないならファボ
self.api.create_favorite(id=tweet.id)
self._favorite(tweet.id)
return True
elif (
len(tweet.text) >= 80
Expand Down
1 change: 1 addition & 0 deletions main/tests/test_hometamon.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def app(self, mocker):
app.manuscript.sweets = ["1F950"] # croissant
app.my_twitter_user_id = "966247026416472064"
app.api = mocker.MagicMock()
app.client = None
return app

@pytest.fixture()
Expand Down
Loading