def pull(): """ Pull an update from GitHub :return: String on failure, None on success """ if only_blacklists_changed(GitManager.get_remote_diff()): GitManager.pull_remote() load_blacklists() return "No code modified, only blacklists reloaded." else: request = requests.get('https://api.github.com/repos/Charcoal-SE/SmokeDetector/git/refs/heads/deploy') latest_sha = request.json()["object"]["sha"] request = requests.get( 'https://api.github.com/repos/Charcoal-SE/SmokeDetector/commits/{commit_code}/statuses'.format( commit_code=latest_sha)) states = [] for ci_status in request.json(): state = ci_status["state"] states.append(state) if "success" in states: os._exit(3) elif "error" in states or "failure" in states: raise CmdException("CI build failed! :( Please check your commit.") elif "pending" in states or not states: raise CmdException("CI build is still pending, wait until the build has finished and then pull again.")
def true(feedback, msg, alias_used="true"): """ Marks a post as a true positive :param feedback: :param msg: :return: string """ post_data = get_report_data(msg) if not post_data: raise CmdException("That message is not a report.") post_url, owner_url = post_data feedback_type = TRUE_FEEDBACKS[alias_used] feedback_type.send(post_url, feedback) user = get_user_from_url(owner_url) _, _, post_type = fetch_post_id_and_site_from_url(post_url) message_url = "https://chat.{}/transcript/{}?m={}".format(msg._client.host, msg.room.id, msg.id) if user is not None: if feedback_type.blacklist: add_blacklisted_user(user, message_url, post_url) result = "Registered " + post_type + " as true positive and blacklisted user." else: result = "Registered " + post_type + " as true positive. If you want to "\ "blacklist the poster, use `trueu` or `tpu`." return result if not feedback_type.always_silent else ""
def why(msg): """ Returns reasons a post was reported :param msg: :return: A string """ post_data = get_report_data(msg) if not post_data: raise CmdException("That's not a report.") else: *post, _ = fetch_post_id_and_site_from_url(post_data[0]) why_info = get_why(post[1], post[0]) if why_info: return why_info else: raise CmdException("There is no `why` data for that user (anymore).")
def addblu(msg, user): """ Adds a user to site whitelist :param msg: ChatExchange message :param user: :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) add_blacklisted_user((uid, val), message_url, "") return "User blacklisted (`{}` on `{}`).".format(uid, val) elif int(uid) == -2: raise CmdException("Error: {}".format(val)) else: raise CmdException("Invalid format. Valid format: `!!/addblu profileurl` *or* `!!/addblu userid sitename`.")
def blacklist(_): """ Returns a string which explains the usage of the new blacklist commands. :return: A string """ raise CmdException("The !!/blacklist command has been deprecated. " "Please use !!/blacklist-website, !!/blacklist-username," "!!/blacklist-keyword, or perhaps !!/watch-keyword. " "Remember to escape dots in URLs using \\.")
def approve(msg, pr_num): if is_code_privileged(msg._client.host, msg.owner.id): resp = requests.post('{}/github/pr_approve/{}'.format(GlobalVars.metasmoke_host, pr_num)) if resp.status_code == 200: return "Posted approval comment. PR will be merged automatically if it's a blacklist PR." else: return "Forwarding request to metasmoke returned HTTP {}. Check status manually.".format(resp.status_code) else: raise CmdException("You don't have permission to do that.")
def notify(msg, room_id, se_site): """ Subscribe a user to events on a site in a single room :param msg: :param room_id: :param se_site: :return: A string """ # TODO: Add check whether smokey reports in that room response, full_site = add_to_notification_list(msg.owner.id, msg._client.host, room_id, se_site) if response == 0: return "You'll now get pings from me if I report a post on `{site}`, in room "\ "`{room}` on `chat.{domain}`".format(site=se_site, room=room_id, domain=msg._client.host) elif response == -1: raise CmdException("That notification configuration is already registered.") elif response == -2: raise CmdException("The given SE site does not exist.") else: raise CmdException("Unrecognized code returned when adding notification.")
def postgone(msg): """ Removes link from a marked report message :param msg: :return: None """ edited = edited_message_after_postgone_command(msg.content) if edited is None: raise CmdException("That's not a report.") msg.edit(edited)
def do_blacklist(pattern, blacklist_type, msg, force=False): """ Adds a string to the website blacklist and commits/pushes to GitHub :param pattern: :param blacklist_type: :param msg: :param force: :return: A string """ chat_user_profile_link = "http://chat.{host}/users/{id}".format(host=msg._client.host, id=msg.owner.id) # noinspection PyProtectedMember try: regex.compile(pattern) except regex._regex_core.error: raise CmdException("An invalid pattern was provided, not blacklisting.") if not force: reasons = check_blacklist(pattern.replace("\\W", " ").replace("\\.", "."), blacklist_type == "username", blacklist_type == "watch_keyword") if reasons: raise CmdException("That pattern looks like it's already caught by " + format_blacklist_reasons(reasons) + "; append `-force` if you really want to do that.") _, result = GitManager.add_to_blacklist( blacklist=blacklist_type, item_to_blacklist=pattern, username=msg.owner.name, chat_profile_link=chat_user_profile_link, code_permissions=is_code_privileged(msg._client.host, msg.owner.id) ) return result
def unnotify(msg, room_id, se_site): """ Unsubscribes a user to specific events :param msg: :param room_id: :param se_site: :return: A string """ response = remove_from_notification_list(msg.owner.id, msg._client.host, room_id, se_site) if response: return "I will no longer ping you if I report a post on `{site}`, in room `{room}` "\ "on `chat.{domain}`".format(site=se_site, room=room_id, domain=msg._client.host) raise CmdException("That configuration doesn't exist.")
def naa(feedback, msg, alias_used="naa"): """ Marks a post as NAA :param feedback: :param msg: :return: String """ post_data = get_report_data(msg) if not post_data: raise CmdException("That message is not a report.") post_url, _ = post_data post_id, site, post_type = fetch_post_id_and_site_from_url(post_url) if post_type != "answer": raise CmdException("That report was a question; questions cannot be marked as NAAs.") feedback_type = NAA_FEEDBACKS[alias_used] feedback_type.send(post_url, feedback) post_id, site, _ = fetch_post_id_and_site_from_url(post_url) add_ignored_post((post_id, site)) return "Recorded answer as an NAA in metasmoke." if not feedback_type.always_silent else ""
def autoflagged(msg): """ Determines whether a post was automatically flagged by Metasmoke :param msg: :return: A string """ post_data = get_report_data(msg) if not post_data: raise CmdException("That's not a report.") is_autoflagged, names = Metasmoke.determine_if_autoflagged(post_data[0]) if is_autoflagged: return "That post was automatically flagged, using flags from: {}.".format(", ".join(names)) else: return "That post was **not** automatically flagged by metasmoke."
def iswlu(user): """ Checks if a user is whitelisted :param user: :return: A string """ uid, val = get_user_from_list_command(user) if int(uid) > -1 and val != "": if is_whitelisted_user((uid, val)): return "User is whitelisted (`{}` on `{}`).".format(uid, val) else: return "User is not whitelisted (`{}` on `{}`).".format(uid, val) elif int(uid) == -2: return "Error: {}".format(val) else: raise CmdException("Invalid format. Valid format: `!!/iswlu profileurl` *or* `!!/iswlu userid sitename`.")
def ignore(feedback, msg): """ Marks a post to be ignored :param feedback: :param msg: :return: String """ post_data = get_report_data(msg) if not post_data: raise CmdException("That message is not a report.") post_url, _ = post_data Feedback.send_custom("ignore", post_url, feedback) post_id, site, _ = fetch_post_id_and_site_from_url(post_url) add_ignored_post((post_id, site)) return "Post ignored; alerts about it will no longer be posted."
def false(feedback, msg, alias_used="false"): """ Marks a post as a false positive :param feedback: :param msg: :return: String """ post_data = get_report_data(msg) if not post_data: raise CmdException("That message is not a report.") post_url, owner_url = post_data feedback_type = FALSE_FEEDBACKS[alias_used] feedback_type.send(post_url, feedback) post_id, site, post_type = fetch_post_id_and_site_from_url(post_url) add_false_positive((post_id, site)) user = get_user_from_url(owner_url) if user is not None: if feedback_type.blacklist: add_whitelisted_user(user) result = "Registered " + post_type + " as false positive and whitelisted user." elif is_blacklisted_user(user): remove_blacklisted_user(user) result = "Registered " + post_type + " as false positive and removed user from the blacklist." else: result = "Registered " + post_type + " as false positive." else: result = "Registered " + post_type + " as false positive." try: if int(msg.room.id) != int(GlobalVars.charcoal_hq.id): msg.delete() except: pass return result if not feedback_type.always_silent else ""
def whois(msg, role): """ Return a list of important users :param msg: :param role: :return: A string """ valid_roles = {"admin": "admin", "code_admin": "code_admin", "admins": "admin", "codeadmins": "code_admin"} if role not in list(valid_roles.keys()): raise CmdException("That is not a user level I can check. " "I know about {0}".format(", ".join(set(valid_roles.values())))) ms_route = "https://metasmoke.erwaysoftware.com/api/users/?role={}&key={}&per_page=100".format( valid_roles[role], GlobalVars.metasmoke_key) user_response = requests.get(ms_route) user_response.encoding = 'utf-8-sig' user_response = user_response.json() chat_host = msg._client.host # Build our list of admin chat ids key = "" if chat_host == "stackexchange.com": key = 'stackexchange_chat_id' elif chat_host == "meta.stackexchange.com": key = 'meta_stackexchange_chat_id' elif chat_host == "stackoverflow.com": key = 'stackoverflow_chat_id' admin_ids = [a[key] for a in user_response['items'] if a[key] and a['id'] != -1] all_users_in_room = msg.room.get_current_user_ids() admins_in_room = list(set(admin_ids) & set(all_users_in_room)) admins_not_in_room = list(set(admin_ids) - set(admins_in_room)) admins_list = [(admin, msg._client.get_user(admin).name, msg._client.get_user(admin).last_message, msg._client.get_user(admin).last_seen) for admin in admin_ids] admins_in_room_list = [(admin, msg._client.get_user(admin).name, msg._client.get_user(admin).last_message, msg._client.get_user(admin).last_seen) for admin in admins_in_room] admins_not_in_room_list = [(admin, msg._client.get_user(admin).name, msg._client.get_user(admin).last_message, msg._client.get_user(admin).last_seen) for admin in admins_not_in_room] return_name = RETURN_NAMES[valid_roles[role]][0 if len(admin_ids) == 1 else 1] response = "I am aware of {} {}".format(len(admin_ids), return_name) if admins_in_room_list: admins_in_room_list.sort(key=lambda x: x[2]) # Sort by last message (last seen = x[3]) response += ". Currently in this room: **" for admin in admins_in_room_list: response += "{}, ".format(admin[1]) response = response[:-2] + "**. " response += "Not currently in this room: " for admin in admins_not_in_room_list: response += "{}, ".format(admin[1]) response = response[:-2] + "." else: response += ": " for admin in admins_list: response += "{}, ".format(admin[1]) response = response[:-2] + ". " response += "None of them are currently in this room. Other users in this room might be able to help you." return response
def report(msg, urls): """ Report a post (or posts) :param msg: :param urls: :return: A string (or None) """ crn, wait = can_report_now(msg.owner.id, msg._client.host) if not crn: raise CmdException("You can execute the !!/report command again in {} seconds. " "To avoid one user sending lots of reports in a few commands and " "slowing SmokeDetector down due to rate-limiting, you have to " "wait 30 seconds after you've reported multiple posts in " "one go.".format(wait)) output = [] urls = list(set(urls.split())) if len(urls) > 5: raise CmdException("To avoid SmokeDetector reporting posts too slowly, you can " "report at most 5 posts at a time. This is to avoid " "SmokeDetector's chat messages getting rate-limited too much, " "which would slow down reports.") for index, url in enumerate(urls, start=1): post_data = api_get_post(url) if post_data is None: output.append("Post {}: That does not look like a valid post URL.".format(index)) continue if post_data is False: output.append("Post {}: Could not find data for this post in the API. " "It may already have been deleted.".format(index)) continue if has_already_been_posted(post_data.site, post_data.post_id, post_data.title) and not is_false_positive( (post_data.post_id, post_data.site)): # Don't re-report if the post wasn't marked as a false positive. If it was marked as a false positive, # this re-report might be attempting to correct that/fix a mistake/etc. if GlobalVars.metasmoke_key is not None: se_link = to_protocol_relative(post_data.post_url) ms_link = "https://m.erwaysoftware.com/posts/by-url?url={}".format(se_link) output.append("Post {}: Already recently reported [ [MS]({}) ]".format(index, ms_link)) continue else: output.append("Post {}: Already recently reported".format(index)) continue post_data.is_answer = (post_data.post_type == "answer") post = Post(api_response=post_data.as_dict) user = get_user_from_url(post_data.owner_url) if user is not None: message_url = "https://chat.{}/transcript/{}?m={}".format(msg._client.host, msg.room.id, msg.id) add_blacklisted_user(user, message_url, post_data.post_url) why_info = u"Post manually reported by user *{}* in room *{}*.\n".format(msg.owner.name, msg.room.name) batch = "" if len(urls) > 1: batch = " (batch report: post {} out of {})".format(index, len(urls)) handle_spam(post=post, reasons=["Manually reported " + post_data.post_type + batch], why=why_info) if 1 < len(urls) > len(output): add_or_update_multiple_reporter(msg.owner.id, msg._client.host, time.time()) if len(output) > 0: return os.linesep.join(output)
def allspam(msg, url): """ Reports all of a user's posts as spam :param msg: :param url: A user profile URL :return: """ crn, wait = can_report_now(msg.owner.id, msg._client.host) if not crn: raise CmdException("You can execute the !!/allspam command again in {} seconds. " "To avoid one user sending lots of reports in a few commands and " "slowing SmokeDetector down due to rate-limiting, you have to " "wait 30 seconds after you've reported multiple posts in " "one go.".format(wait)) user = get_user_from_url(url) if user is None: raise CmdException("That doesn't look like a valid user URL.") user_sites = [] user_posts = [] # Detect whether link is to network profile or site profile if user[1] == 'stackexchange.com': # Respect backoffs etc GlobalVars.api_request_lock.acquire() if GlobalVars.api_backoff_time > time.time(): time.sleep(GlobalVars.api_backoff_time - time.time() + 2) # Fetch sites api_filter = "!6Pbp)--cWmv(1" request_url = "http://api.stackexchange.com/2.2/users/{}/associated?filter={}&key=IAkbitmze4B8KpacUfLqkw((" \ .format(user[0], api_filter) res = requests.get(request_url).json() if "backoff" in res: if GlobalVars.api_backoff_time < time.time() + res["backoff"]: GlobalVars.api_backoff_time = time.time() + res["backoff"] GlobalVars.api_request_lock.release() if 'items' not in res or len(res['items']) == 0: raise CmdException("The specified user does not appear to exist.") if res['has_more']: raise CmdException("The specified user has an abnormally high number of accounts. Please consider flagging " "for moderator attention, otherwise use !!/report on the user's posts individually.") # Add accounts with posts for site in res['items']: if site['question_count'] > 0 or site['answer_count'] > 0: user_sites.append((site['user_id'], get_api_sitename_from_url(site['site_url']))) else: user_sites.append((user[0], get_api_sitename_from_url(user[1]))) # Fetch posts for u_id, u_site in user_sites: # Respect backoffs etc GlobalVars.api_request_lock.acquire() if GlobalVars.api_backoff_time > time.time(): time.sleep(GlobalVars.api_backoff_time - time.time() + 2) # Fetch posts api_filter = "!)Q4RrMH0DC96Y4g9yVzuwUrW" request_url = "http://api.stackexchange.com/2.2/users/{}/posts?site={}&filter={}&key=IAkbitmze4B8KpacUfLqkw((" \ .format(u_id, u_site, api_filter) res = requests.get(request_url).json() if "backoff" in res: if GlobalVars.api_backoff_time < time.time() + res["backoff"]: GlobalVars.api_backoff_time = time.time() + res["backoff"] GlobalVars.api_request_lock.release() if 'items' not in res or len(res['items']) == 0: raise CmdException("The specified user has no posts on this site.") posts = res['items'] if posts[0]['owner']['reputation'] > 100: raise CmdException("The specified user's reputation is abnormally high. Please consider flagging for " "moderator attention, otherwise use !!/report on the posts individually.") # Add blacklisted user - use most downvoted post as post URL message_url = "https://chat.{}/transcript/{}?m={}".format(msg._client.host, msg.room.id, msg.id) add_blacklisted_user(user, message_url, sorted(posts, key=lambda x: x['score'])[0]['owner']['link']) # TODO: Postdata refactor, figure out a better way to use apigetpost for post in posts: post_data = PostData() post_data.post_id = post['post_id'] post_data.post_url = url_to_shortlink(post['link']) *discard, post_data.site, post_data.post_type = fetch_post_id_and_site_from_url( url_to_shortlink(post['link'])) post_data.title = unescape(post['title']) post_data.owner_name = unescape(post['owner']['display_name']) post_data.owner_url = post['owner']['link'] post_data.owner_rep = post['owner']['reputation'] post_data.body = post['body'] post_data.score = post['score'] post_data.up_vote_count = post['up_vote_count'] post_data.down_vote_count = post['down_vote_count'] if post_data.post_type == "answer": # Annoyingly we have to make another request to get the question ID, since it is only returned by the # /answers route # Respect backoffs etc GlobalVars.api_request_lock.acquire() if GlobalVars.api_backoff_time > time.time(): time.sleep(GlobalVars.api_backoff_time - time.time() + 2) # Fetch posts filter = "!*Jxb9s5EOrE51WK*" req_url = "http://api.stackexchange.com/2.2/answers/{}?site={}&filter={}&key=IAkbitmze4B8KpacUfLqkw((" \ .format(post['post_id'], u_site, filter) answer_res = requests.get(req_url).json() if "backoff" in res: if GlobalVars.api_backoff_time < time.time() + res["backoff"]: GlobalVars.api_backoff_time = time.time() + res["backoff"] GlobalVars.api_request_lock.release() # Finally, set the attribute post_data.question_id = answer_res['items'][0]['question_id'] post_data.is_answer = True user_posts.append(post_data) if len(user_posts) == 0: raise CmdException("The specified user hasn't posted anything.") if len(user_posts) > 15: raise CmdException("The specified user has an abnormally high number of spam posts. Please consider flagging " "for moderator attention, otherwise use !!/report on the posts individually.") why_info = u"User manually reported by *{}* in room *{}*.\n".format(msg.owner.name, msg.room.name) # Handle all posts for index, post in enumerate(user_posts, start=1): batch = "" if len(user_posts) > 1: batch = " (batch report: post {} out of {})".format(index, len(user_posts)) handle_spam(post=Post(api_response=post.as_dict), reasons=["Manually reported " + post.post_type + batch], why=why_info) time.sleep(2) # Should this be implemented differently? if len(user_posts) > 2: add_or_update_multiple_reporter(msg.owner.id, msg._client.host, time.time())