scrapeMM 0.1.2__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. scrapemm-0.2.1/.gitignore +6 -0
  2. {scrapemm-0.1.2/scrapeMM.egg-info → scrapemm-0.2.1}/PKG-INFO +17 -6
  3. {scrapemm-0.1.2 → scrapemm-0.2.1}/README.md +9 -3
  4. scrapemm-0.2.1/pyproject.toml +36 -0
  5. scrapemm-0.2.1/requirements.txt +18 -0
  6. {scrapemm-0.1.2 → scrapemm-0.2.1/scrapeMM.egg-info}/PKG-INFO +17 -6
  7. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapeMM.egg-info/SOURCES.txt +10 -2
  8. scrapemm-0.2.1/scrapeMM.egg-info/requires.txt +10 -0
  9. scrapemm-0.2.1/scrapemm/__init__.py +4 -0
  10. scrapemm-0.2.1/scrapemm/common.py +47 -0
  11. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapemm/integrations/__init__.py +8 -1
  12. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapemm/integrations/base.py +3 -1
  13. scrapemm-0.2.1/scrapemm/integrations/bluesky.py +237 -0
  14. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapemm/integrations/telegram.py +5 -5
  15. scrapemm-0.2.1/scrapemm/integrations/tiktok.py +469 -0
  16. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapemm/integrations/x.py +16 -12
  17. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapemm/retrieval.py +10 -7
  18. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapemm/scraping/firecrawl.py +36 -10
  19. scrapemm-0.2.1/scrapemm/scraping/no_bot_domains.txt +15 -0
  20. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapemm/scraping/util.py +8 -10
  21. scrapemm-0.2.1/scrapemm/secrets.py +149 -0
  22. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapemm/util.py +7 -3
  23. scrapemm-0.2.1/scripts/example.py +8 -0
  24. scrapemm-0.2.1/testing/test_retrieval.py +58 -0
  25. scrapemm-0.2.1/testing/test_utils.py +19 -0
  26. scrapemm-0.1.2/pyproject.toml +0 -28
  27. scrapemm-0.1.2/scrapeMM.egg-info/requires.txt +0 -5
  28. scrapemm-0.1.2/scrapemm/__init__.py +0 -4
  29. scrapemm-0.1.2/scrapemm/api_keys.py +0 -43
  30. scrapemm-0.1.2/scrapemm/common.py +0 -42
  31. {scrapemm-0.1.2 → scrapemm-0.2.1}/LICENSE +0 -0
  32. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapeMM.egg-info/dependency_links.txt +0 -0
  33. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapeMM.egg-info/top_level.txt +0 -0
  34. {scrapemm-0.1.2 → scrapemm-0.2.1}/scrapemm/scraping/__init__.py +0 -0
  35. {scrapemm-0.1.2 → scrapemm-0.2.1}/setup.cfg +0 -0
@@ -0,0 +1,6 @@
1
+ /temp/
2
+ /.idea/
3
+ /scrapeMM.egg-info/
4
+ /dist/
5
+ /config.yaml
6
+ __pycache__/
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.1.2
3
+ Version: 0.2.1
4
4
  Summary: LLM-friendly scraper for media and text from social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/multimodal-ai-lab/scrapeMM
8
- Project-URL: Bug Tracker, https://github.com/multimodal-ai-lab/scrapeMM
8
+ Project-URL: Issues, https://github.com/multimodal-ai-lab/scrapeMM
9
9
  Requires-Python: >=3.8
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
@@ -13,7 +13,12 @@ Requires-Dist: ezmm
13
13
  Requires-Dist: telethon
14
14
  Requires-Dist: tweepy
15
15
  Requires-Dist: markdownify
16
- Requires-Dist: keyring
16
+ Requires-Dist: platformdirs
17
+ Requires-Dist: PyYAML
18
+ Requires-Dist: atproto
19
+ Requires-Dist: TikTokResearchApi
20
+ Requires-Dist: yt-dlp
21
+ Requires-Dist: cryptography
17
22
  Dynamic: license-file
