A social pastebin built on atproto.
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

identity: use bluesky cdn for avatars if available

+27 -15
+26 -14
identity.py
··· 85 85 except requests.RequestException, ValueError: 86 86 return {} 87 87 88 + avatar_url = None 88 89 avatar_blob_url = None 89 90 avatar = value.get("avatar") 90 91 if avatar and isinstance(avatar, dict): 91 92 cid = avatar.get("ref", {}).get("$link") 92 93 if cid: 94 + avatar_url = f"https://cdn.bsky.app/img/avatar/plain/{did}/{cid}@webp" 93 95 avatar_blob_url = ( 94 96 f"{pds_url}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}" 95 97 ) ··· 98 100 "display_name": value.get("displayName"), 99 101 "description": value.get("description"), 100 102 "pronouns": value.get("pronouns"), 101 - "avatar_url": f"/avatar/{did}" if avatar_blob_url else None, 103 + "avatar_url": avatar_url, 102 104 "avatar_blob_url": avatar_blob_url, 103 105 } 104 106 _profile_cache[did] = (profile, now) ··· 130 132 dids = [item.get("did", "") for item in raw] 131 133 unique_dids = list(set(d for d in dids if d)) 132 134 with ThreadPoolExecutor(max_workers=5) as pool: 133 - results = dict(zip(unique_dids, pool.map(resolve_identity, unique_dids))) 135 + identity_results = dict(zip(unique_dids, pool.map(resolve_identity, unique_dids))) 136 + 137 + # Fetch profiles in parallel too (for avatar URLs) 138 + def _fetch_profile_for_did(did: str) -> dict[str, str | None]: 139 + handle, pds_url = identity_results.get(did, (None, None)) 140 + if pds_url: 141 + return fetch_profile(did, pds_url) 142 + return {} 143 + 144 + with ThreadPoolExecutor(max_workers=5) as pool: 145 + profile_results = dict(zip(unique_dids, pool.map(_fetch_profile_for_did, unique_dids))) 134 146 135 147 bites = [] 136 148 for item in raw: 137 149 record = item.get("record", {}) 138 150 did = item.get("did", "") 139 - handle, _ = results.get(did, (None, None)) 151 + handle, _ = identity_results.get(did, (None, None)) 152 + profile = profile_results.get(did, {}) 140 153 try: 141 - bites.append( 142 - { 143 - "did": did, 144 - "handle": handle, 145 - "rkey": item.get("rkey", ""), 146 - "title": record["title"], 147 - "content": record["content"], 148 - "created_at": record.get("createdAt", ""), 149 - } 150 - ) 151 - except KeyError, TypeError: 154 + bites.append({ 155 + "did": did, 156 + "handle": handle, 157 + "rkey": item.get("rkey", ""), 158 + "title": record["title"], 159 + "content": record["content"], 160 + "created_at": record.get("createdAt", ""), 161 + "avatar_url": profile.get("avatar_url") or f"/avatar/{did}", 162 + }) 163 + except (KeyError, TypeError): 152 164 continue 153 165 154 166 _recent_bites_cache = (bites, now)
+1 -1
templates/_recent_bites.html
··· 7 7 <div class="flex items-center justify-between mb-1"> 8 8 <span class="flex items-center gap-1.5 hover:underline cursor-pointer js-profile-link" 9 9 data-href="{{ url_for('list_bites', identifier=bite.did) }}"> 10 - <img src="/avatar/{{ bite.did }}" alt="" class="w-4 h-4 rounded-full bg-gray-100"> 10 + <img src="{{ bite.avatar_url }}" onerror="this.onerror=null;this.src='/avatar/{{ bite.did }}'" alt="" class="w-4 h-4 rounded-full bg-gray-100"> 11 11 <span class="text-xs text-amber-600">{{ bite.handle or bite.did }}</span> 12 12 </span> 13 13 <span class="text-xs text-gray-400">{{ bite.created_at | humandate }}</span>