From aba1a0e254baccc9f9d89a8efbdbe0c5b9a2a951 Mon Sep 17 00:00:00 2001 From: Jan Eggers <janeggers@untergeekPro.local> Date: Tue, 31 Dec 2024 10:50:46 +0100 Subject: [PATCH] Phase-1-Proof-of-concept --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 24 +- .~lock.user_posts.csv# | 1 - README.md | 31 +- TODO | 11 +- __init__.py | 0 async_reader.py | 82 ----- check_bsky_profile.py | 283 ++++++++++++++++++ detectora.py | 4 +- .../API-Dokumentation Detectora.pdf | Bin imagecheck.py | 29 +- read_posts.py | 115 ------- user_posts.csv | 55 ---- 13 files changed, 338 insertions(+), 297 deletions(-) create mode 100644 .DS_Store delete mode 100644 .~lock.user_posts.csv# create mode 100644 __init__.py delete mode 100644 async_reader.py create mode 100644 check_bsky_profile.py rename API-Dokumentation Detectora.pdf => docs/API-Dokumentation Detectora.pdf (100%) delete mode 100644 read_posts.py delete mode 100644 user_posts.csv diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6d483015ca9755ebd2f298d5df3d917eefd799c6 GIT binary patch literal 6148 zcmeHKJ8Hu~5S@ty5x8;ba<AYUY>a#YUm)0mq_9{+NUzRyPLSKU^J&tg%$rY)jiNxB zK*9{nKJCmrt@H|whKP9i+%AY_L{!5C=^)F{BriU&8;?{#mV4|T7VB;4_JhjtfN(2n zsnC|5zTls8U1^%lu53_W-@cyS-(HS)m)x$j-{$v^KYq6Hr;bc05DJ6>p+G3`cNIX- z7OSU*v4#SnKqzolK)w$NE|?vAL;ZAM(GdVBzwv6g)>#6WBmm5gy&)nnHY(7l>>~yn z9r5IK*|9e?I<cON`}oQ16AJ6;h$k&4&W5pu0-?aQ0u$TLr2ntrBlG_?DR!YiDDbZo z;Hp_SOT3cz*1^k3uTAiK_@|*(%du!H25KwDMq2UwqAtlb^4YOBR662H2gZ+p=n@tR H{DuM_xH~Gs literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index 82f9275..05e5cd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Runtime results +bsky-checks/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -130,30 +133,9 @@ ENV/ env.bak/ venv.bak/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - # mkdocs documentation /site -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore diff --git a/.~lock.user_posts.csv# b/.~lock.user_posts.csv# deleted file mode 100644 index dc2c062..0000000 --- a/.~lock.user_posts.csv# +++ /dev/null @@ -1 +0,0 @@ -,janeggers,untergeekPro.local,28.12.2024 09:30,file:///Users/janeggers/Library/Application%20Support/LibreOffice/4; \ No newline at end of file diff --git a/README.md b/README.md index 530445e..0ca420f 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,23 @@ Das Fernziel ist eine Recherche zu KI-Inhalten im Wahlkampf mit zwei Stoßrichtu - Verdachtsfälle besonders krasser Fälschungen finden - Gesamt-KI-Anteil nach Partei/Person -## Der Plan +## Wie einsetzen? -### Phase 1: Bluesky +- Mit ```git clone github.com/JanEggers-hr/aichecker``` in ein Verzeichnis klonen +- In dem Verzeichnis eine ```.env``` Datei anlegen mit den Keys: +``` +OPENAI_API_KEY="" # für die Bildbeschreibungen +DETECTORA_API_KEY="" # für die Textanalyse +AIORNOT_API_KEY="" # für die Bildanalyse +``` +- Programm im Verzeichnis starten mit ```python check_bsky_profile.py```` -- Beliebiges Account vier Wochen scannen -- AIorNot-API einbauen https://docs.aiornot.com/ - - Videos checken: - - Audiospur extrahieren - - Audiospur checken -- hive-API einbauen -- Ausgabe: Vermuteter KI-Anteil +Programm ist voreingestellt auf 20 Posts. (Sonst die Variable limit in Zeile 15 von check_bsky_profile.py ändern.) Die Schwelle für einen "echten" KI-Text-Post ist auf 80% (0.8) eingestellt. -### Phase 2: Chemtrail App -- Dash-App auf dem Server zum Check eines Handles +Das Skript legt im Verzeichnis ```bsky-checks``` eine CSV-Datei mit der Analyse nach Posts an, mit dem Namen des Profils: {handle}.CSV -### Phase 3: 4CAT -- 4CAT-Server aufsetzen -- "processor" für KI-Check einbauen -- +## Achtung! + +- Die Detectora-API setzt ein älteres Modell ein, das die GPT4-Erkennung nicht so gut schafft. +- AIORNOT ist teuer! Standardmodell: 100 API-Calls pro Monat für 5$ (bei Abschluss Jahresabo) +- Immer dran denken: Die Detektoren liefern Wahrscheinlichkeiten, keine Gewissheiten. diff --git a/TODO b/TODO index 0ad473a..4805df2 100644 --- a/TODO +++ b/TODO @@ -1,9 +1,5 @@ # App: Chemtrail -## Auswertung -- Anzahl verdächtige Texte -- Anzahl verdächtige Bilder -- Check bis Datum ## UI - Dash-App @@ -14,3 +10,10 @@ - Anzeige Ergebnisse letzter Check - Neue Posts holen und auswerten - CSV aktualisieren und Download-Link anbieten + +## Telegram +- Umbau auf Telegram-Channel + +## 4CAT +- 4Cat-Server aufsetzen +- "Processor" schreiben diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/async_reader.py b/async_reader.py deleted file mode 100644 index b67c0ec..0000000 --- a/async_reader.py +++ /dev/null @@ -1,82 +0,0 @@ -# NOT RUN - - - -# Define the global posts list -posts = [] - -# Nicking the code to read from the bsky firehose here: -# https://gist.github.com/stuartlangridge/20ffe860fee0ecc315d3878c1ea77c35 -def append_post(json_data): - # Parse JSON data and append to - - -# -# Basic idea: -# - Get a feed. -# - Collect basic data on the author -# - Collect four weeks' posts -# - Analyse each post: -# - Check text with Hive and Detectora -# - Check images with Hive and AIorNot -# - Check video by isolating audio to AIorNot - -import json -from atproto_client.models import get_or_create -from atproto import CAR, models -from atproto_firehose import FirehoseSubscribeReposClient, parse_subscribe_repos_message - - - -class JSONExtra(json.JSONEncoder): - """raw objects sometimes contain CID() objects, which - seem to be references to something elsewhere in bluesky. - So, we 'serialise' these as a string representation, - which is a hack but whatevAAAAR""" - def default(self, obj): - try: - result = json.JSONEncoder.default(self, obj) - return result - except: - return repr(obj) - -client = FirehoseSubscribeReposClient() - -# all of this undocumented horseshit is based on cargo-culting the bollocks out of -# https://github.com/MarshalX/atproto/blob/main/examples/firehose/sub_repos.py -# and -# https://github.com/MarshalX/bluesky-feed-generator/blob/main/server/data_stream.py - -def on_message_handler(message): - commit = parse_subscribe_repos_message(message) - if not isinstance(commit, models.ComAtprotoSyncSubscribeRepos.Commit): - return - car = CAR.from_bytes(commit.blocks) - for op in commit.ops: - if op.action in ["create"] and op.cid: - raw = car.blocks.get(op.cid) - cooked = get_or_create(raw, strict=False) - if cooked.py_type == "app.bsky.feed.post": - # other types include "app.bsky.feed.like" etc which we ignore - # note that this data does not include who posted this skeet - # or possibly it does as a "CID" which you have to look up somehow - # who the hell knows? not me - - print(json.dumps(raw, cls=JSONExtra, indent=2)) - - -# Also look at this: -# https://social-media-ethics-automation.github.io/book/bsky/ch04_data/05_data_python_platform/03_demo_data_from_platform.html - -def main(): - client.start(on_message_handler) - - return - - - - - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/check_bsky_profile.py b/check_bsky_profile.py new file mode 100644 index 0000000..266baf4 --- /dev/null +++ b/check_bsky_profile.py @@ -0,0 +1,283 @@ +# Funktionen zum Check von Bluesky-Konten +# +# 12-2024 Jan Eggers + + +import json +import pandas as pd +from detectora import query_detectora +from imagecheck import query_aiornot +from bildbeschreibung import gpt4_description +import requests +import os + +# Konstante +d_thresh = .8 # 80 Prozent +limit = 25 # Posts für den Check + +def detectora_wrapper(text: str): + # Verpackung. Fügt nur den "Fortschrittsbalken" hinzu. + print("?", end="") + score = query_detectora(text) + if score is None: + print("\b_",end="") + else: + print(f"\b{'X' if score >= d_thresh else '.'}",end="") + return score + +def aiornot_wrapper(did,embed): + # Verpackung für die AIORNOT-Funktion: + # Checkt, ob es überhaupt ein Embed gibt, + # und ob es ein Bild enthält. + # Wenn ja: geht durch die Bilder und erstellt KI-Beschreibung und KI-Einschätzung + print("?",end="") + if 'images' in embed: + images = embed['images'] + desc = [] + for i in images: + # Construct an URL for the image thumbnail (normalised size) + link = i['image']['ref']['$link'] + i_url = f"https://cdn.bsky.app/img/feed_thumbnail/plain/{did}/{link}" + aiornot_report = query_aiornot(i_url) + # Beschreibung: https://docs.aiornot.com/#5b3de85d-d3eb-4ad1-a191-54988f56d978 + gpt4_desc = gpt4_description(i_url) + desc.append({ + 'link_id': link, + 'aiornot_score': aiornot_report['verdict'], + 'aiornot_confidence': aiornot_report['ai']['confidence'], + 'aiornot_generator': aiornot_report['generator'], + 'gpt4_description': gpt4_desc, + }) + print(f"\b{'X' if aiornot_report['verdict'] != 'human' else '.'}",end="") + return desc + else: + print("\b_",end="") + return None + +def call_get_author_feed(author: str, limit: int=50, cursor= None) -> list: + # Sucht den Post-Feed für das Bluesky-Konto author + # author kann did oder handle sein + # Gibt ein dict zurück aus: + # 'cursor' + # 'feed' -> Liste der einzelnen Posts + data = { + 'actor': author, + 'limit': limit, + 'cursor': cursor, + } + headers = { + 'Content-Type': 'application/json', + + } + try: + response = requests.get("https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed", + params=data, + headers=headers) + if response.status_code == 200: + # Success + posts = response.json() + # Falls weniger Posts existieren als das Limit, wird kein cursor zurückgegeben, + # in diesem Fall: cursor-Element noch dazupacken + if not 'cursor' in posts: + posts['cursor'] = None + return posts + elif response.status_code == 400: + print("Bluesky Public: Fehlerhafte API-Anfrage") + return None + elif response.status_code == 401: + print("Zugriff auf Bluesky Public nicht erlaubt") + except Exception as e: + print("Fehler beim Verbinden mit der Bluesky-API:", str(e)) + return None + return response[''] + +def call_get_profile(handle: str) -> list: + # Gibt das gefundenen Profil zurück. + data = { + 'actor': handle, + } + headers = { + 'Content-Type': 'application/json', + + } + try: + response = requests.get("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile", + params=data, + headers=headers) + if response.status_code == 200: + # Success + return response.json() + elif response.status_code == 400: + print("Bluesky Public: Fehlerhafte API-Anfrage") + return None + elif response.status_code == 401: + print("Zugriff auf Bluesky Public nicht erlaubt") + except Exception as e: + print("Fehler beim Verbinden mit der Bluesky-API:", str(e)) + return None + return response[''] + +def fetch_user_posts(handle: str, limit: int = 100) -> list: + profile = call_get_profile(handle) + did = profile['did'] + posts = [] + # Fetch timeline for the user (latest posts first) + cursor = None + while len(posts) < limit: + feed = call_get_author_feed(did, limit, cursor) + if not feed['feed']: + break + cursor = feed['cursor'] + for item in feed['feed']: + post =item['post'] + # Extrahiere Info zum einzelnen Post + post_data = { + 'author_handle': post['author']['handle'], # Bluesky-Handle + 'author_display_name': post['author']['displayName'], # Klarname + 'author_avatar': post['author']['avatar'], # Bluesky-Link zum Avatar-Bild + 'author_did': post['author']['did'], # Bluesky-ID + 'created_at': post['record']['createdAt'], # Angelegt... + # 'indexed_at': item[2], + 'text': post['record']['text'], # Text des Posts, falls vorhanden + 'uri': post['uri'], # Link auf den Post + 'cid': post['cid'], + 'like_count': post['likeCount'], # Anzahl von Likes + 'reply_count': post['replyCount'], # Anzahl von Antworten + 'repost_count': post['repostCount'], # Anzahl von Reposts + 'quote_count': post['quoteCount'], # Anzahl von Zitat-Reposts + 'language': post['record'].get('langs') if 'langs' in post['record'] else '', + # Embedded media: images, external, record + # (external sind Links ins Internet, images sind Bilder, record sind eingebettete Posts) + # Image alt, file, and URI + # Das Embed wird einfach so als dict in die Zelle geschrieben und gesondert ausgewertet + 'embed': post['record']['embed'] if 'embed' in post['record'] else '' + # Embed URI and description + # 'external_description': getattr(post['embed']['external'],'description',''), + # 'external_uri': getattr(post['embed']['external'],'uri',''), + + } + posts.append(post_data) + + return posts[:limit] + +def check_handle(handle:str, limit:int = 20): + # Konto und Anzahl der zu prüfenden Posts + if handle == '': + return None + if handle[0]== '@': + handle = handle[1:] + + # Fetch the most recent posts from the specified user + posts = fetch_user_posts(handle, limit) + + if not posts: + print(f"Keine Posts im Feed für Handle {handle}.") + return + + # Convert posts to a DataFrame + df = pd.DataFrame(posts) + + # Now add probability check for each post text + print("Checke Texte:") + df['detectora_ai_score'] = df['text'].apply(detectora_wrapper) + + # Now add "ai" or "human" assessment for images + print("\nChecke Bilder:") + df['aiornot_ai_score'] = df.apply(lambda row: aiornot_wrapper(row['author_did'], row['embed']), axis=1) + print() + return df + +def call_find_handles(text): + # Ruft die Bluesky-Public-API direkt auf und bekommt ein JSON zurück, das ein Element + # actors mit einer Liste der gefundenen Konten enthält + data = { + 'q': text, + } + headers = { + 'Content-Type': 'application/json', + + } + try: + response = requests.get("https://public.api.bsky.app/xrpc/app.bsky.actor.searchActors", + params=data, + headers=headers) + if response.status_code == 200: + # Success + return response.json() + elif response.status_code == 400: + print("Bluesky Public: Fehlerhafte API-Anfrage") + return None + elif response.status_code == 401: + print("Zugriff auf Bluesky Public nicht erlaubt") + except Exception as e: + print("Fehler beim Verbinden mit der Bluesky-API:", str(e)) + return None + return response[''] + + +def find_handles(text): + # Sucht Bluesky-Handles und gibt eine Liste von Handles zurück + actors = call_find_handles(text) + handles = [a['handle'] for a in actors['actors']] + return handles + + +if __name__ == "__main__": + # Bluesky- + handle_str = input("Erstes Handle mit diesen Zeichen wählen: ") + handle = find_handles(handle_str)[0] + print(handle) + # Diese Funktion holt die Infos zum Profil 'handle': + # Erwartet einen String, gibt ein dict zurück. + # Beschreibung: https://docs.bsky.app/docs/api/app-bsky-actor-get-profile + # Manchmal existieren Felder wie 'description' nicht. + profile = call_get_profile(handle) + author = profile['did'] + print(author) + # Diese Funktion holt die Posts. + # 'author' darf irgendwas sein: handle, did... wir nehmen die did + # Gibt ein dict zurück: im Schlüssel 'feed' sind die einzelnen Posts gespeichert, + # im Key 'cursor' gibt es das Datum des frühesten abgefragten Posts zurück (es sei denn, + # es sind weniger Posts als limit, dann ist cursor leer.) + # Beschreibung: https://docs.bsky.app/docs/api/app-bsky-feed-get-author-feed + posts = call_get_author_feed(author, limit = limit) + # In diesem Demo-Programm werden die Posts hier noch nicht ausgewertet. + # Das passiert in der Extra-Funktion check_handle unten. + print(posts['cursor']) + # Funktion prüft die letzten ```limit``` Posts (voreingestellt auf 20) + # Erwartet ein Handle oder ein did - wir nehmen DID + # Gibt ein Dataframe zurück; Struktur ist oben in der Funktion beschrieben. + # Wichtigster Punkt: Ergebnis des KI-Checks in den Spalten + # - 'detectora_ai_score': Detectora-Score des Post-Textes (als real) + # - 'aiornot_ai_score': + df = check_handle(author, limit = limit) + n_posts = len(df) + print(f'\n\nAnalyse des Kontos @{handle} ({profile['displayName']}) seit {profile['createdAt']}- {profile['followersCount']} Follower') + print(f'{profile.get('description','---')}\n') + print(f'Anzahl der analysierten Posts: {n_posts}') + print(f"Durchschnittliche KI-Text-Wahrscheinlichkeit: {df['detectora_ai_score'].mean()}") + detectora_posts_df = df[df['detectora_ai_score'] >= d_thresh] + print(f"Anzahl von Posts über einer detectora-Schwelle von {d_thresh*100:.1f}%: {len(detectora_posts_df)}") + image_posts = [post for post in df['aiornot_ai_score'].to_list() if post is not None] + # Liste auspacken, nur die Dicts ohne None-Elemente + image_list = [item for sublist in image_posts for item in sublist] + ai_list = [item for item in image_list if item['aiornot_score']!='human'] + if len(image_list) == 0: + p_ai = 0 + else: + p_ai = len(ai_list)/len(image_list) * 100 + print(f"Anzahl der Bilder: {len(image_list)}, verdächtig: {len(ai_list)} ({p_ai:.1f})%") + # Jetzt die Daten abspeichern + # Fals das Directory nicht existiert, anlegen + if not os.path.exists('bsky-checks'): + os.makedirs('bsky-checks') + + # Read existing file if it exists + filename = f'bsky-checks/{handle}.csv' + if os.path.exists(filename): + existing_df = pd.read_csv(filename) + df = pd.concat([existing_df, df]).drop_duplicates(subset=['uri']).reset_index(drop=True) + + df.to_csv(f'bsky-checks/{handle}.csv', index=False) # Save to CSV for example + + \ No newline at end of file diff --git a/detectora.py b/detectora.py index c5a538c..8409384 100644 --- a/detectora.py +++ b/detectora.py @@ -24,6 +24,8 @@ import os api_url = "https://backendkidetektor-apim.azure-api.net/watson" def query_detectora(text): + if text == '': + return None data = { 'query': text, } @@ -41,7 +43,7 @@ def query_detectora(text): # Success return response.json()['fake_probability'] elif response.status_code == 400: - print("DETECTORA: Fehlerhafte API-Anfrage") + print(f"DETECTORA: Fehlerhafte API-Anfrage: \'{data['query']}\'") return None elif response.status_code == 401: print(f"DETECTORA-API-Key 'api_key' nicht gültig") diff --git a/API-Dokumentation Detectora.pdf b/docs/API-Dokumentation Detectora.pdf similarity index 100% rename from API-Dokumentation Detectora.pdf rename to docs/API-Dokumentation Detectora.pdf diff --git a/imagecheck.py b/imagecheck.py index 19b9c24..e6c2e85 100644 --- a/imagecheck.py +++ b/imagecheck.py @@ -5,13 +5,22 @@ from bildbeschreibung import ai_description import requests import json import os +import time # Konstanten # endpoint_url = "https://api.aiornot.com/v1/reports/image" def query_aiornot(image): # Erwartet URI eines Bildes - # AIORNot-API-Dokumentation: https://docs.aiornot.com/ + # Wichtigste Rückgabewerte im dict: + # - 'verdict' ('human' oder 'ai') + # - 'ai'/'confidence' (wie sicher ist sich das Modell?) + # - 'generator' ist ein dict, das für die vier geprüften Modelle + # 'dall_e', 'stable_diffusion', 'this_person_does_not_exist' und 'midjourney' + # jeweils einen 'confidence'-Wert angibt. + # + # AIORNot-API-Dokumentation: https://docs.aiornot.com/#5b3de85d-d3eb-4ad1-a191-54988f56d978 + data = json.dumps({ 'object': image, }) @@ -28,14 +37,28 @@ def query_aiornot(image): ) if response.status_code == 200: # Success - return response.json()['report']['verdict'] + return response.json()['report'] elif response.status_code == 400: print("AIORNOT: Fehlerhafte API-Anfrage") return None elif response.status_code == 401: print(f"AIORNOT-API-Key 'api_key' nicht gültig") + return None + elif response.status_code == 429: + # Zu viele Anfragen; also warten und nochmal fragen + time.sleep(1) + response = requests.post(endpoint_url, + headers=headers, + data=data + ) + # Immer noch 429? Dann sind wahrscheinlich die Credits aufgebraucht + if response.status_code == 429: + print("AIORNOT: Credits verbraucht") + return None + else: + return response.json()['report'] except Exception as e: print("Fehler beim Verbinden mit der AIORNOT-API:", str(e)) return None - return response[''] + return None diff --git a/read_posts.py b/read_posts.py deleted file mode 100644 index 51a5d51..0000000 --- a/read_posts.py +++ /dev/null @@ -1,115 +0,0 @@ -import json -import pandas as pd -from atproto import Client, models -from detectora import query_detectora -from imagecheck import query_aiornot -from bildbeschreibung import gpt4_description - -def aiornot_wrapper(did,embed): - # Verpackung für die AIORNOT-Funktion: - # Checkt, ob es überhaupt ein Embed gibt, - # und ob es ein Bild enthält. - # Wenn ja: nimmt das erste Bild und - # erstellt KI-Beschreibung und KI-Einschätzung - if embed is None or embed == '': - return None - images = getattr(embed,'images',None) - if images is None: - return None - desc = [] - for i in images: - # Construct an URL for the image thumbnail (normalised size) - i_url = f"https://cdn.bsky.app/img/feed_thumbnail/plain/{did}/{i.image.ref.link}" - aiornot_score = query_aiornot(i_url) - gpt4_desc = gpt4_description(i_url) - desc.append({'aiornot_score': aiornot_score, - 'gpt4_description': gpt4_desc}) - return desc - - -def fetch_user_posts(handle: str, limit: int = 100) -> list: - # Initialize the Bluesky client (unauthenticated) - client = Client(base_url="https://api.bsky.app") - try: - # Fetch the user ID from the handle - profile = client.app.bsky.actor.get_profile({'actor': handle}) - user_id = profile.did - - # Initialize an empty list to store posts - posts = [] - - # Fetch timeline for the user (latest posts first) - cursor = None - while len(posts) < limit: - if cursor is not None: - feed = client.app.bsky.feed.get_author_feed({'actor':user_id, - 'limit':(min(limit - len(posts), 50)), - 'cursor': cursor, - }) - else: - feed = client.app.bsky.feed.get_author_feed({'actor':user_id, - 'limit': (min(limit - len(posts), 100)), - }) - if not feed['feed']: - break - cursor = feed['cursor'] - for item in feed['feed']: - post = getattr(item,'post') - # Extract basic post information - post_data = { - 'author_handle': getattr(post['author'],'handle',''), - 'author_display_name': getattr(post['author'], 'display_name', ''), - 'author_did': getattr(post['author'], 'did', ''), - 'created_at': getattr(post['record'], 'created_at', ''), - # 'indexed_at': item[2], - - 'text': getattr(post['record'], 'text', ''), - 'uri': post['uri'], - 'cid': post['cid'], - 'like_count': post['like_count'], - 'reply_count': post['reply_count'], - 'repost_count': post['repost_count'], - 'quote_count': post['quote_count'], - 'language': getattr(post['record'], 'langs', [''])[0] if hasattr(post['record'], 'langs') else '', - # Embedded media: images, external, record - # Image alt, file, and URI - 'embed': getattr(post['record'],'embed','') - # Embed URI and description - # 'external_description': getattr(post['embed']['external'],'description',''), - # 'external_uri': getattr(post['embed']['external'],'uri',''), - - } - posts.append(post_data) - - return posts[:limit] - - except Exception as e: - print(f"An error occurred: {e}") - return [] - -def check_handle(handle = 'lion-c.bsky.social', limit = 20): - # Define the Bluesky handle and number of posts to fetch - # Remove the @ before handle strings - - # Fetch the most recent posts from the specified user - posts = fetch_user_posts(handle, limit) - - if not posts: - print("No posts fetched. Please check the handle and try again.") - return - - # Convert posts to a DataFrame - df = pd.DataFrame(posts) - - # Now add probability check for each post text - df['detectora_ai_score'] = df['text'].apply(query_detectora) - - # Now filter those - df['aiornot_ai_score'] = df.apply(lambda row: aiornot_wrapper(row['author_did'], row['embed']), axis=1) - return df - -if __name__ == "__main__": - df = check_handle() - print(f"Durchschnittliche KI-Text-Wahrscheinlichkeit: {df['detectora_ai_score'].mean()}") - df.to_csv('user_posts.csv', index=False) # Save to CSV for example - \ No newline at end of file diff --git a/user_posts.csv b/user_posts.csv deleted file mode 100644 index 22f2782..0000000 --- a/user_posts.csv +++ /dev/null @@ -1,55 +0,0 @@ -author_handle,author_display_name,author_did,created_at,text,uri,cid,like_count,reply_count,repost_count,quote_count,language,embed,detectora_ai_score -tendar.bsky.social,(((Tendar))),did:plc:ernjxefnyk2hhwhbd3zblykf,2024-12-27T18:26:10.347Z,"The case around oil tankers Eagle-S, which is most certainly responsible for damaging the Estlink-2 undersea cable, is getting even more interesting. It appears that ship was not only involved in sabotage but in espionage, as well.",at://did:plc:ernjxefnyk2hhwhbd3zblykf/app.bsky.feed.post/3lecmgh7cz22o,bafyreichh4ydy6hw73goyvsfughj7ft65q4ly2ptiaeexk7tdv66tuwxzi,953,29,252,17,en,"images=[Image(alt='', image=BlobRef(mime_type='image/jpeg', size=294530, ref=IpldLink(link='bafkreih5fhonli4qpbv2xqqctqouv473nahlbzjfzedgchgv4wr7bvzqie'), py_type='blob'), aspect_ratio=AspectRatio(height=1002, width=1170, py_type='app.bsky.embed.defs#aspectRatio'), py_type='app.bsky.embed.images#image')] py_type='app.bsky.embed.images'",0.6172474026679993 -barryalanpiatoff.bsky.social,Barry Alan Piatoff,did:plc:xewv5wqmdxmbf326uidz24rn,2024-12-25T00:01:41.610Z,"""For Bluesky, Massive User Uptick Brings Growing Pains and Divisive Bots. It’s not just human users who’ve been flocking to Bluesky but also bots, including those designed to create partisan division."" via The Hollywood Reporter - -@lion-c.bsky.social is quoted & seems to understand the bot problem",at://did:plc:xewv5wqmdxmbf326uidz24rn/app.bsky.feed.post/3le3nrniflc2w,bafyreihdxpx5r3yi6kbnmtprxvvbgbewnt77qn7sv2yywsr2wn3t6ivjpa,3,0,3,0,en,"external=External(description='It’s not just human users who’ve been flocking to Bluesky but also bots, including those designed to create partisan division.', title='For Bluesky, Massive User Uptick Brings Growing Pains and Divisive Bots', uri='https://www.hollywoodreporter.com/news/general-news/bluesky-user-growth-brings-growing-pains-and-divisive-bots-1236093735/?link_source=ta_thread_link&taid=676b2ef7b0d84200018b70a5&utm_campaign=trueanthem&utm_medium=social&utm_source=threads', thumb=BlobRef(mime_type='image/jpeg', size=795675, ref=IpldLink(link='bafkreiaoza25gxeytmjy4adjxyy6qm2iqtnjob6huatjvqgjl2xassudea'), py_type='blob'), py_type='app.bsky.embed.external#external') py_type='app.bsky.embed.external'",0.0009211198193952441 -conspirator0.bsky.social,Conspirador Norteño,did:plc:7n2gzbzn4xus4nghzfpjgli3,2024-12-21T17:29:26.644Z,"In the two weeks since this thread was posted, the ""passionate about"" spam network has grown to over 15 thousand accounts, and evolved a bit in other ways. Here's an updated analysis... -bsky.app/profile/cons...",at://did:plc:7n2gzbzn4xus4nghzfpjgli3/app.bsky.feed.post/3ldtghifzd22v,bafyreib2edsamrs4uya4xd67pkdxh3chdow2rwagwnn4z536rewfcgtwve,427,13,202,26,en,"record=Main(cid='bafyreifqqzqwhfldbulddfabdemiwi6buqf6kn4z3pcw3efhete2c7znuq', uri='at://did:plc:7n2gzbzn4xus4nghzfpjgli3/app.bsky.feed.post/3lcmg572f4c2q', py_type='com.atproto.repo.strongRef') py_type='app.bsky.embed.record'",0.002669913461431861 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-10T10:35:41.542Z,Another bot network using AI for at least some of the posts...,at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcx27jbqoc2x,bafyreif674cj5mwwc6xudgwo3aphcragdic52eic5bznbhejoktut66htu,11,3,5,0,en,"record=Main(cid='bafyreiahf3qxf2gjbx3drq6ypo22gdi2ywohjxiagypuqkupfbnuqdltkq', uri='at://did:plc:7n2gzbzn4xus4nghzfpjgli3/app.bsky.feed.post/3lcvysn6vek2h', py_type='com.atproto.repo.strongRef') py_type='app.bsky.embed.record'",0.06642594933509827 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-10T07:52:29.081Z,"Grundsätzlich schwer zu sagen. Zwei Möglichkeiten: - -1) Die Folgen tausenden (>100,000) Accounts und gewinnen dadurch ""back follower"". (komisch, aber nicht verboten) - -2) Bekannte Persönlichkeiten, die von X gekommen sind und daher ohne Beiträge viele follower haben.",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcwr3ohlhc2d,bafyreigb3kwnfwx2a72zzarm7shpkblpma4jivlvqmzkaw2jahvbrw33bi,0,0,0,0,de,,0.0009483483736403286 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-08T19:29:02.362Z,Do you have a handle of some? Would like to take a look.,at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcsx3eprhc2s,bafyreidaznh2akx262xtq2b4ioikb63gq5jw46kc2umyzlgluqtdk7rbpq,2,1,0,0,en,,0.6039703488349915 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-08T12:04:36.978Z,"Auf jeden Fall ein Fake-Account (siehe Antwort an OP)! - -Ich denke aber nicht, das der zum gleichen Netzwerk gehört - da diese Bilder von Instagram gestohlen wurden. Das Ziel kann aber natürlich das gleiche sein.",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcs6aomuek2i,bafyreietwh4g5a4fjicrapftwraqznz6ldrhy3mmbzo3i624savkn2ljsu,2,0,0,0,de,,0.0005878527881577611 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-08T11:48:57.041Z,"Eines der Fotos zeigt auf der Uniform den Namen ""Hufschmidt"". Schnelle Google Suche bestätigt deinen Verdacht: Diese ""Christine Wagner"" existiert nicht und hat die Bilder geklaut.",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcs5eoad3s2q,bafyreiff2wrz6f7nxoavv4jybxlviwnmoivsmuntzfbmgwzgtitykusici,3,1,0,0,de,"images=[Image(alt='Aileen Tina Hufschmidt auf LinkedIn.', image=BlobRef(mime_type='image/jpeg', size=42644, ref=IpldLink(link='bafkreiaoq6e4kn5mjeaccdfebh4qydfyeq5mubmtueuyjji6hegqq35mdi'), py_type='blob'), aspect_ratio=AspectRatio(height=217, width=195, py_type='app.bsky.embed.defs#aspectRatio'), py_type='app.bsky.embed.images#image')] py_type='app.bsky.embed.images'",0.05424289032816887 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-08T09:41:32.889Z,"4/ Some sources said it has been up to 50 accounts, but currently I cannot find those accounts on Bluesky (perhaps already blocked?). - -See for example this list: bsky.app/profile/lars... - -Thanks to everyone who helped/helps shining light on this!",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcrwau77y22h,bafyreigzxcnwk2tqgxikalwyouu7mbjns6jo6a3nnmlewexpzloitundzu,16,1,1,0,en,"record=Main(cid='bafyreic4hoy62rhw3t2y5tzmttonevtsymamywwmry4wwe5h2ozwa62k6y', uri='at://did:plc:buuaezktxboze6nudnwqlc7x/app.bsky.feed.post/3lcr4xowe5s22', py_type='com.atproto.repo.strongRef') py_type='app.bsky.embed.record'",0.0031484924256801605 -larswienand.bsky.social,Lars Wienand,did:plc:buuaezktxboze6nudnwqlc7x,2024-12-08T02:09:01.814Z,Es sind noch ein paar mehr schon/noch hinterlegt bei vanillasky. click,at://did:plc:buuaezktxboze6nudnwqlc7x/app.bsky.feed.post/3lcr4xowe5s22,bafyreic4hoy62rhw3t2y5tzmttonevtsymamywwmry4wwe5h2ozwa62k6y,108,3,17,3,de,"images=[Image(alt='1. Screenshot of Subdomains of vanilasky.click https://urlscan.io/ip/23.88.96.130', image=BlobRef(mime_type='image/jpeg', size=945584, ref=IpldLink(link='bafkreihyrxnwyy5zulrkpbj4p7detvsbjm2njoupzwklva3xitm64tmgfq'), py_type='blob'), aspect_ratio=AspectRatio(height=2000, width=924, py_type='app.bsky.embed.defs#aspectRatio'), py_type='app.bsky.embed.images#image'), Image(alt='2. Screenshot of Subdomains of vanilasky.click https://urlscan.io/ip/23.88.96.130', image=BlobRef(mime_type='image/jpeg', size=433690, ref=IpldLink(link='bafkreifypeehhifc3oempo6fhshoz5oxcn23b3f6s4crsrhjwqva7pyaiu'), py_type='blob'), aspect_ratio=AspectRatio(height=2000, width=1123, py_type='app.bsky.embed.defs#aspectRatio'), py_type='app.bsky.embed.images#image'), Image(alt='3. Screenshot of Subdomains of vanilasky.click https://urlscan.io/ip/23.88.96.130', image=BlobRef(mime_type='image/jpeg', size=967691, ref=IpldLink(link='bafkreidxkvbn2hommoysajecddvcna2ggfzw2p46fdnqrw6jxrvgf2ow7a'), py_type='blob'), aspect_ratio=AspectRatio(height=2000, width=989, py_type='app.bsky.embed.defs#aspectRatio'), py_type='app.bsky.embed.images#image'), Image(alt='4. Screenshot of Subdomains of vanilasky.click https://urlscan.io/ip/23.88.96.130, zusätzlicher Hinweis: „Attention: These domains and hostnames were discovered through Certificate Transparency (CT)\nLogs and have been irrevocably published as part of the public record. There is no mechanism for us or anyone else to remove this information from the internet“', image=BlobRef(mime_type='image/jpeg', size=394934, ref=IpldLink(link='bafkreidr34ie4tgjx6rq5tfddkdzxt5rf3gp5r2dio73umcra3bmmn6w4a'), py_type='blob'), aspect_ratio=AspectRatio(height=2000, width=997, py_type='app.bsky.embed.defs#aspectRatio'), py_type='app.bsky.embed.images#image')] py_type='app.bsky.embed.images'",0.0008308747783303261 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-08T09:33:41.048Z,"3/ The 9 remaining accounts stopped posting shortly after my initial thread and have not posted since. Until then, these 9 made 182 posts: bsky.app/profile/badl... - -This network was easy to find due to the common URL. However, it is likely not the only attempt to build a German speaking AI-network.",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcrvssswzs2h,bafyreian3sev3qun7opo7ktedozjckf7vv5jl3rcw6ouc5g2vwrxqy4lry,16,2,2,0,en,"record=Main(cid='bafyreif6azzshs5smbtrbzztfqexbzjwaqpvtqtgexh522zz6hhisgib4e', uri='at://did:plc:7syfakzcriq44mwbdbc7jwvn/app.bsky.feed.post/3lcqvua2erk2k', py_type='com.atproto.repo.strongRef') py_type='app.bsky.embed.record'",0.002489847131073475 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-08T09:33:41.047Z,"2/ This is less than it seemed, based on how the replies to these news articles were dominated by the network (now often called vanillasky network). - -However, the network was likely still being developed. -The oldest account first posted 4 days ago, while approx. half started posting 2 days ago.",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcrvsssw2k2h,bafyreiaboamt2l46icqjgbn6eewzv7scuei2zc7v7pl7xj4w3yblkj37fe,10,1,1,0,en,,0.0021178426686674356 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-08T09:33:41.046Z,"Yesterdays thread on the discovery of an AI bot network gained quite some traction. Since then, ppl. found out more about the network. - -1/ @badlogic.bsky.social and other's found that the bots are hosted through the same domain, which now has a total of only 19 bots. 10 of which are blocked by Bsky.",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcrvss7qrk2h,bafyreiddubek5uu42uwyq5zyl4c2j3ia4oiusg7le2c3wkkuvf4tdxb6cq,48,5,26,3,en,"record=Main(cid='bafyreihvqfupoungdzwnmmohobdgkjfabanyscbumfislm2a4yraujrb7y', uri='at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcpc4ztcmk2n', py_type='com.atproto.repo.strongRef') py_type='app.bsky.embed.record'",0.001831565983593464 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-08T08:45:32.311Z,"Hi Robert, ich hatte selber auch nicht mit so viel Aufmerksamkeit für den post gerechnet und kann deine Verunsicherung daher verstehen. - -Falls noch zweifel bestehen, gerne über meine Arbeits/Uni-E-Mail Kontakt suchen (kann via Google gefunden werden).",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcrt4pckis2h,bafyreievulfuirze6ckdb3devxz3deqzzdsqn5s62ax33b7x7ddh4cd4vu,1,1,0,0,de,,0.0009039518190547824 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-08T08:27:41.727Z,"The accounts were less just a few hours/days old. Roughly half have been deleted by now - so it seems BlueSky is doing a good job! - -They might still rely on us for reporting suspicious activities though.",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcrs4scvdk2v,bafyreic7kkgcwuznbzcg54ybjeqnlpmzits43kipxw4j5aosxitzc4q2oe,1,0,0,0,en,,0.0322367325425148 -badlogic.bsky.social,Mario Zechner,did:plc:7syfakzcriq44mwbdbc7jwvn,2024-12-08T00:01:49.366Z,"And just to quantify the OPs claim that Bluesky is being flooded with German speaking bots. - -This tiny network hat 19 bots, which posted a total of 182 posts (at least the 10 bots that are still active). - -That's not quite a flood. Pretty sure as the elections come closer, we'll se more activity tho.",at://did:plc:7syfakzcriq44mwbdbc7jwvn/app.bsky.feed.post/3lcqvua2erk2k,bafyreif6azzshs5smbtrbzztfqexbzjwaqpvtqtgexh522zz6hhisgib4e,34,4,6,2,en,,0.06593639403581619 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-07T23:27:01.593Z,"Die bots aus dem thread haben mit niemandem interagiert. - -Rein technisch dürfte das aber Möglich sein. Fliegt aber ggf. schneller auf, wenn die Antworten weniger Sinn machen. - -Das Hauptproblem ist, das die mit der Zeit besser werden könnten.",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcqtvyymwk2z,bafyreibcdpjhtaoascodneiudlg475h55sfv4zdanfxw5ddgxoao7lpot4,2,1,0,0,de,,0.0004682602302636951 -badlogic.bsky.social,Mario Zechner,did:plc:7syfakzcriq44mwbdbc7jwvn,2024-12-07T22:58:16.377Z,"In other bot news, these bots are on their on ""Personal Data Server"", which is still running. - -There are a total of 19 accounts on that server. Only 9 of them have been suspended. Curious why @support.bsky.team hasn't defederated with that PDS entirely?",at://did:plc:7syfakzcriq44mwbdbc7jwvn/app.bsky.feed.post/3lcqsclpgjc2k,bafyreids4kowumiuw3huczo6l4ld2yob6fuqxw4ap3rkxgh63ahzz3g4ne,34,3,14,4,en,"media=Main(images=[Image(alt='Fetched repo page (0 repos so far)\nFetched repo page (19 repos so far)\nFound 19 total accounts\nProcessing account 1/19: did:plc:mebbh6ivwisak2wmyhdupner\nHandle: marcoweber91.vanillasky.click\nName: Marco Weber\nDescription: ðŸ•ï¸ Outdoor-Fan | 🎮 Gamer | 💻 Technik-Nerd | Immer bereit für neue Herausforderungen!\nPosts: 26\nProcessing account 2/19: did:plc:mgvycnjdf7ondai6ubsrcesy\nFailed to fetch profile for did:plc:mgvycnjdf7ondai6ubsrcesy: Error: Account has been suspended\nError fetching posts for did:plc:mgvycnjdf7ondai6ubsrcesy: Error: Profile not found\nProcessing account 3/19: did:plc:rp2pzlwtsxzjddbuqy2bbjd5\nFailed to fetch profile for did:plc:rp2pzlwtsxzjddbuqy2bbjd5: Error: Account has been suspended\nError fetching posts for did:plc:rp2pzlwtsxzjddbuqy2bbjd5: Error: Profile not found\nProcessing account 4/19: did:plc:ahfgjz3ymeypkswqd42pulsl\nHandle: julianschneider87.vanillasky.click\nName: Julian Schneider\nDescription: 🌌 Sternenbeobachter | 🚀 Weltraumfan | 📷 Fotograf aus Leidenschaft | Auf der Suche nach neuen Horizonten!\nPosts: 41\nProcessing account 5/19: did:plc:ktzihqnjlc7hw2krl4oygcd3\nFailed to fetch profile for did:plc:ktzihqnjlc7hw2krl4oygcd3: Error: Account has been suspended\nError fetching posts for did:plc:ktzihqnjlc7hw2krl4oygcd3: Error: Profile not found\nProcessing account 6/19: did:plc:2fswymc5t3nl3roo5drhstfa\nFailed to fetch profile for did:plc:2fswymc5t3nl3roo5drhstfa: Error: Account has been suspended\nError fetching posts for did:plc:2fswymc5t3nl3roo5drhstfa: Error: Profile not found\nProcessing account 7/19: did:plc:qx2ktdu6ynfsrblggjf56v6n\nFailed to fetch profile for did:plc:qx2ktdu6ynfsrblggjf56v6n: Error: Account has been suspended\nError fetching posts for did:plc:qx2ktdu6ynfsrblggjf56v6n: Error: Profile not found\n', image=BlobRef(mime_type='image/jpeg', size=677432, ref=IpldLink(link='bafkreiaddfmvk6c6t52c6dw36vvqc5nano6fu5jvdkhfc3jhgsebl3agtq'), py_type='blob'), aspect_ratio=AspectRatio(height=1842, width=1846, py_type='app.bsky.embed.defs#aspectRatio'), py_type='app.bsky.embed.images#image'), Image(alt='Processing account 12/19: did:plc:qq73sngfhgrmy3umvpd34o4f\nHandle: janinabecker89.vanillasky.click\nName: Janina Becker\nDescription: ðŸžï¸ Naturfanatikerin | 📸 Fotografie-Enthusiastin | 🷠Weinkennerin | Auf der Suche nach dem perfekten Moment!\nPosts: 10\nProcessing account 13/19: did:plc:b4by5mbdqwfiekw4aeub34vf\nFailed to fetch profile for did:plc:b4by5mbdqwfiekw4aeub34vf: Error: Account has been suspended\nError fetching posts for did:plc:b4by5mbdqwfiekw4aeub34vf: Error: Profile not found\nProcessing account 14/19: did:plc:a7nv6d5rshdclidvpfy5jtem\nHandle: felixneumann89.vanillasky.click\nName: Felix Neumann\nDescription: ðŸžï¸ Naturentdecker | 🎥 Filmfan | 🳠Kochliebhaber | Immer auf der Jagd nach neuen Geschmackserlebnissen!\nPosts: 8\nProcessing account 15/19: did:plc:i3lnumxsw7owut3cuczj5kw4\nHandle: leonardfischer86.vanillasky.click\nName: Leonard Fischer\nDescription: 🌌 Sternengucker | 🎥 Filmliebhaber | 🎻 Musikenthusiast | Auf der Suche nach Inspiration in jedem Moment!\nPosts: 12\nProcessing account 16/19: did:plc:ncugmc25d3ttozdgad5wubrx\nHandle: felixkonig86.vanillasky.click\nName: Felix König\nDescription: 🎥 Filmfanatiker | 🌠Weltenbummler | 🕠Pizza-Liebhaber | Immer auf der Suche nach der nächsten großen Story!\nPosts: 6\nProcessing account 17/19: did:plc:wzylfjeicx4gwc5ue5ysntef\nHandle: felixwagner87.vanillasky.click\nName: Felix Wagner\nDescription: 🎨 Kreativer Kopf | 📸 Fotografie-Enthusiast | 🌠Weltenbummler | Lebe jeden Tag als wäre es der letzte!\nPosts: 9\nProcessing account 18/19: did:plc:3ptrtsc6gzueasouc6m2y2w7\nFailed to fetch profile for did:plc:3ptrtsc6gzueasouc6m2y2w7: Error: Account has been suspended\nError fetching posts for did:plc:3ptrtsc6gzueasouc6m2y2w7: Error: Profile not found\nProcessing account 19/19: did:plc:nxiafx6e54lk3mu6smcwevvv\nFailed to fetch profile for did:plc:nxiafx6e54lk3mu6smcwevvv: Error: Account has been suspended\nError fetching posts for did:plc:nxiafx6e54lk3mu6smcwevvv: Error: Profile not found', image=BlobRef(mime_type='image/jpeg', size=512761, ref=IpldLink(link='bafkreig5hc43tue4rcdjjbylbiyhf3lhn67pyodx45rsryfgsvd3ccx454'), py_type='blob'), aspect_ratio=AspectRatio(height=458, width=1776, py_type='app.bsky.embed.defs#aspectRatio'), py_type='app.bsky.embed.images#image')], py_type='app.bsky.embed.images') record=Main(record=Main(cid='bafyreihvqfupoungdzwnmmohobdgkjfabanyscbumfislm2a4yraujrb7y', uri='at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcpc4ztcmk2n', py_type='com.atproto.repo.strongRef'), py_type='app.bsky.embed.record') py_type='app.bsky.embed.recordWithMedia'",0.009797980077564716 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-07T19:26:38.798Z,"Die sind alle über eine eigene Bluesky-Instanz registriert. Bluesky ist dezentral aufgebaut, so das man seinen Provider selbst auswählen kann. In diesem Fall wird das wohl genutzt um z.b. E-Mail-Verifizierungen zu umgehen. Oder andere Sicherheitsmaßnahmen.",at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcqgi6du6s26,bafyreieqb7cngmfgxpbgr45d5uizgk6ljvpj7zxggpepbjthcuhlwnfr2u,6,1,0,0,en,,0.0004913366865366697 -lion-c.bsky.social,Lion Cassens,did:plc:2fitdmiaotn22kbwgox4v7hc,2024-12-07T18:46:37.304Z,The domain also seems fairly new. Registered on 26th November according to www.whatsmydns.net/domain-age?q....,at://did:plc:2fitdmiaotn22kbwgox4v7hc/app.bsky.feed.post/3lcqeam42a22e,bafyreiakxiavezgsgmjdpmwzdh5enzc6xixec7tzf4y3hlaudar7s475oa,3,0,0,0,en,,0.013075300492346287 -- GitLab