18
23
 
19
24
  # scrapeMM: Multimodal Web Retrieval
@@ -24,11 +29,15 @@ This library aims to help developers and researchers to easily access multimodal
24
29
  ## Usage
25
30
  ```python
26
31
  from scrapemm import retrieve
32
+ import asyncio
27
33
 
28
34
  url = "https://example.com"
29
- result = retrieve(url)
35
+ loop = asyncio.get_event_loop()
36
+ result = loop.run_until_complete(retrieve(url))
30
37
  result.render()
31
38
  ```
39
+ `scrapeMM` will ask you for the **API keys** needed for the social media integrations. You may skip them if you don't need them.
40
+ You will also be prompted to choose a **password** that is used to secure the secrets in an encrypted file.
32
41
 
33
42
  ## How it works
34
43
  ```
@@ -42,7 +51,9 @@ Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl).
42
51
  ## Supported Proprietary APIs
43
52
  - ✅ X/Twitter
44
53
  - ✅ Telegram
54
+ - ✅ Bluesky
55
+ - ✅ TikTok
56
+ - ⏳ Threads
57
+ - ⏳ Reddit
45
58
  - ⏳ Facebook
46
59
  - ⏳ Instagram
47
- - ⏳ Threads
48
- - ⏳ TikTok
@@ -6,11 +6,15 @@ This library aims to help developers and researchers to easily access multimodal
6
6
  ## Usage
7
7
  ```python
8
8
  from scrapemm import retrieve
9
+ import asyncio
9
10
 
10
11
  url = "https://example.com"
11
- result = retrieve(url)
12
+ loop = asyncio.get_event_loop()
13
+ result = loop.run_until_complete(retrieve(url))
12
14
  result.render()
13
15
  ```
16
+ `scrapeMM` will ask you for the **API keys** needed for the social media integrations. You may skip them if you don't need them.
17
+ You will also be prompted to choose a **password** that is used to secure the secrets in an encrypted file.
14
18
 
15
19
  ## How it works
16
20
  ```
@@ -24,7 +28,9 @@ Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl).
24
28
  ## Supported Proprietary APIs
25
29
  - ✅ X/Twitter
26
30
  - ✅ Telegram
31
+ - ✅ Bluesky
32
+ - ✅ TikTok
33
+ - ⏳ Threads
34
+ - ⏳ Reddit
27
35
  - ⏳ Facebook
28
36
  - ⏳ Instagram
