diff --git a/apigetpost.py b/apigetpost.py index bbd594ca413..0bbcf5f8641 100644 --- a/apigetpost.py +++ b/apigetpost.py @@ -16,6 +16,7 @@ def __init__(self): self.owner_url = None self.owner_name = None self.owner_rep = None + self.owner_acct_id = None self.title = None self.body = None self.body_markdown = None @@ -33,7 +34,12 @@ def as_dict(self): 'title': self.title, 'body': self.body, 'body_markdown': self.body_markdown, - 'owner': {'display_name': self.owner_name, 'link': self.owner_url, 'reputation': self.owner_rep}, + 'owner': { + 'display_name': self.owner_name, + 'link': self.owner_url, + 'reputation': self.owner_rep, + 'account_id': self.owner_acct_id + }, 'site': self.site, 'question_id': self.post_id, 'link': self.post_url, @@ -82,6 +88,7 @@ def api_get_post(post_url): post_data.owner_name = html.unescape(item['owner']['display_name']) post_data.owner_url = item['owner']['link'] post_data.owner_rep = item['owner']['reputation'] + post_data.owner_acct_id = item['owner']['account_id'] else: post_data.owner_name = "" post_data.owner_url = "" diff --git a/chatcommands.py b/chatcommands.py index f80f1f42e2d..32e8114708b 100644 --- a/chatcommands.py +++ b/chatcommands.py @@ -71,13 +71,87 @@ def addblu(msg, user): message_url = "https://chat.{}/transcript/{}?m={}".format(msg._client.host, msg.room.id, msg.id) add_blacklisted_user((uid, val), message_url, "") - return "User blacklisted (`{}` on `{}`).".format(uid, val) + return f"User blacklisted {'network-wide ' if val == 'stackexchange.com' else ''}(`{uid}` on `{val}`)." elif int(uid) == -2: raise CmdException("Error: {}".format(val)) else: raise CmdException("Invalid format. Valid format: `!!/addblu profileurl` *or* `!!/addblu userid sitename`.") +def user_to_net_acct_id(user: tuple[str, str]) -> str: + """Find the network account ID of a user (e.g., a site profile)""" + uid, val = user + + # SE user passed + if val == "stackexchange.com": + return uid + # network user passed + else: + # based on the allspam code + with GlobalVars.api_request_lock: + if GlobalVars.api_backoff_time > time.time(): + time.sleep(GlobalVars.api_backoff_time - time.time() + 2) + request_url = get_se_api_url_for_route(f"users/{uid}") + params = get_se_api_default_params({ + 'filter': '!)scNT2zLcbOpD09vBxHM', + 'site': val + }) + res = requests.get(request_url, params=params, timeout=GlobalVars.default_requests_timeout).json() + if "backoff" in res: + if GlobalVars.api_backoff_time < time.time() + res["backoff"]: + GlobalVars.api_backoff_time = time.time() + res["backoff"] + if 'items' not in res or len(res['items']) == 0: + raise CmdException("The specified user does not appear to exist.") + return str(res['items'][0]['account_id']) + + +def is_user_net_blacklisted(user: tuple[str, str]) -> bool: + """Given a user ID and a site, return if they are network-blacklisted""" + return (user_to_net_acct_id(user), 'stackexchange.com') in GlobalVars.blacklisted_users + + +@command(str, whole_msg=True, privileged=True) +def addnetblu(msg, user): + """ + Adds a user to the network-wide blacklist + :return: A string + """ + uid, val = get_user_from_list_command(user) + + if int(uid) > -1 and val != "": + message_url = "https://chat.{}/transcript/{}?m={}".format(msg._client.host, msg.room.id, msg.id) + net_id: str = user_to_net_acct_id((uid, val)) + add_blacklisted_user((net_id, "stackexchange.com"), message_url, "") + return f"User blacklisted network-wide (`{net_id}`)." + elif int(uid) == -2: + raise CmdException("Error: {}".format(val)) + else: + raise CmdException("Invalid format. Valid format: `!!/addnetblu profileurl` *or* " + "`!!/addnetblu userid stackexchange.com`.") + + +@command(str, privileged=True) +def rmnetblu(user): + """ + Removes user from the network-wide blacklist + :param user: + :return: A string + """ + uid, val = get_user_from_list_command(user) + + if int(uid) > -1 and val != "": + net_id: str = user_to_net_acct_id((uid, val)) + if remove_blacklisted_user((net_id, "stackexchange.com")): + return f"The user has been removed from the network-wide user-blacklist (`{net_id}`)." + else: + return "The user is not network-wide blacklisted." + elif int(uid) == -2: + raise CmdException("Error: {}".format(val)) + else: + raise CmdException("Invalid format. Valid format: `!!/rmnetblu profileurl` " + "*or* `!!/rmnetblu userid stackexchange.com`.") + + # noinspection PyIncorrectDocstring,PyMissingTypeHints @command(str) def isblu(user): @@ -90,10 +164,26 @@ def isblu(user): uid, val = get_user_from_list_command(user) if int(uid) > -1 and val != "": - if is_blacklisted_user((uid, val)): - return "User is blacklisted (`{}` on `{}`).".format(uid, val) + is_site_wide: bool = is_blacklisted_user((uid, val)) + is_network_wide: bool = is_user_net_blacklisted((uid, val)) + is_se_acct: bool = val == "stackexchange.com" + # It could check what sites a user is blacklisted on, even if that + # site isn't passed. That functionality could be useful, but I didn't + # write the code to do that. + if is_se_acct and is_network_wide: + return "User is blacklisted network-wide" + elif is_se_acct and not is_network_wide: + return "User is not blacklisted network-wide" + elif is_site_wide and is_network_wide: + return f"User is blacklisted both on `{val}` (`{uid}`) and network-wide" + elif is_site_wide and not is_network_wide: + return f"User is blacklisted on `{val}` (`{uid}`), but not network-wide" + elif not is_site_wide and is_network_wide: + return f"User is blacklisted network-wide, but not on `{val}` (`{uid}`)" + elif not is_site_wide and not is_network_wide: + return f"User is neither blacklisted on `{val}` (`{uid}`) nor network-wide" else: - return "User is not blacklisted (`{}` on `{}`).".format(uid, val) + return "This should never be reached" elif int(uid) == -2: raise CmdException("Error: {}".format(val)) else: @@ -111,8 +201,9 @@ def rmblu(user): uid, val = get_user_from_list_command(user) if int(uid) > -1 and val != "": + net_wide: str = "network-wide " if (val == "stackexchange.com") else "" if remove_blacklisted_user((uid, val)): - return "The user has been removed from the user-blacklist (`{}` on `{}`).".format(uid, val) + return "The user has been removed from the {}user-blacklist (`{}` on `{}`).".format(net_wide, uid, val) else: return "The user is not blacklisted. Perhaps they have already been removed from the blacklist. Please " \ "see: [Blacklists, watchlists, and the user-whitelist: User-blacklist and user-whitelist]" \ diff --git a/classes/_Post.py b/classes/_Post.py index f9005ce0208..07ec4c0e093 100644 --- a/classes/_Post.py +++ b/classes/_Post.py @@ -2,7 +2,7 @@ import json from helpers import log import html -from typing import AnyStr, Union +from typing import AnyStr, Union, Optional class PostParseError(Exception): @@ -31,6 +31,7 @@ def __init__(self, json_data=None, api_response=None, parent=None): self._user_url = "" self._votes = {'downvotes': None, 'upvotes': None} self._edited = False + self._owner_account_id = None if parent is not None: if not isinstance(parent, Post): @@ -143,7 +144,8 @@ def _parse_api_post(self, response): 'owner': { 'display_name': '_user_name', 'link': '_user_url', - 'reputation': '_owner_rep' + 'reputation': '_owner_rep', + 'account_id': '_owner_account_id' }, 'question_id': '_post_id', 'answer_id': '_post_id', @@ -221,6 +223,9 @@ def post_id(self): def post_score(self): return int(self._post_score) + def get_account_id(self) -> Optional[int]: + return int(self._owner_account_id) if self._owner_account_id is not None else None + @property def post_site(self): if type(self._post_site) in [bytes, bytearray]: diff --git a/globalvars.py b/globalvars.py index 632cf9eb935..0f07c8f5b77 100644 --- a/globalvars.py +++ b/globalvars.py @@ -173,7 +173,7 @@ class GlobalVars: # accurately from the correct route. However, for scanning posts, we also want the question data associated # with an answer. se_api_question_answer_post_filter = \ - "!scxINmJWAnjuckNJkP9YfbG196DrCj)DDXQjRKUGfp-QzZ3b1nRh1Qitw7)FdyW_)iLrL8TBxFYZ1" + "!*b8DH81UdVw)kQyIDQMTMc9jrRZh7*x*46g(fPTyFh)C2d3Fp9kSnnFDHyiG5L3cG39qg66pgP63xLa74Uq" se_api_url_base = "https://api.stackexchange.com/2.4/" se_api_default_params = { 'key': 'IAkbitmze4B8KpacUfLqkw((', diff --git a/spamhandling.py b/spamhandling.py index 764fb91d2b9..37b11d7ad0a 100644 --- a/spamhandling.py +++ b/spamhandling.py @@ -43,10 +43,19 @@ def sum_weight(reasons: list): return s +def is_post_owner_net_blacklisted(post: Post) -> bool: + """Check if the owner of a post is network-wide blacklisted""" + for acct_id, site in GlobalVars.blacklisted_users.keys(): + if int(acct_id) == post.get_account_id() and site == "stackexchange.com": + return True + return False + + # noinspection PyMissingTypeHints def check_if_spam(post, dont_ignore_for=None): test, why = findspam.FindSpam.test_post(post) - if datahandling.is_blacklisted_user(parsing.get_user_from_url(post.user_url)): + net_blacklisted: bool = is_post_owner_net_blacklisted(post) + if datahandling.is_blacklisted_user(parsing.get_user_from_url(post.user_url)) or net_blacklisted: test.append("blacklisted user") blacklisted_user_data = datahandling.get_blacklisted_user_data(parsing.get_user_from_url(post.user_url)) if len(blacklisted_user_data) > 1: @@ -62,6 +71,8 @@ def check_if_spam(post, dont_ignore_for=None): ms_url = datahandling.resolve_ms_link(rel_url) or to_metasmoke_link(rel_url) why += "\nBlacklisted user - blacklisted for {} ({}) by {}".format( blacklisted_post_url, ms_url, blacklisted_by) + if net_blacklisted: + why += f"\nNetwork-wide blacklisted user - blacklisted by {blacklisted_by}" else: why += "\n" + u"Blacklisted user - blacklisted by {}".format(blacklisted_by) if test: diff --git a/test/test_chatcommands.py b/test/test_chatcommands.py index af2ac8bf64b..8ee7631b0fe 100644 --- a/test/test_chatcommands.py +++ b/test/test_chatcommands.py @@ -589,16 +589,16 @@ def test_blacklisted_users(): # Format: !!/*blu profileurl assert chatcommands.isblu("https://stackoverflow.com/users/4622463/angussidney", original_msg=msg) == \ - "User is not blacklisted (`4622463` on `stackoverflow.com`)." + "User is neither blacklisted on `stackoverflow.com` (`4622463`) nor network-wide" assert chatcommands.addblu("https://stackoverflow.com/users/4622463/angussidney", original_msg=msg) == \ "User blacklisted (`4622463` on `stackoverflow.com`)." # TODO: Edit command to check and not blacklist again, add test assert chatcommands.isblu("https://stackoverflow.com/users/4622463/angussidney", original_msg=msg) == \ - "User is blacklisted (`4622463` on `stackoverflow.com`)." + "User is blacklisted on `stackoverflow.com` (`4622463`), but not network-wide" assert chatcommands.rmblu("https://stackoverflow.com/users/4622463/angussidney", original_msg=msg) == \ "The user has been removed from the user-blacklist (`4622463` on `stackoverflow.com`)." assert chatcommands.isblu("https://stackoverflow.com/users/4622463/angussidney", original_msg=msg) == \ - "User is not blacklisted (`4622463` on `stackoverflow.com`)." + "User is neither blacklisted on `stackoverflow.com` (`4622463`) nor network-wide" assert chatcommands.rmblu("https://stackoverflow.com/users/4622463/angussidney", original_msg=msg) == \ "The user is not blacklisted. Perhaps they have already been removed from the blacklist. Please " \ "see: [Blacklists, watchlists, and the user-whitelist: User-blacklist and user-whitelist]" \ @@ -608,16 +608,16 @@ def test_blacklisted_users(): # Format: !!/*blu userid sitename assert chatcommands.isblu("4622463 stackoverflow", original_msg=msg) == \ - "User is not blacklisted (`4622463` on `stackoverflow.com`)." + "User is neither blacklisted on `stackoverflow.com` (`4622463`) nor network-wide" assert chatcommands.addblu("4622463 stackoverflow", original_msg=msg) == \ "User blacklisted (`4622463` on `stackoverflow.com`)." # TODO: Add test here as well assert chatcommands.isblu("4622463 stackoverflow", original_msg=msg) == \ - "User is blacklisted (`4622463` on `stackoverflow.com`)." + "User is blacklisted on `stackoverflow.com` (`4622463`), but not network-wide" assert chatcommands.rmblu("4622463 stackoverflow", original_msg=msg) == \ "The user has been removed from the user-blacklist (`4622463` on `stackoverflow.com`)." assert chatcommands.isblu("4622463 stackoverflow", original_msg=msg) == \ - "User is not blacklisted (`4622463` on `stackoverflow.com`)." + "User is neither blacklisted on `stackoverflow.com` (`4622463`) nor network-wide" assert chatcommands.rmblu("4622463 stackoverflow", original_msg=msg) == \ "The user is not blacklisted. Perhaps they have already been removed from the blacklist. Please " \ "see: [Blacklists, watchlists, and the user-whitelist: User-blacklist and user-whitelist]" \