diff --git a/components/evaluators.py b/components/evaluators.py index 20e4fd7..d5a8619 100755 --- a/components/evaluators.py +++ b/components/evaluators.py @@ -25,8 +25,6 @@ class PostEvaluator(Evaluator): trigger_urls = [ *filter(self.url_blacklist_re.match, self._url_extractor.find_urls(text, only_unique=True)) ] if self.url_blacklist_re and text else [] - trigger_entries = [ - _format_match(entry) for entry in re.finditer(self.blacklist_re, text) - ] if self.blacklist_re and text else [] + trigger_entries = re.findall(self.blacklist_re, text) if self.blacklist_re and text else [] logging.debug(f'Evaluated text:{text}\ntrigger urls:{trigger_urls}\ntrigger entries:{trigger_entries}') return trigger_urls, trigger_entries diff --git a/components/notifiers.py b/components/notifiers.py index c5b454c..d1bf365 100644 --- a/components/notifiers.py +++ b/components/notifiers.py @@ -1,17 +1,29 @@ import subprocess +from os import getcwd from abc import ABC, abstractmethod - class Notifier(ABC): @abstractmethod def notify(self, title, content, *args, **kwargs): raise NotImplementedError - class TermuxNotifier(Notifier): def notify(self, title, content, *args, **kwargs): - subprocess.call(['termux-notification', '--title', title, '--content', content]) + args = ['termux-notification', '--title', title, + '--content', content or 'No Message'] + + #open link when clicking on notification + if 'link' in kwargs: + args = args + ['--action', f'termux-open-url {kwargs["link"]}'] + + #add buttons to the notification + if 'buttons' in kwargs: + post = kwargs["post"] + for i, button in enumerate(kwargs["buttons"], start=1): + args = args + [f'--button{i}', button["text"], + f'--button{i}-action', f'python3 {getcwd()}/notification_button.py -b {post["board"]} -p {post["postId"]} -a {button["actions"]}'] + subprocess.call(args) class NotifySendNotifier(Notifier): def notify(self, title, content, *args, **kwargs): diff --git a/components/watchers.py b/components/watchers.py index 7ad5dbf..a5cde87 100644 --- a/components/watchers.py +++ b/components/watchers.py @@ -5,9 +5,9 @@ from threading import Thread, Event import socketio from requests import RequestException +def get_quote(post): return f'>>>/{post["board"]}/{post["thread"] or post["postId"]} ({post["postId"]})' -def get_path(post): return f'>>>/{post["board"]}/{post["thread"] or post["postId"]} ({post["postId"]})' - +def get_manage_path(post): return f'/{post["board"]}/manage/thread/{post["thread"] or post["postId"]}.html#{post["postId"]}' class Watcher(ABC, Thread): def __init__(self, session): @@ -35,18 +35,22 @@ class RecentWatcher(Watcher): def connect(): logging.debug(f'Live posts client connected') client.emit('room', f'{board}-manage-recent-hashed' if board else 'globalmanage-recent-hashed') - notify(f'Connected', f'Watching live posts') @client.event def disconnect(): logging.error(f'Live posts client disconnected') - notify(f'Lost live posts connection', f'Retrying in {reconnection_delay} seconds') @client.on('newPost') def on_new_post(post): urls, entries = evaluate(post["nomarkup"]) if urls or entries: - notify(f'Alert! {get_path(post)}', '\n'.join(urls) + '\n'.join(entries)) + post_url=f'{session.imageboard_url}{get_manage_path(post)}' + buttons=[{"text":"Delete","actions":"delete"}, + {"text":"Delete+Ban" if board else "Delete+Global Ban","actions":"delete,ban" if board else "delete,global_ban"}] + #todo: add this last button even if a board recents, because global staff can still global ban. but need a way to + #check if the account is global staff, which we dont have a json endpoint for in jschan yet. + #{"text":"Delete+Global Ban","actions":"dismiss" if board else "global_dismiss"}] + notify(f'Alert! {get_quote(post)}\n', post['nomarkup'], link=post_url, post=post, buttons=buttons) self.client = client self.start() @@ -66,6 +70,7 @@ class ReportsWatcher(Watcher): self.notify = notify self.fetch_interval = fetch_interval + self.board = board self._endpoint = f'{session.imageboard_url}/{f"{board}/manage" if board else "globalmanage"}/reports.json' self.known_reports = 0 @@ -80,15 +85,19 @@ class ReportsWatcher(Watcher): try: reported_posts, num_reported_posts = self.fetch_reports() if 0 < num_reported_posts != self.known_reports: - self.notify(f'New reports!', "\n".join([ - f'{get_path(p)} {[r["reason"] for r in (p["globalreports"] if "globalreports" in p else p["reports"])]}' - for p in reported_posts])) + for p in reported_posts: + post_url=f'{self.session.imageboard_url}{get_manage_path(p)}' + #todo: allow to customise these buttons somewhere + buttons=[{"text":"Delete","actions":"delete"}, + {"text":"Delete+Ban" if self.board else "Delete+Global Ban","actions":"delete,ban" if self.board else "delete,global_ban"}, + {"text":"Dismiss","actions":"dismiss" if self.board else "global_dismiss"}] + self.notify(f'New reports!', "\n".join([f'{get_quote(p)} {[r["reason"] for r in (p["globalreports"] if "globalreports" in p else p["reports"])]}']), + link=post_url, post=p, buttons=buttons) self.known_reports = num_reported_posts except RequestException as e: logging.error(f'Exception {e} occurred while fetching reports') - self.notify(f'Error while fetching reports', f'Trying to reconnect') if self._stp.wait(self.fetch_interval): logging.info("Exiting reports watcher") diff --git a/notification_button.py b/notification_button.py new file mode 100755 index 0000000..ebe60aa --- /dev/null +++ b/notification_button.py @@ -0,0 +1,40 @@ +import logging +import subprocess + +import sys, getopt +from config import config +from session import ModSession + +def main(argv): + logging.basicConfig(level=logging.DEBUG, + format='[%(asctime)s %(funcName)s] %(message)s (%(name)s)') + + try: + opts, args = getopt.getopt(argv, 'b:p:a:', ['board=', 'postid=', 'actions=']) + optdict = dict(opts) + except getopt.GetoptError: + logging.error('invalid arguments') + sys.exit(1) + + session = ModSession(imageboard=config.IMAGEBOARD, username=config.ACCOUNT_USERNAME, + password=config.ACCOUNT_PASSWORD, retries=config.REQUEST_RETRIES, + timeout=config.REQUEST_TIMEOUT, backoff_factor=config.RETRIES_BACKOFF_FACTOR) + + session.update_csrf() + + res = session.post_actions(board=optdict['-b'], postid=optdict['-p'], actions=optdict['-a']) + + toast_message = None + if 'message' in res: + toast_message = res['message'] + elif 'messages' in res: + toast_message = "\n".join(res['messages']) + elif 'error' in res: + toast_message = res['error'] + elif 'errors' in res: + toast_message = "\n".join(res['errors']) + if toast_message: + subprocess.call(['termux-toast', toast_message]) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/session.py b/session.py index 4924922..617f3cd 100644 --- a/session.py +++ b/session.py @@ -12,6 +12,7 @@ class ModSession(Session): self.imageboard = imageboard self.imageboard_url = f"https://{imageboard}" self.auth_params = {'username': username, 'password': password} + self.csrf_token = None # overwrites session default behaviour self.mount(self.imageboard_url, HTTPAdapter(max_retries=Retry(total=retries, backoff_factor=backoff_factor))) @@ -32,3 +33,29 @@ class ModSession(Session): except requests.RequestException as e: # ambiguous catch but atm nothing can be done in more specific cases logging.error(f'Exception {e} occurred while authenticating moderator') raise Exception('Unable to authenticate moderator') + + def update_csrf(self): + try: + res = self.get(url=f'{self.imageboard_url}/csrf.json', + headers={'Referer': f'{self.imageboard_url}/csrf.json'}).json() + if 'token' in res: + self.csrf_token = res['token'] + else: + raise Exception('Unable to update csrf token') + except requests.RequestException as e: + logging.error(f'Exception {e} occurred while updating csrf token') + raise Exception('Unable to update csrf token') + + def post_actions(self, **kwargs): + try: + actions = kwargs['actions'].split(',') + body = {'checkedposts':kwargs["postid"],'_csrf':self.csrf_token,'log_message':'globalafk'} + for action in actions: + body[action] = '1' + res = self.post(url=f'{self.imageboard_url}/forms/board/{kwargs["board"]}/modactions', + headers={'Referer': f'{self.imageboard_url}/forms/board/{kwargs["board"]}/modactions','x-using-xhr': 'true'}, + data=body).json() + return res + except requests.RequestException as e: + logging.error(f'Exception {e} occurred while posting action') + raise Exception('Failed to submit post actions')