From ca59f2882a7ed485a53c5390f63952b989456cc5 Mon Sep 17 00:00:00 2001 From: ted Date: Sun, 12 Nov 2023 21:51:23 +0000 Subject: [PATCH] initial --- .gitignore | 13 ++ beloved_logger.py | 18 ++ bot.py | 39 ++++ classes/post.py | 158 +++++++++++++++++ classes/rating.py | 54 ++++++ data/filter-dev.txt | 9 + data/filter.txt | 1 + data/labels-dev.json | 71 ++++++++ data/labels.json | 69 ++++++++ data/off.png | Bin 0 -> 7516 bytes data/on.png | Bin 0 -> 9549 bytes functions/check_chat.py | 274 +++++++++++++++++++++++++++++ functions/delete_post.py | 28 +++ functions/get_filters_from_text.py | 8 + functions/get_newest_count.py | 26 +++ functions/roll_token.py | 45 +++++ functions/scan_posts.py | 67 +++++++ functions/send_message.py | 65 +++++++ functions/suspend_user.py | 46 +++++ functions/update_display_name.py | 89 ++++++++++ requirements.txt | 5 + var.py | 35 ++++ 22 files changed, 1120 insertions(+) create mode 100644 .gitignore create mode 100755 beloved_logger.py create mode 100755 bot.py create mode 100755 classes/post.py create mode 100755 classes/rating.py create mode 100644 data/filter-dev.txt create mode 100644 data/filter.txt create mode 100644 data/labels-dev.json create mode 100644 data/labels.json create mode 100644 data/off.png create mode 100644 data/on.png create mode 100755 functions/check_chat.py create mode 100755 functions/delete_post.py create mode 100755 functions/get_filters_from_text.py create mode 100755 functions/get_newest_count.py create mode 100755 functions/roll_token.py create mode 100755 functions/scan_posts.py create mode 100755 functions/send_message.py create mode 100755 functions/suspend_user.py create mode 100755 functions/update_display_name.py create mode 100755 requirements.txt create mode 100755 var.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1670072 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.idea +.venv +lib64 +lib +includes +.env +pyvenv.cfg +__pycache__ +bin +logs +venv +.env +.env.dev diff --git a/beloved_logger.py b/beloved_logger.py new file mode 100755 index 0000000..f9c80d0 --- /dev/null +++ b/beloved_logger.py @@ -0,0 +1,18 @@ +import logging +import var + +logger = logging.getLogger('logger') +logger.setLevel(logging.INFO) + +file_log = logging.FileHandler(f'logs/log{var.log_startup_time}.txt') +file_log.setLevel(logging.INFO) + +console_log = logging.StreamHandler() +console_log.setLevel(logging.INFO) + +formatter = logging.Formatter('%(asctime)s - %(name)s - %(module)s - %(levelname)s - %(message)s') +file_log.setFormatter(formatter) +console_log.setFormatter(formatter) + +logger.addHandler(file_log) +logger.addHandler(console_log) diff --git a/bot.py b/bot.py new file mode 100755 index 0000000..4a1006e --- /dev/null +++ b/bot.py @@ -0,0 +1,39 @@ +#!/home/opc/automod3/mv/bin/python3.11 +import asyncio +from datetime import datetime +import functions.get_newest_count +from functions.send_message import send_message +from functions.scan_posts import scan_posts +from functions.check_chat import check_chat +import var +from beloved_logger import logger +from functions.update_display_name import update_display_name + + +async def main_loop(): + await check_chat() + + if var.text_scanning_is_running or var.image_scanning_is_running: + await scan_posts() + + +async def startup(): + var.current_posts_at_start_time = await functions.get_newest_count.get_newest_count() + var.bot_started_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + logger.info(f"Started bot with values:\n " + f"bot start time: {var.bot_started_at}\n" + f"logging start time: {var.log_startup_time}\n" + f"site used: {var.site}\n" + f"imagescanning runs: {var.image_scanning_is_running}\n" + f"image scan only scans new users: {var.scan_new_users_only}\n" + f"textscanning runs: {var.text_scanning_is_running}\n" + f"chat number: {var.chat_number}\n" + f"filters list: {var.filters_list}\n") + await update_display_name() + while True: + await main_loop() + + +if __name__ == "__main__": + asyncio.run(startup()) diff --git a/classes/post.py b/classes/post.py new file mode 100755 index 0000000..64803c9 --- /dev/null +++ b/classes/post.py @@ -0,0 +1,158 @@ +import requests +import datetime +import boto3 +import var +import classes.rating +from functions.delete_post import delete_post +from beloved_logger import logger + + +class Post: + post_id = None + post_description = None + post_name = None + image = None + user_id = None + poster_username = None + poster_sign_up_time = None + response = None + date = None + infringing_words = [] + ratings = None + post_will_be_deleted = None + deletion_status = None + + def __init__(self, post_id): + logger.info(f"Post was made with ID {post_id}") + self.post_id = post_id + self.infringing_words = [] + + async def get_values(self): + response = requests.get(f"https://{var.site}/posts/{self.post_id}", + headers={'User-Agent': 'automod', + "Accept": "application/json,text/plain, */*"}) + + if response.status_code == 200: + self.response = response.json() + if response.json()['post']['resource_url']: + self.image = requests.get(self.response['post']['resource_url']).content + + self.post_name = self.response['post']["title"] + self.user_id = self.response['post']['profile_id'] + self.poster_username = self.response['post']['profile']['username'] + self.poster_sign_up_time = self.response['post']['profile']['created_at'] + self.date = self.response['post']['created_at'] + self.post_description = self.response['post']["description"] + + logger.debug(f"Got post {self.post_id} with details {self.response}") + else: + logger.info(f"An error occurred getting post with ID {self.post_id} for reason {response.status_code}") + return False + + async def analyse_text(self): + logger.info(f"Getting text analysis of post with post ID {self.post_id}") + + if self.post_name: + for word in var.filters_list: + if word.lower() in self.post_name.lower() and word not in self.infringing_words: + logger.info(f"Post {self.post_id}'s name contained infringing word {word}") + self.infringing_words.append(word) + + if self.post_description: + for word in var.filters_list: + if word.lower() in self.post_description.lower() and word not in self.infringing_words: + logger.info(f"Post {self.post_id}'s description contained infringing word {word}") + self.infringing_words.append(word) + + async def analyse_image(self): + logger.info(f"Getting image analysis of post with post ID {self.post_id}") + if self.image: + logger.info(f"Post {self.post_id} had an image") + await self.detect_image() + if self.ratings: + logger.info(f"Post {self.post_id} has ratings: {await self.get_ratings_string()}") + await self.check_ratings() + if self.post_will_be_deleted: + logger.info(f"Post {self.post_id} will be deleted") + await self.remove_post() + logger.info(f"Post {self.post_id} was deleted") + else: + logger.info(f"Post {self.post_id} will not be deleted") + else: + logger.info(f"Post {self.post_id} did not have any ratings") + else: + logger.info(f"Post {self.post_id} was either a text post or deleted before analysis") + + async def detect_image(self): + logger.info(f"Sending data to AWS for post {self.post_id}") + client = boto3.client('rekognition', region_name=var.aws_region) + self.ratings = classes.rating.create_ratings(client.detect_moderation_labels(Image={'Bytes': self.image})) + + async def check_ratings(self): + logger.info(f"Checking ratings for post {self.post_id}") + one_rating_was_false = False + + for post_rating in self.ratings: + if post_rating.confidence > 90 and post_rating.enabled: + logger.info(f"Post {self.post_id} contained enabled rating more than 90 for {post_rating.name} which " + f"was type of {post_rating.type}") + self.post_will_be_deleted = True + elif not post_rating.enabled: + logger.info(f"Post {self.post_id} had a rating of {post_rating.name} in the " + f"{post_rating.type} category at confidence {post_rating.confidence}" + f"but it was not enabled. Post will not be deleted.") + one_rating_was_false = True + + if one_rating_was_false: + self.post_will_be_deleted = False + self.ratings = None + + async def remove_post(self): + var.most_recently_deleted_post_id = self.post_id + self.deletion_status = False + if self.post_will_be_deleted: + logger.info(f"Attempting to delete a post {self.post_id} for {await self.get_ratings_string()}") + + self.deletion_status = await delete_post(self.post_id) + if self.deletion_status == 200: + logger.info(f"Post {self.post_id} was deleted") + self.deletion_status = True + else: + logger.info(f"An issue occurred deleting {self.post_id} for reason {self.deletion_status}") + self.deletion_status = f"Problem occurred with deletion: {self.deletion_status}" + + async def get_readable_date(self): + return datetime.datetime.utcfromtimestamp(self.date).strftime('%Y-%m-%d %H:%M:%S') + + async def get_censorship_string(self): + return '", "'.join(self.infringing_words) + + async def get_ratings_string(self): + ratings_string = "" + for post_rating in self.ratings: + ratings_string += str(post_rating) + return ratings_string + + async def get_as_text_detection_text(self): + string = (f'â€ŧī¸ *DETECTED:* "{await self.get_censorship_string()}"\n' + + f'đŸ—’ī¸ *TITLE:* "{self.post_name}"\n' + + f"🔗 *URL:* https://{var.site}/admin/posts/{self.post_id}\n" + + f"👤 *POSTER:* https://{var.site}/users/{self.user_id} / @{self.poster_username}\n" + f"The user was also suspended for a day, you will have to reverse this suspension if the moderation action was not warranted..") + return string + + async def get_as_image_deletion_text(self): + ratings_string = "" + for post_rating in self.ratings: + ratings_string += str(post_rating) + + if self.deletion_status is None: + self.deletion_status = False + + string = (f"{ratings_string} \n " + + f'đŸ—’ī¸ *TITLE:* "{self.post_name}"\n' + + f"🔗 *URL:* https://{var.site}/admin/posts/{self.post_id}\n" + + f"👤 *POSTER:* https://{var.site}/users/{self.user_id} / @{self.poster_username}\n" + + f"❌ *DELETED:* {str(self.deletion_status)}" + f"The user was also suspended for a day, if the suspension was not warranted you must undo the suspension") + return string diff --git a/classes/rating.py b/classes/rating.py new file mode 100755 index 0000000..4f11652 --- /dev/null +++ b/classes/rating.py @@ -0,0 +1,54 @@ +import var +import json + + +class Rating: + name = "" + confidence = 0 + enabled = False + type = "" + + def __init__(self, name, confidence): + self.name = name + self.confidence = confidence + self.enabled = self.get_status() + + def __str__(self): + return f"â€ŧī¸ *{self.name}:* {round(self.confidence, 2)}%\n" + + def get_status(self): + with open(var.labels_file, 'r') as file: + data = json.load(file) + + top_level_true = [] + top_level_false = [] + lower_level_true = [] + lower_level_false = [] + + for top_level_object, top_level_data in data.items(): + if top_level_data.get("status") is True: + top_level_true.append(top_level_object) + elif top_level_data.get("status") is False: + top_level_false.append(top_level_object) + + for lower_level_object, lower_level_status in top_level_data.items(): + if lower_level_status is True and lower_level_object != "status": + lower_level_true.append(f"{lower_level_object}") + elif lower_level_status is False and lower_level_object != "status": + lower_level_false.append(f"{lower_level_object}") + + if self.name in lower_level_true: + self.type = "secondary" + return True + if self.name in top_level_true: + self.type = "primary" + return True + return False + + +def create_ratings(response): + ratings = [] + for label in response['ModerationLabels']: + rating = Rating(label['Name'], label['Confidence']) + ratings.append(rating) + return ratings diff --git a/data/filter-dev.txt b/data/filter-dev.txt new file mode 100644 index 0000000..1544dbe --- /dev/null +++ b/data/filter-dev.txt @@ -0,0 +1,9 @@ +ted +green +bert +filter +HELLO +DIGLES +wiglett +STUPID + diff --git a/data/filter.txt b/data/filter.txt new file mode 100644 index 0000000..095afd2 --- /dev/null +++ b/data/filter.txt @@ -0,0 +1 @@ +boto3~=1.28.39\nselenium~=4.12.0\npy-cord\nrequests~=2.31.0\npython-dotenv diff --git a/data/labels-dev.json b/data/labels-dev.json new file mode 100644 index 0000000..aa432b4 --- /dev/null +++ b/data/labels-dev.json @@ -0,0 +1,71 @@ +{ + "Explicit Nudity": { + "status": true, + "Nudity": true, + "Graphic Male Nudity": true, + "Graphic Female Nudity": true, + "Sexual Activity": true, + "Illustrated Explicit Nudity": true, + "Adult Toys": true + }, + "Suggestive": { + "status": true, + "Female Swimwear Or Underwear": true, + "Male Swimwear Or Underwear": true, + "Partial Nudity": false, + "Barechested Male": false, + "Revealing Clothes": false, + "Sexual Situations": true + }, + "Violence": { + "status": true, + "Graphic Violence Or Gore": true, + "Physical Violence": true, + "Weapon Violence": true, + "Weapons": false, + "Self Injury": true + }, + "Visually Disturbing": + { + "status": true, + "Emaciated Bodies": true, + "Corpses": true, + "Hanging": true, + "Air Crash": false, + "Explosions And Blasts": false + }, + "Rude Gestures": + { + "status": false, + "Middle Finger": false + }, + "Drugs": { + "status": false, + "Drug Products": false, + "Drug Use": false, + "Pills": false, + "Drug Paraphernalia": false + }, + "Tobacco": { + "status": false, + "Tobacco Products": false, + "Smoking": false + }, + "Alcohol": { + "status": false, + "Drinking": false, + "Alcoholic Beverages": false + }, + "Gambling": { + "status": false, + "Gambling": false + }, + "Hate Symbols": { + "status": true, + "Nazi Party": true, + "White Supremacy": true, + "Extremist": true + } +} + + diff --git a/data/labels.json b/data/labels.json new file mode 100644 index 0000000..058e623 --- /dev/null +++ b/data/labels.json @@ -0,0 +1,69 @@ +{ + "Explicit Nudity": { + "status": true, + "Nudity": true, + "Graphic Male Nudity": true, + "Graphic Female Nudity": true, + "Sexual Activity": true, + "Illustrated Explicit Nudity": true, + "Adult Toys": true + }, + "Suggestive": { + "status": true, + "Female Swimwear Or Underwear": false, + "Male Swimwear Or Underwear": false, + "Partial Nudity": false, + "Barechested Male": false, + "Revealing Clothes": false, + "Sexual Situations": true + }, + "Violence": { + "status": true, + "Graphic Violence Or Gore": true, + "Physical Violence": true, + "Weapon Violence": true, + "Weapons": false, + "Self Injury": true + }, + "Visually Disturbing": + { + "status": true, + "Emaciated Bodies": true, + "Corpses": true, + "Hanging": true, + "Air Crash": false, + "Explosions And Blasts": false + }, + "Rude Gestures": + { + "status": false, + "Middle Finger": false + }, + "Drugs": { + "status": false, + "Drug Products": false, + "Drug Use": false, + "Pills": false, + "Drug Paraphernalia": false + }, + "Tobacco": { + "status": false, + "Tobacco Products": false, + "Smoking": false + }, + "Alcohol": { + "status": false, + "Drinking": false, + "Alcoholic Beverages": false + }, + "Gambling": { + "status": false, + "Gambling": false + }, + "Hate Symbols": { + "status": true, + "Nazi Party": true, + "White Supremacy": true, + "Extremist": true + } +} \ No newline at end of file diff --git a/data/off.png b/data/off.png new file mode 100644 index 0000000000000000000000000000000000000000..8a9e13584bae80ed03a2c40ea93ee6415b7b150f GIT binary patch literal 7516 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 YqwGy=eMwURo4z$emn;kXDnP#fe*wC7@Bjb+ literal 0 HcmV?d00001 diff --git a/data/on.png b/data/on.png new file mode 100644 index 0000000000000000000000000000000000000000..66ce685ebd10e8dc29fc11beb0fe7b4085eefd5a GIT binary patch literal 9549 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 var.current_posts_at_start_time: + var.current_posts_at_start_time = var.most_recently_deleted_post_id + + current_posts_now = var.current_posts_at_start_time + new_posts = await functions.get_newest_count.get_newest_count() + if new_posts != 0: + current_posts_now = new_posts + + logger.debug(f"posts now: {current_posts_now}") + logger.debug(f"posts previous: {var.current_posts_at_start_time}") + + if current_posts_now != var.current_posts_at_start_time: + for i in range(int(var.current_posts_at_start_time) + 1, int(current_posts_now) + 1): + post_to_analyse = post.Post(i) + await post_to_analyse.get_values() + + if post_to_analyse.response: + if var.text_scanning_is_running: + await post_to_analyse.analyse_text() + if len(post_to_analyse.infringing_words) > 0: + await send_message(await post_to_analyse.get_as_text_detection_text()) + + if var.image_scanning_is_running: + if var.scan_new_users_only: + cut_off_date = datetime.now() - timedelta(weeks=2) + current_date = datetime.strptime(post_to_analyse.poster_sign_up_time, "%Y-%m-%d %H:%M:%S") + + logger.debug(f"User signed up at {current_date}, we are checking for " + f"users that signed up after {cut_off_date}") + + if current_date < cut_off_date: + logger.info(f"User was signed up before {cut_off_date}, so checks won't occur") + var.most_recently_deleted_post_id = i + return + else: + logger.info(f"User was signed up after {cut_off_date}, so checks will occur") + + logger.info(f"Analysing {i} for image") + await post_to_analyse.analyse_image() + if post_to_analyse.ratings: + if post_to_analyse.deletion_status: + message_string = f"""👋 Hello, I am an automatic moderation bot for Sketchers United. + + Your most recent post titled '{post_to_analyse.post_name}' was removed automatically as it was detected to have inappropriate content. + As a result, your account was suspended for one day. + + *If this was a mistake don't worry, all deletions and suspensions are subject to human review and corrections to automatic moderation actions (removal of suspensions and restoration of deleted posts) will be made as soon as possible if they are not warranted.* + + ❓ If there any questions, problems, or concerns please contact @username + """ + + await suspend_user_for_post(post_to_analyse) + await send_message(await post_to_analyse.get_as_image_deletion_text()) + await send_message(message_string, post_to_analyse.poster_username) + + var.current_posts_at_start_time = current_posts_now diff --git a/functions/send_message.py b/functions/send_message.py new file mode 100755 index 0000000..300a9d2 --- /dev/null +++ b/functions/send_message.py @@ -0,0 +1,65 @@ +import requests +from functions.roll_token import roll_token +import var +from beloved_logger import logger + + +async def send_message(text, chat_id=None): + if chat_id is None: + logger.debug(f"No chat id was given, using {var.chat_number}") + chat_id = int(var.chat_number) + + logger.info(f"Sending message to {chat_id}") + logger.debug(f"Message contents: {text}") + + await roll_token() + + if type(chat_id) != int: + cookies = { + var.cookie_name: var.cookie_value, + 'sketchers_united_session': var.session + } + + headers = { + 'User-Agent': 'automod', + 'Accept': 'application/json, text/plain, */*', + } + + response = requests.get(f'https://{var.site}/chats/@{chat_id}', + cookies=cookies, + headers=headers) + + if response.status_code != 200 and response.status_code != 201: + logger.error(f"There was an issue with getting an id from the {chat_id} " + f"with error {response.status_code}") + return False + chat_id = response.json()['id'] + + cookies = { + var.cookie_name: var.cookie_value, + 'sketchers_united_session': var.session, + 'XSRF-TOKEN': var.xsrf_token + } + + headers = { + 'User-Agent': 'automod', + 'Accept': 'application/json, */*', + 'X-CSRF-TOKEN': var.csrf_token, + 'X-XSRF-TOKEN': var.xsrf_token, + 'Content-Type': 'multipart/form-data; boundary=---------------------------9051447283201882072943098316' + } + + data = (f'-----------------------------9051447283201882072943098316' + f'\r\nContent-Disposition: form-data; ' + f'name="text"\r\n\r\n{text}' + f'\r\n-----------------------------9051447283201882072943098316--\r\n').encode() + + response = requests.post(f'https://{var.site}/chats/{chat_id}/messages', cookies=cookies, + headers=headers, + data=data) + logger.info(f"Sending message concluded with response {response.status_code}") + + if response.status_code != 200 and response.status_code != 201: + logger.error(f"There was an issue with sending a message to the {chat_id} " + f"with error {response.status_code}") + return False diff --git a/functions/suspend_user.py b/functions/suspend_user.py new file mode 100755 index 0000000..d87b7e6 --- /dev/null +++ b/functions/suspend_user.py @@ -0,0 +1,46 @@ +import logging + +import var +import requests +from functions.roll_token import roll_token +from beloved_logger import logger + + +async def suspend_user_for_post(post): + await roll_token() + + cookies = { + var.cookie_name: var.cookie_value, + 'sketchers_united_session': var.session, + 'XSRF-TOKEN': var.xsrf_token + } + + logging.debug(f"Suspending user with cookies {cookies}") + + headers = { + 'User-Agent': 'automod', + 'Content-Type': 'multipart/form-data; boundary=---------------------------33943441651739618973055574259', + } + + data = ('-----------------------------33943441651739618973055574259\r\n' + 'Content-Disposition: form-data; name="_method"\r\n' + '\r\nPOST\r\n' + '-----------------------------33943441651739618973055574259\r\n' + 'Content-Disposition: form-data; name="_token"\r\n' + f'\r\n{var.csrf_token}\r\n' + '-----------------------------33943441651739618973055574259' + '\r\nContent-Disposition: form-data; name="blocked_id"\r\n' + f'\r\n{post.user_id}\r\n' + f'-----------------------------33943441651739618973055574259\r\n' + 'Content-Disposition: form-data; name="days"\r\n' + '\r\n1\r\n' + '-----------------------------33943441651739618973055574259\r\n' + 'Content-Disposition: form-data; name="reason"\r\n' + '\r\nother\r\n' + '-----------------------------33943441651739618973055574259' + '\r\nContent-Disposition: form-data; name="note"\r\n' + f'\r\n{await post.get_ratings_string()}\r\n\r\n at https://{var.site}/admin/posts/{post.post_id}\r\n' + f'-----------------------------33943441651739618973055574259--\r\n').encode() + + response = requests.post(f'https://{var.site}/admin/blocks', cookies=cookies, headers=headers, data=data) + logger.info(f"Suspending user {post.user_id} occurred with: {response.status_code}") diff --git a/functions/update_display_name.py b/functions/update_display_name.py new file mode 100755 index 0000000..b888a81 --- /dev/null +++ b/functions/update_display_name.py @@ -0,0 +1,89 @@ +import requests +import var +from functions.roll_token import roll_token +from beloved_logger import logger + + +async def update_display_name(): + await roll_token() + + cookies = { + var.cookie_name: var.cookie_value, + 'sketchers_united_session': var.session, + 'XSRF-TOKEN': var.xsrf_token + } + logger.debug(f"Updating display name with cookies {cookies}") + + headers = { + 'User-Agent': 'automod', + 'Content-Type': 'multipart/form-data; boundary=---------------------------39838723211327797103443672495', + } + + value = "" + + if var.text_scanning_is_running: + value += "đŸ’Ŧ" + if var.image_scanning_is_running: + value += "đŸ–ŧī¸" + + new_name = "ms alice a. automod " + value + description = (f"automatic moderation bot run by @username, send me a message if there\'s a problem" + f"\r\nmessages sent to this account will not be read\r\n") + + data = (f'-----------------------------39838723211327797103443672495' + f'\r\nContent-Disposition: form-data; ' + f'name="_token"\r\n' + f'\r\n{var.csrf_token}' + f'\r\n-----------------------------39838723211327797103443672495\r' + f'\nContent-Disposition: form-data; name="profile_image"; filename=""' + f'\r\nContent-Type: ' + f'application/octet-stream\r\n' + f'\r\n-----------------------------39838723211327797103443672495' + f'\r\nContent' + f'-Disposition: form-data; name="backdrop_image"; filename=""' + f'\r\nContent-Type: ' + f'application/octet-stream\r\n' + f'\r\n-----------------------------39838723211327797103443672495' + f'\r\nContent' + f'-Disposition: form-data; name="name"\r\n' + f'\r\n{new_name}' + f'\r\n-----------------------------39838723211327797103443672495' + f'\r\nContent-Disposition: form-data; ' + f'name="username"\r\n' + f'\r\nakemi' + f'\r\n-----------------------------39838723211327797103443672495' + f'\r\nContent' + f'-Disposition: form-data; name="description"\r\n' + f'\r\n{description}' + f'-----------------------------39838723211327797103443672495' + f'\r\nContent' + f'-Disposition: form-data; ' + f'name="link_amino"\r\n' + f'\r\n\r\n-----------------------------39838723211327797103443672495' + f'\r\nContent' + f'-Disposition: form-data; ' + f'name="link_deviantart"\r\n' + f'\r\n\r\n-----------------------------39838723211327797103443672495' + f'\r\nContent' + f'-Disposition: form-data; ' + f'name="link_facebook"\r\n' + f'\r\n\r\n-----------------------------39838723211327797103443672495' + f'\r\nContent' + f'-Disposition: form-data; ' + f'name="link_instagram"\r\n' + f'\r\n\r\n-----------------------------39838723211327797103443672495' + f'\r\nContent' + f'-Disposition: form-data; ' + f'name="link_twitter"\r\n' + f'\r\n\r\n-----------------------------39838723211327797103443672495' + f'\r\nContent' + f'-Disposition: form-data; ' + f'name="link_youtube"\r\n' + f'\r\n\r\n-----------------------------39838723211327797103443672495--' + f'\r\n').encode() + + response = requests.post(f'https://{var.site}/users/{var.bot_user_number}/update', + cookies=cookies, + headers=headers, + data=data) + logger.info(f"Updating display name concluded with response {response.status_code}") diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..0e2a071 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +boto3~=1.28.39 +requests~=2.31.0 +python-dotenv~=1.0.0 +websockets~=11.0.3 +beautifulsoup4~=4.12.2 \ No newline at end of file diff --git a/var.py b/var.py new file mode 100755 index 0000000..f2a6f37 --- /dev/null +++ b/var.py @@ -0,0 +1,35 @@ +import asyncio +import os +from dotenv import load_dotenv +from datetime import datetime +from functions.get_filters_from_text import get_filters_from_text + + +load_dotenv('data/.env') + +site = os.getenv("SITE") +cookie_name = os.getenv("COOKIE_NAME") +cookie_value = os.getenv("COOKIE_VALUE") +chat_number = os.getenv("CHAT_NUMBER") +filter_file = os.getenv("FILTER_FILE") +websocket_url = os.getenv("WEBSOCKET_URL") +labels_file = os.getenv("LABELS_FILE") +session = os.getenv("STARTUP_SESSION_VALUE") +bot_user_number = os.getenv("USER_NUMBER") +aws_region = os.getenv("AWS_REGION") + +old_chat = {} +scan_new_users_only = True +image_scanning_is_running = True +text_scanning_is_running = False +most_recently_deleted_post_id = 0 +current_posts_at_start_time = None +bot_started_at = None +posts_removed = None +scanning_since = None +user_timeout = None +xsrf_token = None +csrf_token = None + +filters_list = asyncio.run(get_filters_from_text(filter_file)) +log_startup_time = datetime.now().strftime("%Y%m%d%H%M%S")