Skip to content

Instantly share code, notes, and snippets.

@called-d

called-d/anchor_bot.py

Last active Oct 19, 2018
Embed
What would you like to do?
ネタで作った安価bot (https://mstdn-workers.com/@anchor_bot で稼働中)
from mastodon import Mastodon
import os
import re
import html
from textwrap import dedent
from mastodon import StreamListener
param_envs = "CLIENT_ID", "CLIENT_SECRET", "ACCESS_TOKEN", "API_BASE_URL"
mstdn = Mastodon(**{name.lower(): os.getenv(name) for name in param_envs})
DEBUG=False
class Target:
def __init__(self, n):
self.n = n
self.n_original = n
self.status = None
def hit_status(self, status):
self.status = status
class AnchorContainer:
def __init__(self, status, num_list):
self.status = status
self.targets = [Target(n) for n in num_list]
class WaitingRemove:
def __init__(self, status, n=100):
self.status = status
self.n = n
if 'anchors' not in globals():
anchors = []
if 'remove_list' not in globals():
remove_list = []
def remove_tag(html):
return re.sub(r"<[^>]+?>", '', html)
def to_oneline(html):
return html.replace("<br />", ' ').replace("</p><p>", ' ').replace('\n', '\\n')
def remove_mention(content):
return content.replace("@", "@")
def remove_hashtag(content):
return content.replace("#", "#")
def remove_image(content, status):
for media in status.media_attachments:
content = content.replace(media.text_url, "[画像]")
return content
def get_anchor(content):
return [int(n_str) for n_str in re.findall(r">>(-?\d+)", content)]
def get_anchor_from_status(status):
content = html.unescape(remove_tag(status['content']))
return get_anchor(content)
def append_anchor_from_status(status):
anchor_nums = sorted(set(get_anchor_from_status(status)))
if anchor_nums:
if all(0 < n <= 1000 for n in anchor_nums):
anchors.append(AnchorContainer(status, anchor_nums))
post_you_got_it(status, anchor_nums)
else:
post_anchor_out_of_range(status)
def decrement_anchors():
for a in anchors:
for t in a.targets:
t.n -= 1
def decrement_remove():
for r in remove_list:
r.n -= 1
def get_status_str(status):
content = html.unescape(remove_tag(to_oneline(status.content)))
content = remove_mention(content)
content = remove_hashtag(content)
content = remove_image(content, status)
return f'> "{content}"' if len(content) < 20 else status.url
def post_you_re_anchored_and_anchor_hit(anchor, display_all=False):
lines = [get_status_str(anchor.status) + " の"]
for t in anchor.targets:
if t.status:
target_is_near = t.n > -15
if target_is_near or display_all:
lines.extend([
f">>{t.n_original}は次の投稿となりました",
get_status_str(t.status)
])
if len(lines) < 4:
status = mstdn.status_post("\n".join(lines), visibility="private" if DEBUG else "public")
else:
status = mstdn.status_post(
spoiler_text="\n".join(lines[:3]) + "...",
status="\n".join(lines[3:]),
visibility="private" if DEBUG else "public"
)
remove_list.append(WaitingRemove(
status,
100))
def post_anchor_out_of_range(status):
remove_list.append(WaitingRemove(
mstdn.status_post(dedent(f"""\
@{status['account']['username']} {status.url} 安価は 0 < n <= 1000 で指定してください"""),
visibility="private"),
100))
def post_you_got_it(status, anchor_nums):
remove_list.append(WaitingRemove(
mstdn.status_post(dedent(f"""
{status.url}{', '.join(map(str, anchor_nums))}個後の投稿をピックアップします
(幾つかのbotの投稿はスキップされます)"""),
visibility="private" if DEBUG else "public"),
10))
def post_for_zero(status):
for a in anchors:
for t in a.targets:
if t.n == 0:
t.hit_status(status)
if all(t.n <= 0 for t in a.targets):
anchors.remove(a)
post_you_re_anchored_and_anchor_hit(a, display_all=True)
elif any(t.n == 0 for t in a.targets):
done_targets = [t for t in a.targets if t.n < 0]
undone_targets = [t for t in a.targets if t.n > 0]
prev_target_n = max(t.n for t in done_targets) if done_targets else -20
next_target_n = min(t.n for t in undone_targets) if undone_targets else 20
target_is_near = prev_target_n > -15 or next_target_n < 15
if target_is_near: continue
post_you_re_anchored_and_anchor_hit(a)
def remove_bot_statuses():
for r in [r for r in remove_list if r.n == 0]:
remove_list.remove(r)
status_current = mstdn.status(r.status.id)
if status_current.favourites_count or status_current.reblogs_count:
pass
else:
mstdn.status_delete(r.status.id)
def remove_anchor_by_status_id(status_id):
for a in [a for a in anchors if a.status.id == status_id]:
anchors.remove(a)
def is_some_bots(status):
app = status['application']
if not app: return False
bot_names = [
'オフ会カレンダー',
'off_bot',
'安価bot',
'不適切bot',
"トレンドbot",
'nekonyanApp',
'色bot',
'ダイスbot',
]
return app['name'] in bot_names
class AnchorListener(StreamListener):
def on_update(self, status):
if is_some_bots(status): return
decrement_anchors()
decrement_remove()
post_for_zero(status)
append_anchor_from_status(status)
remove_bot_statuses()
def on_delete(self, status_id):
remove_anchor_by_status_id(status_id)
listener = AnchorListener()
mstdn.stream_local(listener)
convert -size 300x300 canvas:"#d9e1e8" -fill "#9baec8" -gravity Center -pointsize 50 -annotate 0 ">>42" icon.png
@called-d

This comment has been minimized.

Copy link
Owner Author

@called-d called-d commented Feb 5, 2018

安価bot

当botの運用とコードは現状有姿にて提供される

2018/02/05 16:46 廃止要求により停止。再開は様子を見る

@called-d

This comment has been minimized.

Copy link
Owner Author

@called-d called-d commented Feb 6, 2018

廃止要求意図の聞き取りと改善要望を募り
2018/02/06 15:33 誰かを呼び出さないよう改修し再開
廃止要求あるいはLTLが再び地雷原になるようであれば今度こそ完全に廃止する

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.