2018-10-09 01:11:51 +00:00
|
|
|
#!/usr/bin/env python3
|
2021-07-26 04:52:13 +00:00
|
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
2018-10-09 01:11:51 +00:00
|
|
|
|
2021-06-16 03:49:34 +00:00
|
|
|
import re
|
|
|
|
import anyio
|
|
|
|
import pleroma
|
|
|
|
import contextlib
|
2021-07-26 04:52:13 +00:00
|
|
|
from third_party import utils
|
2018-10-09 01:11:51 +00:00
|
|
|
|
2021-06-16 03:49:34 +00:00
|
|
|
def parse_args():
|
2021-07-26 04:52:13 +00:00
|
|
|
return utils.arg_parser_factory(description='Reply service. Leave running in the background.').parse_args()
|
2021-06-16 03:49:34 +00:00
|
|
|
|
|
|
|
class ReplyBot:
|
|
|
|
def __init__(self, cfg):
|
|
|
|
self.cfg = cfg
|
|
|
|
self.pleroma = pleroma.Pleroma(access_token=cfg['access_token'], api_base_url=cfg['site'])
|
|
|
|
|
|
|
|
async def run(self):
|
|
|
|
async with self.pleroma as self.pleroma:
|
|
|
|
self.me = (await self.pleroma.me())['id']
|
|
|
|
self.follows = frozenset(user['id'] for user in await self.pleroma.following(self.me))
|
|
|
|
async for notification in self.pleroma.stream_mentions():
|
|
|
|
await self.process_notification(notification)
|
|
|
|
|
|
|
|
async def process_notification(self, notification):
|
|
|
|
acct = "@" + notification['account']['acct'] # get the account's @
|
|
|
|
post_id = notification['status']['id']
|
|
|
|
context = await self.pleroma.status_context(post_id)
|
|
|
|
|
2022-12-23 08:36:57 +00:00
|
|
|
# check if we've already been participating in this thread
|
|
|
|
if self.check_hellthread_size(notification):
|
|
|
|
return
|
|
|
|
|
2021-06-16 03:49:34 +00:00
|
|
|
# check if we've already been participating in this thread
|
|
|
|
if self.check_thread_length(context):
|
|
|
|
return
|
|
|
|
|
2022-07-05 08:17:55 +00:00
|
|
|
content = self.extract_toot(notification['status']['content'], self.cfg)
|
2022-10-21 04:30:09 +00:00
|
|
|
if content.startswith('reply '):
|
|
|
|
command = content.split(' ')
|
|
|
|
await self.process_command(context, notification, command[0], command[1])
|
|
|
|
elif content in {'pin', 'unpin'}:
|
|
|
|
await self.process_command(context, notification, content, '')
|
2021-06-16 03:49:34 +00:00
|
|
|
else:
|
|
|
|
await self.reply(notification)
|
|
|
|
|
2022-12-23 08:36:57 +00:00
|
|
|
def check_hellthread_size(self, notification) -> bool:
|
|
|
|
"""return whether the notification should not be replied to due to sheer amount of mentions"""
|
|
|
|
if len(notification['status']['mentions']) >= self.cfg['hellthread_threshold']:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
2022-12-23 08:42:06 +00:00
|
|
|
def check_thread_length(self, context) -> bool:
|
2021-06-16 03:49:34 +00:00
|
|
|
"""return whether the thread is too long to reply to"""
|
|
|
|
posts = 0
|
|
|
|
for post in context['ancestors']:
|
|
|
|
if post['account']['id'] == self.me:
|
|
|
|
posts += 1
|
|
|
|
if posts >= self.cfg['max_thread_length']:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
2022-10-21 04:30:09 +00:00
|
|
|
async def process_command(self, context, notification, command, argument):
|
2021-06-16 03:49:34 +00:00
|
|
|
post_id = notification['status']['id']
|
|
|
|
if notification['account']['id'] not in self.follows: # this user is unauthorized
|
|
|
|
await self.pleroma.react(post_id, '❌')
|
|
|
|
return
|
|
|
|
|
|
|
|
# find the post the user is talking about
|
|
|
|
for post in context['ancestors']:
|
|
|
|
if post['id'] == notification['status']['in_reply_to_id']:
|
|
|
|
target_post_id = post['id']
|
|
|
|
|
|
|
|
try:
|
2022-10-21 04:30:09 +00:00
|
|
|
if command in ('pin', 'unpin'):
|
|
|
|
await (self.pleroma.pin if command == 'pin' else self.pleroma.unpin)(target_post_id)
|
|
|
|
elif command == 'reply':
|
|
|
|
toot = await utils.make_post(self.cfg)
|
|
|
|
toot = self.cleanup_toot(toot, self.cfg)
|
|
|
|
status = await self.pleroma.get_status(argument)
|
|
|
|
await self.pleroma.reply(status, toot, cw=self.cfg['cw'])
|
2021-06-16 03:49:34 +00:00
|
|
|
except pleroma.BadRequest as exc:
|
|
|
|
async with anyio.create_task_group() as tg:
|
|
|
|
tg.start_soon(self.pleroma.react, post_id, '❌')
|
|
|
|
tg.start_soon(self.pleroma.reply, notification['status'], 'Error: ' + exc.args[0])
|
|
|
|
else:
|
|
|
|
await self.pleroma.react(post_id, '✅')
|
|
|
|
|
|
|
|
async def reply(self, notification):
|
2022-10-21 04:30:09 +00:00
|
|
|
toot = await utils.make_post(self.cfg)
|
|
|
|
toot = self.cleanup_toot(toot, self.cfg)
|
2021-06-16 03:49:34 +00:00
|
|
|
await self.pleroma.reply(notification['status'], toot, cw=self.cfg['cw'])
|
|
|
|
|
2022-10-21 04:30:09 +00:00
|
|
|
@staticmethod
|
|
|
|
def cleanup_toot(text, cfg):
|
|
|
|
PAIRED_PUNCTUATION = re.compile(r"[{}]".format(re.escape('[](){}"‘’“”«»„')))
|
|
|
|
if cfg['strip_paired_punctuation']:
|
|
|
|
text = PAIRED_PUNCTUATION.sub("", text)
|
|
|
|
text = text.replace("@", "@\u200b") # sanitize mentions
|
|
|
|
text = utils.remove_mentions(cfg, text)
|
|
|
|
return text
|
|
|
|
|
2021-06-16 03:49:34 +00:00
|
|
|
@staticmethod
|
2022-07-05 08:17:55 +00:00
|
|
|
def extract_toot(toot, cfg):
|
2022-07-04 21:31:23 +00:00
|
|
|
text = utils.extract_post_content(toot)
|
2021-06-16 03:49:34 +00:00
|
|
|
text = re.sub(r"^@\S+\s", r"", text) # remove the initial mention
|
2022-10-21 04:30:09 +00:00
|
|
|
# text = text.lower() # treat text as lowercase for easier keyword matching (if this bot uses it)
|
2021-06-16 03:49:34 +00:00
|
|
|
return text
|
|
|
|
|
|
|
|
async def amain():
|
|
|
|
args = parse_args()
|
2021-07-26 04:52:13 +00:00
|
|
|
cfg = utils.load_config(args.cfg)
|
2021-06-16 03:49:34 +00:00
|
|
|
await ReplyBot(cfg).run()
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
with contextlib.suppress(KeyboardInterrupt):
|
|
|
|
anyio.run(amain)
|