ニジカのスカトロ,ニジカトロ. 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.

180 lines
7.3 KiB

  1. from datetime import datetime, timedelta
  2. import json
  3. import time
  4. import sys
  5. from atproto import Client, models
  6. import requests
  7. from ai.talk import Talk
  8. import account
  9. def check_notifications (
  10. client: Client,
  11. ) -> list:
  12. (uris, last_seen_at) = ([], client.get_current_time_iso ())
  13. for notification in (client.app.bsky.notification.list_notifications ()
  14. .notifications):
  15. if not notification.is_read:
  16. if notification.reason in ['mention', 'reply']:
  17. uris += [notification.uri]
  18. elif notification.reason == 'follow':
  19. client.follow (notification.author.did)
  20. client.app.bsky.notification.update_seen ({ 'seen_at': last_seen_at })
  21. return uris
  22. def get_thread_contents (
  23. client: Client,
  24. uri: str,
  25. parent_height: int,
  26. ) -> list:
  27. response = (client.get_post_thread (uri = uri,
  28. parent_height = parent_height)
  29. .thread)
  30. records = []
  31. while response is not None:
  32. records += [{ 'strong_ref': models.create_strong_ref (response.post),
  33. 'did': response.post.author.did,
  34. 'handle': response.post.author.handle,
  35. 'name': response.post.author.display_name,
  36. 'datetime': response.post.record.created_at,
  37. 'text': response.post.record.text,
  38. 'embed': response.post.record.embed }]
  39. response = response.parent
  40. return records
  41. def main (
  42. ) -> None:
  43. client = Client (base_url = 'https://bsky.social')
  44. client.login (account.USER_ID, account.PASSWORD)
  45. last_posted_at = datetime.now () - timedelta (hours = 6)
  46. has_got_snack_time = False
  47. watched_videos = []
  48. while True:
  49. now = datetime.now ()
  50. for uri in check_notifications (client):
  51. records = get_thread_contents (client, uri, 20)
  52. if len (records) > 0:
  53. answer = Talk.main ((records[0]['text']
  54. if (records[0]['embed'] is None
  55. or not hasattr (records[0]['embed'],
  56. 'images'))
  57. else [
  58. { 'type': 'text', 'text': records[0]['text'] },
  59. { 'type': 'image_url', 'image_url': {
  60. 'url': f"https://cdn.bsky.app/img/feed_fullsize/plain/{ records[0]['did'] }/{ records[0]['embed'].images[0].image.ref.link }" } }]),
  61. records[0]['name'],
  62. [*map (lambda record: {
  63. 'role': ('assistant'
  64. if (record['handle']
  65. == account.USER_ID)
  66. else 'user'),
  67. 'content':
  68. record['text']},
  69. reversed (records[1:]))])
  70. client.post (answer,
  71. reply_to = models.AppBskyFeedPost.ReplyRef (
  72. parent = records[0]['strong_ref'],
  73. root = records[-1]['strong_ref']))
  74. for datum in [e for e in get_nico_deerjika ()
  75. if e['contentId'] not in watched_videos]:
  76. watched_videos += [datum['contentId']]
  77. embed_external = models.AppBskyEmbedExternal.Main (
  78. external = models.AppBskyEmbedExternal.External (
  79. title = datum['title'],
  80. description = datum['description'],
  81. uri = f"https://www.nicovideo.jp/watch/{ datum['contentId'] }"))
  82. client.post (Talk.main (f"""
  83. ニコニコに『{ datum['title'] }』という動画がアップされました。
  84. つけられたタグは「{ '」、「'.join (datum['tags']) }」です。
  85. 概要には次のように書かれています:
  86. ```html
  87. { datum['description'] }
  88. ```
  89. このことについて、みんなに告知するとともに、ニジカちゃんの感想を教えてください。 """),
  90. embed = embed_external)
  91. if now.hour == 14 and has_got_snack_time:
  92. has_got_snack_time = False
  93. if now.hour == 15 and not has_got_snack_time:
  94. try:
  95. with open ('./assets/snack-time.jpg', 'rb') as f:
  96. image = models.AppBskyEmbedImages.Image (
  97. alt = ('左に喜多ちゃん、右に人面鹿のニジカが'
  98. 'V字に並んでいる。'
  99. '喜多ちゃんは右手でピースサインをして'
  100. '片目をウインクしている。'
  101. 'ニジカは両手を広げ、'
  102. '右手にスプーンを持って'
  103. 'ポーズを取っている。'
  104. '背景には'
  105. '赤と黄色の放射線状の模様が広がり、'
  106. '下部に「おやつタイムだ!!!!」という'
  107. '日本語のテキストが表示されている。'),
  108. image = client.com.atproto.repo.upload_blob (f).blob)
  109. client.post (Talk.main ('おやつタイムだ!!!!'),
  110. embed = models.app.bsky.embed.images.Main (
  111. images = [image]))
  112. last_posted_at = now
  113. except Exception:
  114. pass
  115. has_got_snack_time = True
  116. if now - last_posted_at >= timedelta (hours = 6):
  117. client.post (Talk.main ('今どうしてる?'))
  118. last_posted_at = now
  119. time.sleep (60)
  120. def get_nico_deerjika ():
  121. URL = ('https://snapshot.search.nicovideo.jp/api/v2/snapshot/video'
  122. '/contents/search')
  123. now = datetime.now ()
  124. base = now - timedelta (hours = 1)
  125. params = { 'q': '伊地知ニジカ',
  126. 'targets': 'tags',
  127. '_sort': '-startTime',
  128. 'fields': 'contentId,title,description,tags,startTime',
  129. '_limit': 20,
  130. 'jsonFilter': json.dumps ({ 'type': 'or',
  131. 'filters': [{
  132. 'type': 'range',
  133. 'field': 'startTime',
  134. 'from': ('%04d-%02d-%02dT%02d:%02d:00+09:00'
  135. % (base.year, base.month, base.day,
  136. base.hour, base.minute)),
  137. 'to': ('%04d-%02d-%02dT23:59:59+09:00'
  138. % (now.year, now.month, now.day)),
  139. 'include_lower': True }] }) }
  140. res = requests.get (URL, params = params).json ()
  141. data = []
  142. for datum in res['data']:
  143. datum['tags'] = datum['tags'].split ()
  144. data.append (datum)
  145. return data
  146. if __name__ == '__main__':
  147. main (*sys.argv[1:])