from __future__ import annotations import asyncio import os import time from datetime import datetime from io import BytesIO from typing import Any, TypedDict import atproto # type: ignore import requests from atproto import Client # type: ignore from atproto.models import AppBskyEmbedExternal, AppBskyEmbedImages # type: ignore from atproto.models.AppBskyFeedPost import ReplyRef # type: ignore from atproto_client.models.app.bsky.feed.get_timeline import Response # type: ignore from requests.exceptions import Timeout import account import nicolib from nizika_ai.consts import Character, GPTModel, Platform, QueryType from nizika_ai.models import Answer, AnsweredFlag, Query, User TARGET_WORDS = ['deerjika', 'ニジカ', 'ぼっち', '虹夏', '郁代', 'バーカ', 'kfif', 'kita-flatten-ikuyo-flatten', 'ラマ田', 'ゴートう', 'ぼざクリ', 'オオミソカ', '伊地知', '喜多ちゃん', '喜タイ', '洗澡鹿', 'シーザオ', '今日は本当に', 'ダイソーで', '変なチンチン', 'daisoで', 'だね~(笑)', 'おやつタイム', 'わさしが', 'わさび県', 'たぬマ', 'にくまる', 'ルイズマリー', '餅', 'ニジゴ', 'ゴニジ', 'ニジニジ', '新年だよね', 'うんこじゃん', 'ほくほくのジャガイモ'] time.sleep (60) client = Client (base_url = 'https://bsky.social') client.login (account.USER_ID, account.PASSWORD) async def main ( ) -> None: """ メーン処理 """ await asyncio.gather (like_posts (), check_mentions (), answer ()) async def like_posts ( ) -> None: while True: try: for post in fetch_target_posts (): client.like (**post) except Exception as e: print (f"[like_posts] { type (e).__name__ }: { e }") await asyncio.sleep (60) async def check_mentions ( ) -> None: while True: try: for uri in check_notifications (): records = fetch_thread_contents (uri, 20) if records: record = records[0] image_url: str | None = None if record['embed'] and hasattr (record['embed'], 'images'): image_url = ('https://cdn.bsky.app/img/feed_fullsize/plain' f"/{ record['did'] }" f"/{ record['embed'].images[0].image.ref.link }") user = _fetch_user (record['did'], record['name']) _add_query (user, record['text'], image_url, { 'uri': record['strong_ref']['uri'], 'cid': record['strong_ref']['cid'] }) except Exception as e: print (f"[check_mentions] { type (e).__name__ }: { e }") await asyncio.sleep (60) async def answer ( ) -> None: while True: answered_flags = ( AnsweredFlag .where ('platform', Platform.BLUESKY.value) .where ('answered', False) .get ()) for answered_flag in answered_flags: td: dict[str, Any] answer = answered_flag.answer match QueryType (answer.query_rel.query_type): case QueryType.BLUESKY_COMMENT: td = answer.query_rel.transfer_data or { } uri: str | None = td.get ('uri') cid: str | None = td.get ('cid') if (not uri) or (not cid): continue sref = { 'uri': uri, 'cid': cid } strong_ref = atproto.models.create_strong_ref (sref) # type: ignore reply_ref = ReplyRef (root = strong_ref, parent = strong_ref) try: client.post (answer.content, reply_to = reply_ref) except Exception as e: print (f"[answer/reply] { type (e).__name__ }: { e }") continue answered_flag.answered = True answered_flag.save () case QueryType.KIRIBAN | QueryType.NICO_REPORT: td = answer.query_rel.transfer_data or { } video_code: str | None = td.get ('video_code') if not video_code: continue uri = f"https://www.nicovideo.jp/watch/{ video_code }" (title, description, thumbnail) = nicolib.fetch_embed_info (uri) try: resp = requests.get (thumbnail, timeout = 60) resp.raise_for_status () upload = client.com.atproto.repo.upload_blob (BytesIO (resp.content)) thumb = upload.blob except Timeout: thumb = None except Exception as e: print (f"[answer/nico-thumb] { type (e).__name__ }: { e }") thumb = None external = AppBskyEmbedExternal.External ( title = title, description = description, thumb = thumb, uri = uri) embed_external = AppBskyEmbedExternal.Main (external = external) try: client.post (answer.content, embed = embed_external) except Exception as e: print (f"[answer/nico-post] { type (e).__name__ }: { e }") continue answered_flag.answered = True answered_flag.save () case QueryType.SNACK_TIME: try: with open ('./assets/snack-time.jpg', 'rb') as f: image = AppBskyEmbedImages.Image ( alt = ( '左に喜多ちゃん、右に人面鹿のニジカが' 'V字に並んでいる。' '喜多ちゃんは右手でピースサインをして' '片目をウインクしている。' 'ニジカは両手を広げ、' '右手にスプーンを持って' 'ポーズを取っている。' '背景には' '赤と黄色の放射線状の模様が広がり、' '下部に「おやつタイムだ!!!!」という' '日本語のテキストが表示されている。'), image = client.com.atproto.repo.upload_blob (f).blob) client.post (answer.content, embed = AppBskyEmbedImages.Main (images = [image])) answered_flag.answered = True answered_flag.save () except Exception: pass case QueryType.HOT_SPRING: try: with open ('./assets/hot-spring.jpg', 'rb') as f: image = AppBskyEmbedImages.Image ( alt = ('左に喜多ちゃん、右にわさび県産滋賀県が' 'V字に並んでいる。' '喜多ちゃんは右手でピースサインをして' '片目をウインクしている。' 'わさび県産滋賀県はただ茫然と' '立ち尽くしている。' '背景には' '血と空の色をした放射線状の模様が広がり、' '下部に「温泉に入ろう!!!」という' '日本語のテキストが表示されている。'), image = client.com.atproto.repo.upload_blob (f).blob) client.post (answer.content, embed = AppBskyEmbedImages.Main (images = [image])) answered_flag.answered = True answered_flag.save () except Exception: pass await asyncio.sleep (10) def check_notifications ( ) -> list[str]: uris: list[str] = [] last_seen_at = client.get_current_time_iso () notifications = client.app.bsky.notification.list_notifications () for notification in notifications.notifications: if not notification.is_read: match notification.reason: case 'mention' | 'reply' | 'quote': uris.append (notification.uri) case 'follow': client.follow (notification.author.did) client.app.bsky.notification.update_seen ({ 'seen_at': last_seen_at }) return uris def fetch_thread_contents ( uri: str, parent_height: int, ) -> list[Record]: post_thread = client.get_post_thread (uri = uri, parent_height = parent_height) if not post_thread: return [] res = post_thread.thread records: list[Record] = [] while res: if hasattr (res, 'post'): records.append ({ 'strong_ref': { 'uri': res.post.uri, 'cid': res.post.cid }, 'did': res.post.author.did, 'handle': res.post.author.handle, 'name': (res.post.author.display_name or res.post.author.handle), 'datetime': res.post.record.created_at, 'text': getattr (res.post.record, 'text', None) or '', 'embed': getattr (res.post.record, 'embed', None) }) res = res.parent else: break return records def fetch_target_posts ( ) -> list[LikeParams]: posts: list[LikeParams] = [] timeline: Response = client.get_timeline () for feed in timeline.feed: me = getattr (client, 'me', None) my_did = me.did if me else '' if (feed.post.author.did != my_did and (feed.post.viewer.like is None) and any (target_word in (feed.post.record.text or '').casefold () for target_word in TARGET_WORDS)): posts.append (LikeParams ({ 'uri': feed.post.uri, 'cid': feed.post.cid })) return posts def _add_query ( user: User, content: str, image_url: str | None = None, transfer_data: dict[str, Any] | None = None, ) -> None: query = Query () query.user_id = user.id query.target_character = Character.DEERJIKA.value query.content = content query.image_url = image_url or None query.query_type = QueryType.BLUESKY_COMMENT.value query.model = GPTModel.GPT3_TURBO.value query.sent_at = datetime.now () query.answered = False query.transfer_data = transfer_data query.save () # TODO: 履歴情報の追加 def _fetch_user ( did: str, name: str, ) -> User: user = User.where ('platform', Platform.BLUESKY.value).where ('code', did).first () if user is None: user = User () user.platform = Platform.BLUESKY.value user.code = did user.name = name user.save () return user class LikeParams (TypedDict): uri: str cid: str class Record (TypedDict): strong_ref: StrongRef did: str handle: str name: str datetime: str text: str embed: object class StrongRef (TypedDict): uri: str cid: str if __name__ == '__main__': asyncio.run (main ())