ニジカのスカトロ,ニジカトロ. https://bsky.app/profile/deerjika-bot.bsky.social
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

161 lines
6.9 KiB

  1. """
  2. Bluesky のニジカがいろいろする.
  3. (近々機能ごとにファイル分けて systemd でイベント管理する予定)
  4. """
  5. from __future__ import annotations
  6. import sys
  7. import time
  8. from datetime import datetime, timedelta
  9. from typing import TypedDict
  10. from atproto import Client, models
  11. from atproto_client.models.app.bsky.feed.get_timeline import Response
  12. import account
  13. def main (
  14. ) -> None:
  15. time.sleep (60)
  16. client = Client (base_url = 'https://bsky.social')
  17. client.login (account.USER_ID, account.PASSWORD)
  18. last_posted_at = datetime.now () - timedelta (hours = 6)
  19. has_got_snack_time = False
  20. has_taken_hot_spring = False
  21. while True:
  22. now = datetime.now ()
  23. for uri in check_notifications (client):
  24. records = get_thread_contents (client, uri, 20)
  25. if records:
  26. answer = Talk.main ((records[0]['text']
  27. if (records[0]['embed'] is None
  28. or not hasattr (records[0]['embed'],
  29. 'images'))
  30. else [
  31. { 'type': 'text', 'text': records[0]['text'] },
  32. { 'type': 'image_url', 'image_url': {
  33. 'url': (f"https://cdn.bsky.app/img/feed_fullsize"
  34. f"/plain/{ records[0]['did'] }"
  35. f"/{ records[0]['embed'].images[0].image.ref.link }") } }]),
  36. records[0]['name'],
  37. [*map (lambda record: {
  38. 'role': ('assistant'
  39. if (record['handle']
  40. == account.USER_ID)
  41. else 'user'),
  42. 'content':
  43. record['text']},
  44. reversed (records[1:]))])
  45. client.post (answer,
  46. reply_to = models.AppBskyFeedPost.ReplyRef (
  47. parent = records[0]['strong_ref'],
  48. root = records[-1]['strong_ref']))
  49. if now.hour == 15:
  50. if not has_got_snack_time:
  51. try:
  52. with open ('./assets/snack-time.jpg', 'rb') as f:
  53. image = models.AppBskyEmbedImages.Image (
  54. alt = ('左に喜多ちゃん、右に人面鹿のニジカが'
  55. 'V字に並んでいる。'
  56. '喜多ちゃんは右手でピースサインをして'
  57. '片目をウインクしている。'
  58. 'ニジカは両手を広げ、'
  59. '右手にスプーンを持って'
  60. 'ポーズを取っている。'
  61. '背景には'
  62. '赤と黄色の放射線状の模様が広がり、'
  63. '下部に「おやつタイムだ!!!!」という'
  64. '日本語のテキストが表示されている。'),
  65. image = client.com.atproto.repo.upload_blob (f).blob)
  66. client.post (Talk.main ('おやつタイムだ!!!!'),
  67. embed = models.app.bsky.embed.images.Main (
  68. images = [image]))
  69. last_posted_at = now
  70. except Exception:
  71. pass
  72. has_got_snack_time = True
  73. if now.hour == 20 and has_taken_hot_spring:
  74. has_taken_hot_spring = False
  75. if now.hour == 21 and not has_taken_hot_spring:
  76. try:
  77. with open ('./assets/hot-spring.jpg', 'rb') as f:
  78. image = models.AppBskyEmbedImages.Image (
  79. alt = ('左に喜多ちゃん、右にわさび県産滋賀県が'
  80. 'V字に並んでいる。'
  81. '喜多ちゃんは右手でピースサインをして'
  82. '片目をウインクしている。'
  83. 'わさび県産滋賀県はただ茫然と'
  84. '立ち尽くしている。'
  85. '背景には'
  86. '血と空の色をした放射線状の模様が広がり、'
  87. '下部に「温泉に入ろう!!!」という'
  88. '日本語のテキストが表示されている。'),
  89. image = client.com.atproto.repo.upload_blob (f).blob)
  90. client.post (Talk.main ('温泉に入ろう!!!'),
  91. embed = models.app.bsky.embed.images.Main (
  92. images = [image]))
  93. last_posted_at = now
  94. except Exception:
  95. pass
  96. has_taken_hot_spring = True
  97. time.sleep (60)
  98. def check_notifications (
  99. client: Client,
  100. ) -> list:
  101. (uris, last_seen_at) = ([], client.get_current_time_iso ())
  102. for notification in (client.app.bsky.notification.list_notifications ()
  103. .notifications):
  104. if not notification.is_read:
  105. if notification.reason in ['mention', 'reply', 'quote']:
  106. uris += [notification.uri]
  107. elif notification.reason == 'follow':
  108. client.follow (notification.author.did)
  109. client.app.bsky.notification.update_seen ({ 'seen_at': last_seen_at })
  110. return uris
  111. def get_thread_contents (
  112. client: Client,
  113. uri: str,
  114. parent_height: int,
  115. ) -> list:
  116. response = (client.get_post_thread (uri = uri,
  117. parent_height = parent_height)
  118. .thread)
  119. records = []
  120. while response is not None:
  121. records += [{ 'strong_ref': models.create_strong_ref (response.post),
  122. 'did': response.post.author.did,
  123. 'handle': response.post.author.handle,
  124. 'name': response.post.author.display_name,
  125. 'datetime': response.post.record.created_at,
  126. 'text': response.post.record.text,
  127. 'embed': response.post.record.embed }]
  128. response = response.parent
  129. return records
  130. class LikeParams (TypedDict):
  131. uri: str
  132. cid: str
  133. if __name__ == '__main__':
  134. main (*sys.argv[1:])