From afbe8d09777fdb7c6fc44876e763a42f1025f93f Mon Sep 17 00:00:00 2001 From: Khoi Date: Wed, 19 Jul 2023 13:11:38 -0700 Subject: [PATCH] Completed and tested all parsers fro Cardingleaks forum --- Forums/Altenens/crawler_selenium.py | 22 +- Forums/Cardingleaks/crawler_selenium.py | 22 +- Forums/Cardingleaks/parser.py | 251 +++++------------- Forums/CryptBB/crawler_selenium.py | 6 +- Forums/Initialization/forumsList.txt | 2 +- Forums/Initialization/geckodriver.log | 73 +++-- Forums/Initialization/prepare_parser.py | 5 + Forums/Procrax/parser.py | 2 +- .../__pycache__/utilities.cpython-311.pyc | Bin 15104 -> 15121 bytes setup.ini | 8 +- 10 files changed, 158 insertions(+), 233 deletions(-) diff --git a/Forums/Altenens/crawler_selenium.py b/Forums/Altenens/crawler_selenium.py index c9edd9d..2d9e3cd 100644 --- a/Forums/Altenens/crawler_selenium.py +++ b/Forums/Altenens/crawler_selenium.py @@ -30,18 +30,18 @@ baseURL = 'https://altenens.is/' # Opens Tor Browser, crawls the website def startCrawling(): - # opentor() + opentor() forumName = getForumName() - # driver = getAccess() - # - # if driver != 'down': - # try: - # login(driver) - # crawlForum(driver) - # except Exception as e: - # print(driver.current_url, e) - # closetor(driver) - # + driver = getAccess() + + if driver != 'down': + try: + login(driver) + crawlForum(driver) + except Exception as e: + print(driver.current_url, e) + closetor(driver) + new_parse(forumName, baseURL, False) diff --git a/Forums/Cardingleaks/crawler_selenium.py b/Forums/Cardingleaks/crawler_selenium.py index 835024e..bff1b3e 100644 --- a/Forums/Cardingleaks/crawler_selenium.py +++ b/Forums/Cardingleaks/crawler_selenium.py @@ -29,17 +29,17 @@ baseURL = 'https://cardingleaks.ws/' # Opens Tor Browser, crawls the website def startCrawling(): - opentor() + # opentor() forumName = getForumName() - driver = getAccess() + # driver = getAccess() - if driver != 'down': - try: - login(driver) - crawlForum(driver) - except Exception as e: - print(driver.current_url, e) - closetor(driver) + # if driver != 'down': + # try: + # login(driver) + # crawlForum(driver) + # except Exception as e: + # print(driver.current_url, e) + # closetor(driver) new_parse(forumName, baseURL, False) @@ -242,7 +242,7 @@ def crawlForum(driver): savePage(driver.page_source, topic + f"page{counter}") # very important # comment out - if counter == 2: + if counter == 5: break try: @@ -261,7 +261,7 @@ def crawlForum(driver): break # comment out - if count == 1: + if count == 10: break try: diff --git a/Forums/Cardingleaks/parser.py b/Forums/Cardingleaks/parser.py index 69d475f..98ddf3a 100644 --- a/Forums/Cardingleaks/parser.py +++ b/Forums/Cardingleaks/parser.py @@ -7,12 +7,12 @@ from datetime import timedelta import re # Here, we are importing BeautifulSoup to search through the HTML tree -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup, ResultSet, Tag # This is the method to parse the Description Pages (one page to each topic in the Listing Pages) -def cryptBB_description_parser(soup): +def cardingleaks_description_parser(soup: Tag): # Fields to be parsed @@ -26,135 +26,44 @@ def cryptBB_description_parser(soup): feedback = [] # 7 all feedbacks of each vendor (this was found in just one Forum and with a number format) addDate = [] # 8 all dates of each post - # Finding the topic (should be just one coming from the Listing Page) - - li = soup.find("td", {"class": "thead"}).find('strong') - topic = li.text - topic = re.sub("\[\w*\]", '', topic) - - topic = topic.replace(",","") - topic = topic.replace("\n","") - topic = cleanString(topic.strip()) - - # Finding the repeated tag that corresponds to the listing of posts - - # try: - posts = soup.find('table', {"class": "tborder tfixed clear"}).find('td', {"id": "posts_container"}).find_all( - 'div', {"class": "post"}) - - # For each message (post), get all the fields we are interested to: - - for ipost in posts: - - # Finding a first level of the HTML page - - post_wrapper = ipost.find('span', {"class": "largetext"}) - - # Finding the author (user) of the post - - author = post_wrapper.text.strip() - user.append(cleanString(author)) # Remember to clean the problematic characters - - # Finding the status of the author - - smalltext = ipost.find('div', {"class": "post_author"}) - - ''' - # Testing here two possibilities to find this status and combine them - if ipost.find('div', {"class": "deleted_post_author"}): - status.append(-1) - interest.append(-1) - reputation.append(-1) - addDate.append(-1) - post.append("THIS POST HAS BEEN REMOVED!") - sign.append(-1) - feedback.append(-1) - continue - ''' - - # CryptBB does have membergroup and postgroup - - membergroup = smalltext.find('div', {"class": "profile-rank"}) - postgroup = smalltext.find('div', {"class": "postgroup"}) - if membergroup != None: - membergroup = membergroup.text.strip() - if postgroup != None: - postgroup = postgroup.text.strip() - membergroup = membergroup + " - " + postgroup - else: - if postgroup != None: - membergroup = postgroup.text.strip() - else: - membergroup = "-1" - status.append(cleanString(membergroup)) - - # Finding the interest of the author - # CryptBB does not have blurb - blurb = smalltext.find('li', {"class": "blurb"}) - if blurb != None: - blurb = blurb.text.strip() - else: - blurb = "-1" - interest.append(cleanString(blurb)) - - # Finding the reputation of the user - # CryptBB does have reputation - author_stats = smalltext.find('div', {"class": "author_statistics"}) - karma = author_stats.find('strong') - if karma != None: - karma = karma.text - karma = karma.replace("Community Rating: ", "") - karma = karma.replace("Karma: ", "") - karma = karma.strip() - else: - karma = "-1" - reputation.append(cleanString(karma)) - - # Getting here another good tag to find the post date, post content and users' signature - - postarea = ipost.find('div', {"class": "post_content"}) - - dt = postarea.find('span', {"class": "post_date"}).text - # dt = dt.strip().split() - dt = dt.strip() - day=date.today() - if "Yesterday" in dt: - yesterday = day - timedelta(days=1) - yesterday = yesterday.strftime('%m-%d-%Y') - stime = dt.replace('Yesterday,','').strip() - date_time_obj = yesterday+ ', '+stime - date_time_obj = datetime.strptime(date_time_obj,'%m-%d-%Y, %I:%M %p') - elif "hours ago" in dt: - day = day.strftime('%m-%d-%Y') - date_time_obj = postarea.find('span', {"class": "post_date"}).find('span')['title'] - date_time_obj = datetime.strptime(date_time_obj, '%m-%d-%Y, %I:%M %p') - else: - date_time_obj = datetime.strptime(dt, '%m-%d-%Y, %I:%M %p') - stime = date_time_obj.strftime('%b %d, %Y') - sdate = date_time_obj.strftime('%I:%M %p') - addDate.append(date_time_obj) - - # Finding the post - - inner = postarea.find('div', {"class": "post_body scaleimages"}) - inner = inner.text.strip() - post.append(cleanString(inner)) - - # Finding the user's signature - - # signature = ipost.find('div', {"class": "post_wrapper"}).find('div', {"class": "moderatorbar"}).find('div', {"class": "signature"}) - signature = ipost.find('div', {"class": "signature scaleimages"}) - if signature != None: - signature = signature.text.strip() - # print(signature) - else: - signature = "-1" - sign.append(cleanString(signature)) - - # As no information about user's feedback was found, just assign "-1" to the variable - + li = soup.find("h1", {"class": "p-title-value"}) + topic = cleanString(li.text.strip()) + + post_list: ResultSet[Tag] = soup.find("div", {"class": "block-body js-replyNewMessageContainer"}).find_all("article", {"data-author": True}) + + for ipost in post_list: + username = ipost.get('data-author') + user.append(username) + + user_status = ipost.find("h5", {"class": "userTitle message-userTitle"}).text + status.append(cleanString(user_status.strip())) + + user_statistics: ResultSet[Tag] = ipost.find("div", {"class": "message-userExtras"}).find_all("dl", {"class": "pairs pairs--justified"}) + + user_reputation = "-1" + + for stat in user_statistics: + data_type = stat.find("span").get("data-original-title") + if data_type == "Points": + user_reputation = stat.find("dd").text + break + + reputation.append(cleanString(user_reputation.strip())) + + interest.append("-1") + + sign.append("-1") + + user_post = ipost.find("div", {"class": "message-content js-messageContent"}).text + post.append(cleanString(user_post.strip())) + feedback.append("-1") - + + datetime_text = ipost.find("ul", {"class": "message-attribution-main listInline"}).find("time").get("datetime") + datetime_obj = datetime.strptime(datetime_text, "%Y-%m-%dT%H:%M:%S%z") + addDate.append(datetime_obj) + + # Populate the final variable (this should be a list with all fields scraped) row = (topic, user, status, reputation, interest, sign, post, feedback, addDate) @@ -165,10 +74,10 @@ def cryptBB_description_parser(soup): # This is the method to parse the Listing Pages (one page with many posts) -def cryptBB_listing_parser(soup): +def cardingleaks_listing_parser(soup: Tag): nm = 0 # *this variable should receive the number of topics - forum = "OnniForums" # 0 *forum name + forum = "Cardingleaks" # 0 *forum name board = "-1" # 1 *board name (the previous level of the topic in the Forum categorization tree. # For instance: Security/Malware/Tools to hack Facebook. The board here should be Malware) author = [] # 2 *all authors of each topic @@ -181,55 +90,35 @@ def cryptBB_listing_parser(soup): # Finding the board (should be just one) - board = soup.find('span', {"class": "active"}).text - board = cleanString(board.strip()) - - # Finding the repeated tag that corresponds to the listing of topics - - itopics = soup.find_all('tr',{"class": "inline_row"}) - - for itopic in itopics: - - # For each topic found, the structure to get the rest of the information can be of two types. Testing all of them - # to don't miss any topic - - # Adding the topic to the topic list - try: - topics = itopic.find('span', {"class": "subject_old"}).find('a').text - except: - topics = itopic.find('span', {"class": "subject_new"}).find('a').text - topics = re.sub("\[\w*\]", '', topics) - topic.append(cleanString(topics)) - - # Counting how many topics we have found so far - - nm = len(topic) - - # Adding the url to the list of urls - try: - link = itopic.find('span', {"class": "subject_old"}).find('a').get('href') - except: - link = itopic.find('span',{"class": "subject_new"}).find('a').get('href') - href.append(link) - - # Finding the author of the topic - ps = itopic.find('div', {"class":"author smalltext"}).find('a').text - user = ps.strip() - author.append(cleanString(user)) - - # Finding the number of replies - columns = itopic.findChildren('td',recursive=False) - replies = columns[3].text - - posts.append(cleanString(replies)) - - # Finding the number of Views - tview = columns[4].text - views.append(cleanString(tview)) - - # If no information about when the topic was added, just assign "-1" to the variable - - addDate.append("-1") + li = soup.find("h1", {"class": "p-title-value"}) + board = cleanString(li.text.strip()) + + thread_list: ResultSet[Tag] = soup.find("div", {"class": "structItemContainer-group js-threadList"}).find_all("div", {"data-author": True}) + + nm = len(thread_list) + + for thread in thread_list: + thread_author = thread.get("data-author") + author.append(thread_author) + + thread_topic = thread.find("div", {"class": "structItem-title"}).text + topic.append(cleanString(thread_topic.strip())) + + thread_view = thread.find("dl", {"class": "pairs pairs--justified structItem-minor"}).find("dd").text + # Context text view count (i.e., 8.8K) to numerical (i.e., 8800) + if thread_view.find("K") > 0: + thread_view = str(int(float(thread_view.replace("K", "")) * 1000)) + views.append(thread_view) + + thread_posts = thread.find("dl", {"class": "pairs pairs--justified"}).find("dd").text + posts.append(cleanString(thread_posts.strip())) + + thread_href = thread.find("div", {"class": "structItem-title"}).find("a").get("href") + href.append(thread_href) + + thread_date = thread.find("li", {"class": "structItem-startDate"}).find("time").get("datetime") + datetime_obj = datetime.strptime(thread_date, "%Y-%m-%dT%H:%M:%S%z") + addDate.append(datetime_obj) return organizeTopics(forum, nm, board, author, topic, views, posts, href, addDate) diff --git a/Forums/CryptBB/crawler_selenium.py b/Forums/CryptBB/crawler_selenium.py index c69bd6a..a0ad16d 100644 --- a/Forums/CryptBB/crawler_selenium.py +++ b/Forums/CryptBB/crawler_selenium.py @@ -273,7 +273,7 @@ def crawlForum(driver): savePage(driver.page_source, topic + f"page{counter}") # very important # comment out - if counter == 2: + if counter == 10: break try: @@ -291,10 +291,10 @@ def crawlForum(driver): driver.back() # comment out - break + # break # comment out - if count == 1: + if count == 20: break try: diff --git a/Forums/Initialization/forumsList.txt b/Forums/Initialization/forumsList.txt index 801a104..6f635a1 100644 --- a/Forums/Initialization/forumsList.txt +++ b/Forums/Initialization/forumsList.txt @@ -1 +1 @@ -Altenens \ No newline at end of file +Cardingleaks \ No newline at end of file diff --git a/Forums/Initialization/geckodriver.log b/Forums/Initialization/geckodriver.log index 77ff601..0497a9d 100644 --- a/Forums/Initialization/geckodriver.log +++ b/Forums/Initialization/geckodriver.log @@ -14894,29 +14894,23 @@ JavaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: Type JavaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: TypeError: this.elem is null JavaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: TypeError: this.elem is null JavaScript error: moz-extension://ea77c2e9-b8db-4689-b4a8-30cbf0652171/content/staticNS.js, line 162: TypeError: can't access dead object -1689787519066 geckodriver INFO Listening on 127.0.0.1:50232 -1689787522517 mozrunner::runner INFO Running command: "C:\\Users\\minhkhoitran\\Desktop\\Tor Browser\\Browser\\firefox.exe" "--marionette" "--remote-debugging-port" "50233" "--remote-allow-hosts" "localhost" "-no-remote" "-profile" "C:\\Users\\MINHKH~1\\AppData\\Local\\Temp\\rust_mozprofileVprPHA" -console.log: "TorSettings: loadFromPrefs()" -console.log: "TorConnect: init()" -console.log: "TorConnect: Entering Initial state" -console.log: "TorConnect: Observed profile-after-change" -console.log: "TorConnect: Observing topic 'TorProcessExited'" -console.log: "TorConnect: Observing topic 'TorLogHasWarnOrErr'" -console.log: "TorConnect: Observing topic 'torsettings:ready'" -console.log: "TorSettings: Observed profile-after-change" -1689787522977 Marionette INFO Marionette enabled -console.log: "TorConnect: Will load after bootstrap => [about:blank]" -console.error: "Could not load engine blockchair-onion@search.mozilla.org: Error: Extension is invalid" -JavaScript error: resource://gre/modules/XULStore.jsm, line 66: Error: Can't find profile directory. -JavaScript error: resource://gre/modules/XULStore.jsm, line 66: Error: Can't find profile directory. -JavaScript error: resource://gre/modules/XULStore.jsm, line 66: Error: Can't find profile directory. -JavaScript error: resource://gre/modules/XULStore.jsm, line 66: Error: Can't find profile directory. -JavaScript error: resource://gre/modules/XPCOMUtils.jsm, line 161: TypeError: Cc[aContract] is undefined -DevTools listening on ws://localhost:50233/devtools/browser/d02a6124-5f75-45d7-9517-a98b7d46eeea -1689787524053 Marionette INFO Listening on port 50238 -1689787524078 RemoteAgent WARN TLS certificate errors will be ignored for this session +JavaScript error: resource://gre/modules/PromiseWorker.jsm, line 106: Error: Could not get children of file(C:\Users\minhkhoitran\AppData\Local\Temp\rust_mozprofileux3rvG\thumbnails) because it does not exist JavaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: TypeError: this.elem is null JavaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: TypeError: this.elem is null +1689792192254 Marionette INFO Stopped listening on port 50161 +JavaScript error: resource:///modules/Interactions.jsm, line 209: NS_ERROR_FAILURE: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIUserIdleService.removeIdleObserver] +!!! error running onStopped callback: TypeError: callback is not a function +JavaScript error: resource:///modules/sessionstore/SessionFile.jsm, line 375: Error: _initWorker called too early! Please read the session file from disk first. +JavaScript error: resource://gre/modules/PromiseWorker.jsm, line 106: Error: Could not get children of file(C:\Users\minhkhoitran\AppData\Local\Temp\rust_mozprofileux3rvG\thumbnails) because it does not exist +[Parent 3988, IPC I/O Parent] WARNING: file /var/tmp/build/firefox-b6010b1466c9/ipc/chromium/src/base/process_util_win.cc:167 +1689792192665 RemoteAgent ERROR unable to stop listener: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIWindowMediator.getEnumerator]" nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)" location: "JS frame :: chrome://remote/content/cdp/observers/TargetObserver.jsm :: stop :: line 64" data: no] Stack trace: stop()@TargetObserver.jsm:64 +unwatchForTabs()@TargetList.jsm:70 +unwatchForTargets()@TargetList.jsm:37 +destructor()@TargetList.jsm:109 +stop()@CDP.jsm:104 +close()@RemoteAgent.jsm:138 +avaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: TypeError: this.elem is null +JavaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: TypeError: this.elem is null JavaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: TypeError: this.elem is null JavaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: TypeError: this.elem is null JavaScript error: https://altenens.is/js/xenmake/headroom.min.js, line 155: TypeError: this.elem is null @@ -14987,3 +14981,40 @@ unwatchForTargets()@TargetList.jsm:37 destructor()@TargetList.jsm:109 stop()@CDP.jsm:104 close()@RemoteAgent.jsm:138 +1689790072936 geckodriver INFO Listening on 127.0.0.1:50011 +1689790078108 mozrunner::runner INFO Running command: "C:\\Users\\minhkhoitran\\Desktop\\Tor Browser\\Browser\\firefox.exe" "--marionette" "--remote-debugging-port" "50012" "--remote-allow-hosts" "localhost" "-no-remote" "-profile" "C:\\Users\\MINHKH~1\\AppData\\Local\\Temp\\rust_mozprofile7s5IYY" +console.log: "TorSettings: loadFromPrefs()" +console.log: "TorConnect: init()" +console.log: "TorConnect: Entering Initial state" +console.log: "TorConnect: Observed profile-after-change" +console.log: "TorConnect: Observing topic 'TorProcessExited'" +console.log: "TorConnect: Observing topic 'TorLogHasWarnOrErr'" +console.log: "TorConnect: Observing topic 'torsettings:ready'" +console.log: "TorSettings: Observed profile-after-change" +1689790078736 Marionette INFO Marionette enabled +console.log: "TorConnect: Will load after bootstrap => [about:blank]" +console.error: "Could not load engine blockchair-onion@search.mozilla.org: Error: Extension is invalid" +JavaScript error: resource://gre/modules/XULStore.jsm, line 66: Error: Can't find profile directory. +JavaScript error: resource://gre/modules/XULStore.jsm, line 66: Error: Can't find profile directory. +JavaScript error: resource://gre/modules/XULStore.jsm, line 66: Error: Can't find profile directory. +JavaScript error: resource://gre/modules/XULStore.jsm, line 66: Error: Can't find profile directory. +JavaScript error: resource://gre/modules/XPCOMUtils.jsm, line 161: TypeError: Cc[aContract] is undefined +DevTools listening on ws://localhost:50012/devtools/browser/d82f5bb0-7a3f-46d2-83b0-9451c01d88b2 +1689790080352 Marionette INFO Listening on port 50017 +1689790080437 RemoteAgent WARN TLS certificate errors will be ignored for this session +JavaScript error: resource://gre/modules/PromiseWorker.jsm, line 106: Error: Could not get children of file(C:\Users\minhkhoitran\AppData\Local\Temp\rust_mozprofile7s5IYY\thumbnails) because it does not exist +JavaScript error: https://cardingleaks.ws/js/xenconcept/hidebbcode/message.min.js?_v=516cdbc2, line 1: TypeError: XF.QuickReply is undefined +1689790828818 Marionette INFO Stopped listening on port 50017 +JavaScript error: resource:///modules/Interactions.jsm, line 209: NS_ERROR_FAILURE: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIUserIdleService.removeIdleObserver] +!!! error running onStopped callback: TypeError: callback is not a function +JavaScript error: resource:///modules/sessionstore/SessionFile.jsm, line 375: Error: _initWorker called too early! Please read the session file from disk first. +JavaScript error: resource://gre/modules/PromiseWorker.jsm, line 106: Error: Could not get children of file(C:\Users\minhkhoitran\AppData\Local\Temp\rust_mozprofile7s5IYY\thumbnails) because it does not exist + +###!!! [Parent][MessageChannel] Error: (msgtype=0x390076,name=PContent::Msg_DestroyBrowsingContextGroup) Closed channel: cannot send/recv + +1689790829319 RemoteAgent ERROR unable to stop listener: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIWindowMediator.getEnumerator]" nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)" location: "JS frame :: chrome://remote/content/cdp/observers/TargetObserver.jsm :: stop :: line 64" data: no] Stack trace: stop()@TargetObserver.jsm:64 +unwatchForTabs()@TargetList.jsm:70 +unwatchForTargets()@TargetList.jsm:37 +destructor()@TargetList.jsm:109 +stop()@CDP.jsm:104 +close()@RemoteAgent.jsm:138 diff --git a/Forums/Initialization/prepare_parser.py b/Forums/Initialization/prepare_parser.py index 4c6a407..1c624b3 100644 --- a/Forums/Initialization/prepare_parser.py +++ b/Forums/Initialization/prepare_parser.py @@ -6,6 +6,7 @@ import os, re import shutil from Forums.DB_Connection.db_connection import * from Forums.BestCardingWorld.parser import * +from Forums.Cardingleaks.parser import * from Forums.CryptBB.parser import * from Forums.OnniForums.parser import * from Forums.Altenens.parser import * @@ -149,6 +150,8 @@ def new_parse(forum, url, createLog): if forum == "BestCardingWorld": rmm = bestcardingworld_description_parser(soup) + elif forum == "Cardingleaks": + rmm = cardingleaks_description_parser(soup) elif forum == "CryptBB": rmm = cryptBB_description_parser(soup) elif forum == "OnniForums": @@ -230,6 +233,8 @@ def new_parse(forum, url, createLog): if forum == "BestCardingWorld": rw = bestcardingworld_listing_parser(soup) + elif forum == "Cardingleaks": + rw = cardingleaks_listing_parser(soup) elif forum == "CryptBB": rw = cryptBB_listing_parser(soup) elif forum == "OnniForums": diff --git a/Forums/Procrax/parser.py b/Forums/Procrax/parser.py index 7c9c463..117fdca 100644 --- a/Forums/Procrax/parser.py +++ b/Forums/Procrax/parser.py @@ -110,7 +110,7 @@ def procrax_listing_parser(soup: Tag): datetime_obj = datetime.strptime(thread_date, "%Y-%m-%dT%H:%M:%S%z") addDate.append(datetime_obj) - thread_link = thread.find("div", {"class": "structItem-title"}).find('a').get('href') + thread_link: str = thread.find("div", {"class": "structItem-title"}).find('a').get('href') href.append(thread_link) diff --git a/Forums/Utilities/__pycache__/utilities.cpython-311.pyc b/Forums/Utilities/__pycache__/utilities.cpython-311.pyc index 0e77224374cde626619b0db72f93e91379f93bd2..1104f930b66b859817113aef7a70b964b73669b7 100644 GIT binary patch delta 68 zcmZoDn^?xXoR^o20SL;(ccdKJ$m?t&u5FW&Sr#2<7pqlVQkj!#8>18*7o!xrIm{xI Xk=yMA(+RJOg5g&L!#8iU^kM`6+fWu? delta 51 zcmbPO)=