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

main.py 7.6 KiB

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