29
- - ⏳ Threads
30
- - ⏳ TikTok
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "scrapeMM"
3
+ version = "0.2.1"
4
+ description = "LLM-friendly scraper for media and text from social media and the open web."
5
+ readme = "README.md"
6
+ requires-python = ">=3.8"
7
+ license = "Apache-2.0"
8
+ authors = [{name = "Mark Rothermel", email = "mark.rothermel@tu-darmstadt.de"}]
9
+ dependencies = [
10
+ "ezmm",
11
+ "telethon",
12
+ "tweepy",
13
+ "markdownify",
14
+ "platformdirs",
15
+ "PyYAML",
16
+ "atproto",
17
+ "TikTokResearchApi",
18
+ "yt-dlp",
19
+ "cryptography",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/multimodal-ai-lab/scrapeMM"
24
+ Issues = "https://github.com/multimodal-ai-lab/scrapeMM"
25
+
26
+ [build-system]
27
+ requires = ["setuptools>=60", "setuptools-scm>=8.0"]
28
+ build-backend = "setuptools.build_meta"
29
+
30
+ [tool.setuptools.packages.find]
31
+ namespaces = false # Disable implicit namespaces
32
+ # include = ["scrapemm/*"]
33
+ # exclude = ["*.egg-info", "tests*", "in*", "scripts*", "temp*", "secrets.yaml", "dist*"]
34
+
35
+ [tool.setuptools.package-data]
36
+ "*" = ["*.txt"] # Include all .txt files contained in any (sub)package in the build
@@ -0,0 +1,18 @@
1
+ pytest~=8.4.1
2
+ aiohttp~=3.12.13
3
+ numpy~=1.26.4
4
+ requests~=2.32.4
5
+ ezMM~=0.3.1
6
+ Telethon~=1.40.0
7
+ PyYAML~=6.0.1
8
+ tweepy~=4.16.0
9
+ markdownify~=1.1.0
10
+ tqdm~=4.66.3
11
+ pytest-asyncio
12
+ async_lru
13
+ oauthlib
14
+ platformdirs~=4.3.8
15
+ atproto
16
+ TikTokResearchApi
17
+ yt-dlp
18
+ cryptography
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.1.2
3
+ Version: 0.2.1
4
4
  Summary: LLM-friendly scraper for media and text from social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/multimodal-ai-lab/scrapeMM
8
- Project-URL: Bug Tracker, https://github.com/multimodal-ai-lab/scrapeMM
8
+ Project-URL: Issues, https://github.com/multimodal-ai-lab/scrapeMM
9
9
  Requires-Python: >=3.8
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
@@ -13,7 +13,12 @@ Requires-Dist: ezmm
13
13
  Requires-Dist: telethon
14
14
  Requires-Dist: tweepy
15
15
  Requires-Dist: markdownify
16
- Requires-Dist: keyring
16
+ Requires-Dist: platformdirs
17
+ Requires-Dist: PyYAML
18
+ Requires-Dist: atproto
19
+ Requires-Dist: TikTokResearchApi
20
+ Requires-Dist: yt-dlp
21
+ Requires-Dist: cryptography
17
22
  Dynamic: license-file
18
23
 
19
24
  # scrapeMM: Multimodal Web Retrieval
@@ -24,11 +29,15 @@ This library aims to help developers and researchers to easily access multimodal
24
29
  ## Usage
25
30
  ```python
26
31
  from scrapemm import retrieve
32
+ import asyncio
27
33
 
28
34
  url = "https://example.com"
29
- result = retrieve(url)
35
+ loop = asyncio.get_event_loop()
36
+ result = loop.run_until_complete(retrieve(url))
30
37
  result.render()
31
38
  ```
39
+ `scrapeMM` will ask you for the **API keys** needed for the social media integrations. You may skip them if you don't need them.
40
+ You will also be prompted to choose a **password** that is used to secure the secrets in an encrypted file.
32
41
 
33
42
  ## How it works
34
43
  ```
@@ -42,7 +51,9 @@ Web scraping is done with [Firecrawl](https://github.com/mendableai/firecrawl).
42
51
  ## Supported Proprietary APIs
43
52
  - ✅ X/Twitter
44
53
  - ✅ Telegram
54
+ - ✅ Bluesky
55
+ - ✅ TikTok
56
+ - ⏳ Threads
57
+ - ⏳ Reddit
45
58
  - ⏳ Facebook
46
59
  - ⏳ Instagram
47
- - ⏳ Threads
48
- - ⏳ TikTok
@@ -1,20 +1,28 @@
1
+ .gitignore
1
2
  LICENSE
2
3
  README.md
3
4
  pyproject.toml
5
+ requirements.txt
4
6
  scrapeMM.egg-info/PKG-INFO
5
7
  scrapeMM.egg-info/SOURCES.txt
6
8
  scrapeMM.egg-info/dependency_links.txt
7
9
  scrapeMM.egg-info/requires.txt
8
10
  scrapeMM.egg-info/top_level.txt
9
11
  scrapemm/__init__.py
10
- scrapemm/api_keys.py
11
12
  scrapemm/common.py
12
13
  scrapemm/retrieval.py
14
+ scrapemm/secrets.py
13
15
  scrapemm/util.py
14
16
  scrapemm/integrations/__init__.py
15
17
  scrapemm/integrations/base.py
18
+ scrapemm/integrations/bluesky.py
16
19
  scrapemm/integrations/telegram.py
20
+ scrapemm/integrations/tiktok.py
17
21
  scrapemm/integrations/x.py
18
22
  scrapemm/scraping/__init__.py
19
23
  scrapemm/scraping/firecrawl.py
20
- scrapemm/scraping/util.py
24
+ scrapemm/scraping/no_bot_domains.txt
25
+ scrapemm/scraping/util.py
26
+ scripts/example.py
27
+ testing/test_retrieval.py
28
+ testing/test_utils.py
@@ -0,0 +1,10 @@
1
+ ezmm
2
+ telethon
3
+ tweepy
4
+ markdownify
5
+ platformdirs
6
+ PyYAML
7
+ atproto
8
+ TikTokResearchApi
9
+ yt-dlp
10
+ cryptography
@@ -0,0 +1,4 @@
1
+ import scrapemm.common
2
+ from .secrets import configure_secrets
3
+ from .retrieval import retrieve
4
+ from .integrations import Telegram, X
@@ -0,0 +1,47 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+ from platformdirs import user_config_dir
6
+
7
+ import yaml
8
+
9
+ APP_NAME = "scrapeMM"
10
+
11
+ # Set up logger
12
+ logger = logging.getLogger(APP_NAME)
13
+ logger.setLevel(logging.DEBUG)
14
+
15
+ # Only add handler if none exists (avoid duplicate logs on rerun)
16
+ if not logger.hasHandlers():
17
+ handler = logging.StreamHandler(sys.stdout)
18
+ formatter = logging.Formatter('[%(levelname)s]: %(message)s')
19
+ handler.setFormatter(formatter)
20
+ logger.addHandler(handler)
21
+ logger.propagate = False
22
+
23
+ # Set up config directory
24
+ CONFIG_DIR = Path(user_config_dir(APP_NAME))
25
+ os.makedirs(CONFIG_DIR, exist_ok=True)
26
+ CONFIG_PATH = CONFIG_DIR / "config.yaml"
27
+
28
+
29
+ def load_config() -> dict:
30
+ if os.path.exists(CONFIG_PATH):
31
+ with open(CONFIG_PATH, "r") as f:
32
+ return yaml.safe_load(f) or {}
33
+ else:
34
+ return {}
35
+
36
+
37
+ def update_config(**kwargs):
38
+ _config.update(kwargs)
39
+ yaml.dump(_config, open(CONFIG_PATH, "w"))
40
+
41
+
42
+ def get_config_var(name: str, default=None) -> str:
43
+ return _config.get(name, default)
44
+
45
+
46
+ # Load config
47
+ _config = load_config()
@@ -1,13 +1,20 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger("scrapeMM")
4
+ logger.debug("Starting integrations...")
5
+
1
6
  from typing import Optional
2
7
 
3
8
  import aiohttp
4
9
  from ezmm import MultimodalSequence
5
10
 
6
11
  from scrapemm.util import get_domain
12
+ from .bluesky import Bluesky
7
13
  from .telegram import Telegram
14
+ from .tiktok import TikTok
8
15
  from .x import X
9
16
 
10
- RETRIEVAL_INTEGRATIONS = [X(), Telegram()]
17
+ RETRIEVAL_INTEGRATIONS = [X(), Telegram(), Bluesky(), TikTok()]
11
18
  DOMAIN_TO_INTEGRATION = {domain: integration
12
19
  for integration in RETRIEVAL_INTEGRATIONS
13
20
  for domain in integration.domains
@@ -1,3 +1,5 @@
1
+ from typing import Optional
2
+
1
3
  import aiohttp
2
4
  from ezmm import MultimodalSequence
3
5
 
@@ -9,6 +11,6 @@ class RetrievalIntegration:
9
11
  domains: list[str] # The domains supported by this integration
10
12
  connected: bool = False
11
13
 
12
- async def get(self, url: str, session: aiohttp.ClientSession) -> MultimodalSequence:
14
+ async def get(self, url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
13
15
  """Retrieves the contents present at the given URL."""
14
16
  raise NotImplementedError
@@ -0,0 +1,237 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import aiohttp
5
+ from atproto_client.exceptions import RequestErrorBase
6
+ from atproto_client.models.common import XrpcError
7
+ from ezmm import MultimodalSequence, download_image, download_video
8
+
9
+ from scrapemm.secrets import get_secret
10
+ from scrapemm.integrations.base import RetrievalIntegration
11
+ from scrapemm.util import get_domain
12
+
13
+ logger = logging.getLogger("scrapeMM")
14
+
15
+
16
+ class Bluesky(RetrievalIntegration):
17
+ domains = ["bsky.app"]
18
+
19
+ def __init__(self):
20
+ self.username = get_secret("bluesky_username")
21
+ self.password = get_secret("bluesky_password")
22
+
23
+ if not (self.username and self.password):
24
+ logger.warning("❌ Bluesky integration not configured: Missing username or password.")
25
+ self.connected = False
26
+ return
27
+
28
+ from atproto import Client
29
+ self.client = Client()
30
+ self.authenticated = False
31
+ self._authenticate()
32
+
33
+ async def get(self, url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
34
+ if not self.authenticated:
35
+ logger.error("❌ Not authenticated with Bluesky. Cannot retrieve content.")
36
+ return None
37
+
38
+ if get_domain(url) not in self.domains:
39
+ logger.error(f"❌ Invalid domain for Bluesky: {get_domain(url)}")
40
+ return None
41
+
42
+ if "post" in url:
43
+ result = await self._retrieve_post(url, session)
44
+ else:
45
+ result = await self._retrieve_profile(url, session)
46
+
47
+ return result
48
+
49
+ async def _retrieve_post(self, url: str, session: aiohttp.ClientSession = None) -> Optional[MultimodalSequence]:
50
+ """Retrieve a post from the given Bluesky URL."""
51
+ uri = self._construct_uri(url)
52
+ if not uri:
53
+ logger.error(f"❌ Could not construct URI for Bluesky post: {url}")
54
+ return None
55
+
56
+ try:
57
+ thread_response = self.client.get_post_thread(uri=uri, depth=0, parent_height=0)
58
+ thread = thread_response.thread
59
+
60
+ if hasattr(thread, 'py_type'):
61
+ thread_type = getattr(thread, 'py_type')
62
+ if thread_type == 'app.bsky.feed.defs#notFoundPost':
63
+ raise Exception(f"Post not found for url {url}")
64
+ if thread_type == 'app.bsky.feed.defs#blockedPost':
65
+ raise Exception(f"Post is blocked for url {url}")
66
+
67
+ # Extract post data
68
+ post_view = thread.post
69
+ record = post_view.record
70
+
71
+ # Basic post information
72
+ post_text = record.text if hasattr(record, 'text') else ''
73
+ created_at_str = record.created_at[:-1] if hasattr(record, 'created_at') else None
74
+
75
+ # Author information
76
+ author = post_view.author
77
+ author_username = author.handle if hasattr(author, 'handle') else ''
78
+ author_display_name = author.display_name if hasattr(author, 'display_name') else ''
79
+
80
+ # Engagement metrics
81
+ like_count = post_view.like_count if hasattr(post_view, 'like_count') else 0
82
+ comment_count = post_view.reply_count if hasattr(post_view, 'reply_count') else 0
83
+ share_count = post_view.repost_count if hasattr(post_view, 'repost_count') else 0
84
+
85
+ # Extract media (images)
86
+ media = []
87
+ # Check for embedded images in the post
88
+ if hasattr(post_view, 'embed'):
89
+ embed = post_view.embed
90
+
91
+ # For image embeds
92
+ if hasattr(embed, 'py_type') and getattr(embed, 'py_type') == 'app.bsky.embed.images#view':
93
+ for img in embed.images:
94
+ if hasattr(img, 'fullsize'):
95
+ img_url = img.fullsize
96
+ img = await download_image(img_url, session)
97
+ media.append(img)
98
+ # For video embeds
99
+ elif hasattr(embed, 'py_type') and getattr(embed, 'py_type') == 'app.bsky.embed.video#view':
100
+ video = await download_video(embed.playlist, session)
101
+ if video:
102
+ media.append(video)
103
+
104
+ # Extract hashtags and mentions
105
+ hashtags, mentions, external_links = [], [], []
106
+ # Parse facets (rich text features like links, mentions, etc.)
107
+ if hasattr(record, 'facets') and record.facets:
108
+ for facet in record.facets:
109
+ if hasattr(facet, 'features'):
110
+ for feature in facet.features:
111
+ if hasattr(feature, 'py_type'):
112
+ feature_type = getattr(feature, 'py_type')
113
+ if feature_type == 'app.bsky.richtext.facet#tag':
114
+ hashtags.append(feature.tag if hasattr(feature, 'tag') else '')
115
+ elif feature_type == 'app.bsky.richtext.facet#mention':
116
+ mentions.append(feature.did if hasattr(feature, 'did') else '')
117
+ elif feature_type == 'app.bsky.richtext.facet#link':
118
+ external_links.append(feature.uri)
119
+
120
+ # Check if this is a reply
121
+ is_reply, reply_to = False, None
122
+ if hasattr(record, 'reply'):
123
+ is_reply = True
124
+ # Get the parent post's author
125
+ if hasattr(record.reply, 'parent') and hasattr(record.reply.parent, 'uri'):
126
+ parent_uri = record.reply.parent.uri
127
+ post_id = parent_uri.split('/')[-1]
128
+ reply_to_post = self.client.get_posts([parent_uri]).posts[0]
129
+ reply_to_author = reply_to_post.author
130
+ reply_to = f"https://bsky.app/profile/{reply_to_author.handle}/post/{post_id}"
131
+
132
+ text = f"""**Post on Bluesky**
133
+ Author: {author_display_name} (@{author_username})
134
+ Posted on: {created_at_str}
135
+ Likes: {like_count} - Comments: {comment_count} - Shares: {share_count}
136
+ {"Reply to: " + reply_to if is_reply and reply_to else ""}
137
+ {post_text}"""
138
+ return MultimodalSequence([text, *media])
139
+
140
+ except Exception as e:
141
+ err_msg = error_to_string(e)
142
+ logger.error(f"❌ Error retrieving Bluesky post: {err_msg}")
143
+ return None
144
+
145
+ async def _retrieve_profile(self, url: str, session: aiohttp.ClientSession) -> Optional[MultimodalSequence]:
146
+ """Retrieve a profile from the given Bluesky URL."""
147
+ profile = self.client.get_profile(url.split('/')[-1])
148
+
149
+ avatar = await download_image(profile.avatar, session) if profile.avatar else None
150
+ banner = await download_image(profile.banner, session) if profile.banner else None
151
+
152
+ text = f"""**Profile on Bluesky**
153
+ User: {profile.display_name} (@{profile.handle})
154
+ Created on: {profile.created_at}
155
+ Profile image: {avatar.reference if avatar else 'None'}
156
+ Profile banner: {banner.reference if banner else 'None'}
157
+
158
+ URL: {url}
159
+ Description: {profile.description or 'No description provided'}
160
+
161
+ Metrics:
162
+ - Follower count: {profile.followers_count}
163
+ - Following count: {profile.follows_count}
164
+ - Post count: {profile.posts_count}
165
+ """
166
+ return MultimodalSequence(text)
167
+
168
+ def _authenticate(self) -> bool:
169
+ """Authenticate with Bluesky using provided credentials."""
170
+ try:
171
+ self.client.login(self.username, self.password)
172
+ self.authenticated = True
173
+ logger.info(f"✅ Successfully authenticated with Bluesky as {self.username}")
174
+ return True
175
+ except Exception as e:
176
+ logger.error(f"❌ Error authenticating with Bluesky: {str(e)}")
177
+ return False
178
+
179
+ def _construct_uri(self, url: str) -> str:
180
+ # Extract post URI from the URL - Bluesky URLs typically look like:
181
+ # https://bsky.app/profile/username.bsky.social/post/abcdef123
182
+ try:
183
+ # Parse URL to extract components for building the AT URI
184
+ parts = url.split('/')
185
+ if len(parts) < 5 or "bsky.app" not in url:
186
+ raise Exception(f"Invalid Bluesky URL format for {url}.")
187
+
188
+ # Find the profile part of the URL
189
+ profile_idx = -1
190
+ for i, part in enumerate(parts):
191
+ if part == "profile":
192
+ profile_idx = i
193
+ break
194
+
195
+ if profile_idx < 0 or profile_idx + 3 >= len(parts):
196
+ raise Exception(f"Could not extract profile or post ID from {url}.")
197
+
198
+ handle = parts[profile_idx + 1]
199
+ post_id = parts[profile_idx + 3]
200
+
201
+ # Resolve the handle to a DID
202
+ did = self._resolve_handle(handle)
203
+
204
+ # Construct the AT URI
205
+ uri = f"at://{did}/app.bsky.feed.post/{post_id}"
206
+
207
+ return uri
208
+
209
+ except Exception as e:
210
+ err_msg = error_to_string(e)
211
+ logger.error(f"Error retrieving Bluesky post: {err_msg}")
212
+
213
+ def _resolve_handle(self, handle: str) -> str:
214
+ """Resolve a handle to a DID."""
215
+ try:
216
+ response = self.client.resolve_handle(handle)
217
+ return response.did
218
+ except Exception as e:
219
+ err_msg = error_to_string(e)
220
+ logger.error(f"Error resolving handle: {err_msg}")
221
+ return handle # Return the handle itself as fallback
222
+
223
+
224
+ def error_to_string(error: RequestErrorBase | Exception) -> str:
225
+ """Takes an Error object containing a response and prints the contents."""
226
+ if isinstance(error, RequestErrorBase):
227
+ response = error.response
228
+ code = response.status_code
229
+ content = response.content
230
+ if isinstance(content, XrpcError):
231
+ error_type = content.error
232
+ msg = content.message
233
+ return f"Error {code} ({error_type}): {msg}."
234
+ else:
235
+ return f"Error {code}: {content}."
236
+ else:
237
+ return str(error)
@@ -7,11 +7,11 @@ from ezmm import MultimodalSequence, Image, Item, Video
7
7
  from telethon import TelegramClient
8
8
  from telethon.tl.types import Channel, User
9
9
 
10
- from scrapemm.api_keys import get_api_key
10
+ from scrapemm.secrets import get_secret
11
11
  from scrapemm.integrations.base import RetrievalIntegration
12
12
  from scrapemm.util import get_domain
13
13
 
14
- logger = logging.getLogger("Retriever")
14
+ logger = logging.getLogger("scrapeMM")
15
15
 
16
16
 
17
17
  class Telegram(RetrievalIntegration):
@@ -21,9 +21,9 @@ class Telegram(RetrievalIntegration):
21
21
  session_path = "temp/telegram"
22
22
 
23
23
  def __init__(self):
24
- api_id = int(get_api_key("telegram_api_id"))
25
- api_hash = get_api_key("telegram_api_hash")
26
- bot_token = get_api_key("telegram_bot_token")
24
+ api_id = int(get_secret("telegram_api_id")) if get_secret("telegram_api_id") else None
25
+ api_hash = get_secret("telegram_api_hash")
26
+ bot_token = get_secret("telegram_bot_token")
27
27
 
28
28
  if api_id and api_hash and bot_token:
29
29
  self.client = TelegramClient(self.session_path, api_id, api_hash)