blocket-api 0.3.5__tar.gz → 0.3.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blocket_api
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: A python API wrapper for blocket.se
5
5
  Author-email: dunderrrrrr <emil.bjorkroth@gmail.com>
6
6
  Requires-Python: >=3.10
@@ -15,7 +15,8 @@ Requires-Dist: pydantic>=2.11.7
15
15
  Requires-Dist: mkdocs>=1.6.1
16
16
  Requires-Dist: mkdocs-material>=9.6.21
17
17
  Requires-Dist: mkdocs-swagger-ui-tag>=0.7.2
18
- Requires-Dist: ruff>=0.14.4
18
+ Requires-Dist: websockets>=15.0.1
19
+ Requires-Dist: ruff>=0.14.5
19
20
  Dynamic: license-file
20
21
 
21
22
  <p align="center">
@@ -80,6 +81,9 @@ Some calls require a `bearerToken`. However, some calls are public and don't req
80
81
  | [`api.get_user_by_id()`](#get_user_by_id) | 👏 No | Yes | Get user data from id.
81
82
  | [`api.save_ad()`](#save_ad) | 🔐 Yes | - | Save ad to "Saved ads".
82
83
  | [`api.get_saved_ads()`](#get_saved_ads) | 🔐 Yes | - | List all saved ads.
84
+ | [`api.get_threads()`](#get_threads) | 🔐 Yes | - | List all threads (messages).
85
+ | [`api.get_messages_from_thread()`](#get_messages_from_thread) | 🔐 Yes | - | Get all messages from a thread.
86
+ | [`api.unread_messages_count()`](#unread_messages_count) | 🔐 Yes | Yes | Get the total count of unread messages.
83
87
 
84
88
 
85
89
  ## 🤓 Detailed usage
@@ -368,6 +372,70 @@ Returns all of your saved ads from `https://www.blocket.se/sparade/annonser`.
368
372
 
369
373
  ```
370
374
 
375
+ ### get_threads
376
+ Returns all of your threads/conversations from Blocket. This api requires a token.
377
+
378
+ - `limit` (`int`, optional) - Number of threads to return, default is 15.
379
+
380
+ ```py
381
+ >>> api = BlocketAPI("YourBlocketTokenHere")
382
+ >>> api.get_threads()
383
+ {
384
+ "channels": [
385
+ {
386
+ "channel_url": "sendbird_group_channel_123",
387
+ "name": "from:user-id,ad:1214281684,to:c1-user-id",
388
+ "data": {
389
+ "ad": {
390
+ "id": "1214281684",
391
+ "advertiser_name": "Kalle",
392
+ "subject": "Elsparkcykel",
393
+ "status": "active",
394
+ "url": "https://www.blocket.se/annons/1214281684",
395
+ "price": "2 400 kr"
396
+ },
397
+ },
398
+ },
399
+ ...
400
+ ],
401
+ }
402
+ ```
403
+
404
+
405
+
406
+ ### get_messages_from_thread
407
+ Returns all of your messages from an existing thread/conversation.
408
+
409
+ - `channel_id` (`str`, required) - The channel id to fetch messages from.
410
+
411
+ ```py
412
+ >>> api = BlocketAPI("YourBlocketTokenHere")
413
+ >>> api.get_messages_from_thread("sendbird_group_channel_123")
414
+ {
415
+ "messages": [
416
+ {
417
+ "type": "MESG",
418
+ "message_id": 1354913370,
419
+ "message": "Hey, is your thing still for sale?",
420
+ ...
421
+ },
422
+ ],
423
+ }
424
+ ```
425
+
426
+ ### unread_messages_count
427
+ Returns the count of unread messages.
428
+
429
+ ```py
430
+ >>> api = BlocketAPI("YourBlocketTokenHere")
431
+ >>> api.unread_messages_count()
432
+ {
433
+ "unread_count": 5
434
+ }
435
+ ```
436
+
437
+
438
+
371
439
  ## 🔐 Blocket API token
372
440
 
373
441
  There are two ways to acquire your token:
@@ -1,23 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: blocket_api
3
- Version: 0.3.5
4
- Summary: A python API wrapper for blocket.se
5
- Author-email: dunderrrrrr <emil.bjorkroth@gmail.com>
6
- Requires-Python: >=3.10
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Requires-Dist: httpx>=0.28.1
10
- Requires-Dist: pre-commit>=3.7.1
11
- Requires-Dist: pytest>=8.2.2
12
- Requires-Dist: respx>=0.21.1
13
- Requires-Dist: mypy>=1.15.0
14
- Requires-Dist: pydantic>=2.11.7
15
- Requires-Dist: mkdocs>=1.6.1
16
- Requires-Dist: mkdocs-material>=9.6.21
17
- Requires-Dist: mkdocs-swagger-ui-tag>=0.7.2
18
- Requires-Dist: ruff>=0.14.4
19
- Dynamic: license-file
20
-
21
1
  <p align="center">
22
2
  <img src="https://blocket-api.se/blocket-api.png" />
23
3
  </p>
@@ -80,6 +60,9 @@ Some calls require a `bearerToken`. However, some calls are public and don't req
80
60
  | [`api.get_user_by_id()`](#get_user_by_id) | 👏 No | Yes | Get user data from id.
81
61
  | [`api.save_ad()`](#save_ad) | 🔐 Yes | - | Save ad to "Saved ads".
82
62
  | [`api.get_saved_ads()`](#get_saved_ads) | 🔐 Yes | - | List all saved ads.
63
+ | [`api.get_threads()`](#get_threads) | 🔐 Yes | - | List all threads (messages).
64
+ | [`api.get_messages_from_thread()`](#get_messages_from_thread) | 🔐 Yes | - | Get all messages from a thread.
65
+ | [`api.unread_messages_count()`](#unread_messages_count) | 🔐 Yes | Yes | Get the total count of unread messages.
83
66
 
84
67
 
85
68
  ## 🤓 Detailed usage
@@ -368,6 +351,70 @@ Returns all of your saved ads from `https://www.blocket.se/sparade/annonser`.
368
351
 
369
352
  ```
370
353
 
354
+ ### get_threads
355
+ Returns all of your threads/conversations from Blocket. This api requires a token.
356
+
357
+ - `limit` (`int`, optional) - Number of threads to return, default is 15.
358
+
359
+ ```py
360
+ >>> api = BlocketAPI("YourBlocketTokenHere")
361
+ >>> api.get_threads()
362
+ {
363
+ "channels": [
364
+ {
365
+ "channel_url": "sendbird_group_channel_123",
366
+ "name": "from:user-id,ad:1214281684,to:c1-user-id",
367
+ "data": {
368
+ "ad": {
369
+ "id": "1214281684",
370
+ "advertiser_name": "Kalle",
371
+ "subject": "Elsparkcykel",
372
+ "status": "active",
373
+ "url": "https://www.blocket.se/annons/1214281684",
374
+ "price": "2 400 kr"
375
+ },
376
+ },
377
+ },
378
+ ...
379
+ ],
380
+ }
381
+ ```
382
+
383
+
384
+
385
+ ### get_messages_from_thread
386
+ Returns all of your messages from an existing thread/conversation.
387
+
388
+ - `channel_id` (`str`, required) - The channel id to fetch messages from.
389
+
390
+ ```py
391
+ >>> api = BlocketAPI("YourBlocketTokenHere")
392
+ >>> api.get_messages_from_thread("sendbird_group_channel_123")
393
+ {
394
+ "messages": [
395
+ {
396
+ "type": "MESG",
397
+ "message_id": 1354913370,
398
+ "message": "Hey, is your thing still for sale?",
399
+ ...
400
+ },
401
+ ],
402
+ }
403
+ ```
404
+
405
+ ### unread_messages_count
406
+ Returns the count of unread messages.
407
+
408
+ ```py
409
+ >>> api = BlocketAPI("YourBlocketTokenHere")
410
+ >>> api.unread_messages_count()
411
+ {
412
+ "unread_count": 5
413
+ }
414
+ ```
415
+
416
+
417
+
371
418
  ## 🔐 Blocket API token
372
419
 
373
420
  There are two ways to acquire your token:
@@ -17,11 +17,13 @@ from blocket_api.models import (
17
17
  SavedSearchResponse,
18
18
  StoreListings,
19
19
  StoreSearchResults,
20
+ UnreadMessagesCount,
20
21
  )
21
22
  from blocket_api.qasa import HOME_SEARCH_ORDERING, HomeType, OrderBy, Qasa
22
23
 
23
24
  from .constants import BASE_URL, BYTBIL_URL
24
25
  from .decorators import auth_token, public_token
26
+ from .sendbird import Sendbird
25
27
 
26
28
  if TYPE_CHECKING:
27
29
  from httpx import Response
@@ -588,3 +590,42 @@ class BlocketAPI:
588
590
  url = f"{BASE_URL}/search_bff/v1/saved_content?lim={limit}"
589
591
  response = _make_request(url=f"{url}", token=self.token).json()
590
592
  return response
593
+
594
+ @auth_token
595
+ def get_threads(self, limit: int = 15) -> dict:
596
+ """
597
+ Returns set number of conversations/threads from Blocket, ordered by latest message.
598
+ """
599
+ assert self.token
600
+ sendbird = Sendbird.generate(self.token)
601
+ return sendbird.get_threads(limit=limit)
602
+
603
+ @auth_token
604
+ def get_messages_from_thread(self, channel_id: str) -> dict:
605
+ """
606
+ Returns all messages from a specific channel/thread.
607
+ Requires a channel_id which can be found by using get_threads().
608
+ Ex: sendbird_group_channel_12345_abc123abcde
609
+ """
610
+ assert self.token
611
+ sendbird = Sendbird.generate(self.token)
612
+ return sendbird.get_messages_from_thread(channel_id)
613
+
614
+ @overload
615
+ def unread_messages_count(
616
+ self, *, as_objects: Literal[True]
617
+ ) -> UnreadMessagesCount: ...
618
+
619
+ @overload
620
+ def unread_messages_count(self, *, as_objects: Literal[False] = False) -> dict: ...
621
+
622
+ @auth_token
623
+ def unread_messages_count(
624
+ self, as_objects: bool = False
625
+ ) -> dict | UnreadMessagesCount:
626
+ """
627
+ Returns the total count of unread messages.
628
+ """
629
+ assert self.token
630
+ response = Sendbird.generate(self.token).unread_messages_count()
631
+ return UnreadMessagesCount.model_validate(response) if as_objects else response
@@ -2,3 +2,6 @@ BASE_URL = "https://api.blocket.se"
2
2
  SITE_URL = "https://www.blocket.se"
3
3
  BYTBIL_URL = "https://api.bytbil.com"
4
4
  QASA_URL = "https://api.qasa.se/graphql"
5
+
6
+ BLOCKET_SENDBIRD_APP_ID = "162ABE1D-86E9-4C2A-866A-EF1034525DAF"
7
+ SENDBIRD_URL = f"https://api-{BLOCKET_SENDBIRD_APP_ID}.sendbird.com"
@@ -612,3 +612,10 @@ class SavedSearch(_BaseModel):
612
612
 
613
613
  class SavedSearchResponse(_BaseModel):
614
614
  searches: list[SavedSearch]
615
+
616
+
617
+ ## UnreadMessagesCount ##
618
+
619
+
620
+ class UnreadMessagesCount(_BaseModel):
621
+ unread_count: int
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ import urllib.parse
6
+ from dataclasses import dataclass
7
+
8
+ import httpx
9
+ from pydantic import BaseModel, ConfigDict
10
+ from websockets.sync.client import connect
11
+
12
+ from .constants import BASE_URL, BLOCKET_SENDBIRD_APP_ID, SENDBIRD_URL
13
+
14
+ USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0"
15
+
16
+
17
+ class SendbirdResponse(BaseModel):
18
+ user_id: str
19
+ nickname: str
20
+ profile_url: str
21
+ ekey: str
22
+ metadata: dict
23
+
24
+ ping_interval: int
25
+ pong_timeout: int
26
+ login_ts: int
27
+
28
+ key: str # Session key
29
+ expires_at: int
30
+
31
+ unread_cnt: dict
32
+ premium_feature_list: list[str]
33
+ application_attributes: list[str]
34
+ services: list[str]
35
+
36
+ model_config = ConfigDict(extra="ignore")
37
+
38
+ @property
39
+ def session_key(self) -> str:
40
+ return self.key
41
+
42
+
43
+ def get_blocket_tokens(bearer_token: str) -> dict:
44
+ token_response = httpx.get(
45
+ f"{BASE_URL}/messaging-sendbird-communication/v1/users/tokens",
46
+ headers={
47
+ "Authorization": f"Bearer {bearer_token}",
48
+ },
49
+ )
50
+ token_response.raise_for_status()
51
+ return token_response.json()
52
+
53
+
54
+ class SendbirdClient:
55
+ def __init__(
56
+ self,
57
+ app_id: str,
58
+ user_id: str,
59
+ access_token: str,
60
+ ):
61
+ self.app_id = app_id
62
+ self.user_id = user_id
63
+ self.access_token = access_token
64
+
65
+ def _build_wss_url(self) -> str:
66
+ base_url = f"wss://ws-{self.app_id.lower()}.sendbird.com/"
67
+ timestamp = str(int(time.time() * 1000))
68
+
69
+ params = {
70
+ "p": "JS",
71
+ "pv": USER_AGENT,
72
+ "sv": "4.10.8",
73
+ "ai": self.app_id,
74
+ "pmce": "1",
75
+ "active": "1",
76
+ "device_token_types": "gcm,huawei,apns",
77
+ "SB-User-Agent": "JS/c4.10.8///oweb",
78
+ "SB-SDK-User-Agent": "main_sdk_info=chat/js/4.10.8&device_os_platform=web&os_version=Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0",
79
+ "Request-Sent-Timestamp": timestamp,
80
+ "include_extra_data": "premium_feature_list,file_upload_size_limit,application_attributes,emoji_hash,multiple_file_send_max_size,notifications",
81
+ "use_local_cache": "0",
82
+ "include_poll_details": "1",
83
+ "user_id": self.user_id,
84
+ }
85
+
86
+ return base_url + "?" + urllib.parse.urlencode(params)
87
+
88
+ def get_data(self) -> str:
89
+ with connect(
90
+ self._build_wss_url(),
91
+ subprotocols=[urllib.parse.quote(json.dumps({"token": self.access_token}))], # type: ignore[list-item]
92
+ additional_headers={
93
+ "User-Agent": USER_AGENT,
94
+ "Origin": "https://www.blocket.se",
95
+ },
96
+ open_timeout=5,
97
+ ) as websocket:
98
+ message: str = websocket.recv(timeout=5) # type: ignore[assignment]
99
+ return str(message.split("LOGI")[1])
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class Sendbird:
104
+ sendbird_client: SendbirdClient
105
+
106
+ @classmethod
107
+ def generate(cls, blocket_token: str) -> Sendbird:
108
+ tokens = get_blocket_tokens(blocket_token)
109
+ sb_token = tokens["sb_session_token"]
110
+ sb_user_id = tokens["sb_user_id"]
111
+ return cls(
112
+ sendbird_client=SendbirdClient(
113
+ app_id=BLOCKET_SENDBIRD_APP_ID,
114
+ user_id=sb_user_id,
115
+ access_token=sb_token,
116
+ )
117
+ )
118
+
119
+ def get_threads(self, limit: int) -> dict:
120
+ sendbird = SendbirdResponse.model_validate_json(self.sendbird_client.get_data())
121
+ response = httpx.get(
122
+ f"{SENDBIRD_URL}/v3/users/{self.sendbird_client.user_id}/my_group_channels",
123
+ params={
124
+ "token": "",
125
+ "limit": limit,
126
+ "order": "latest_last_message",
127
+ "show_member": True,
128
+ "show_read_receipt": True,
129
+ "show_delivery_receipt": True,
130
+ "show_empty": False,
131
+ "member_state_filter": "all",
132
+ "super_mode": "all",
133
+ "public_mode": "all",
134
+ "unread_filter": "all",
135
+ "name_contains": "",
136
+ "custom_type_startswith": "",
137
+ "hidden_mode": "unhidden_only",
138
+ "show_frozen": True,
139
+ "show_metadata": True,
140
+ "include_chat_notification": False,
141
+ "include_left_channel": False,
142
+ },
143
+ headers={
144
+ "Session-Key": sendbird.session_key,
145
+ "Access-Token": self.sendbird_client.access_token,
146
+ },
147
+ )
148
+ response.raise_for_status()
149
+ return response.json()
150
+
151
+ def get_messages_from_thread(self, channel_id: str) -> dict:
152
+ sendbird = SendbirdResponse.model_validate_json(self.sendbird_client.get_data())
153
+ response = httpx.get(
154
+ f"{SENDBIRD_URL}/v3/group_channels/{channel_id}/messages",
155
+ params={
156
+ "is_sdk": True,
157
+ "prev_limit": 30,
158
+ "next_limit": 0,
159
+ "include": False,
160
+ "reverse": False,
161
+ "message_ts": 9007199254740991,
162
+ "message_type": "",
163
+ "include_reply_type": "none",
164
+ "with_sorted_meta_array": False,
165
+ "include_reactions": False,
166
+ "include_thread_info": False,
167
+ "include_parent_message_info": False,
168
+ "show_subchannel_message_only": False,
169
+ "include_poll_details": True,
170
+ },
171
+ headers={
172
+ "Session-Key": sendbird.session_key,
173
+ "Access-Token": self.sendbird_client.access_token,
174
+ },
175
+ )
176
+ response.raise_for_status()
177
+ return response.json()
178
+
179
+ def unread_messages_count(self) -> dict:
180
+ sendbird = SendbirdResponse.model_validate_json(self.sendbird_client.get_data())
181
+ response = httpx.get(
182
+ f"{SENDBIRD_URL}/v3/users/{self.sendbird_client.user_id}/unread_message_count",
183
+ params={"super_mode": "all", "include_feed_channel": False},
184
+ headers={
185
+ "Session-Key": sendbird.session_key,
186
+ "Access-Token": self.sendbird_client.access_token,
187
+ },
188
+ )
189
+ response.raise_for_status()
190
+ return response.json()
@@ -1,3 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: blocket_api
3
+ Version: 0.3.7
4
+ Summary: A python API wrapper for blocket.se
5
+ Author-email: dunderrrrrr <emil.bjorkroth@gmail.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: httpx>=0.28.1
10
+ Requires-Dist: pre-commit>=3.7.1
11
+ Requires-Dist: pytest>=8.2.2
12
+ Requires-Dist: respx>=0.21.1
13
+ Requires-Dist: mypy>=1.15.0
14
+ Requires-Dist: pydantic>=2.11.7
15
+ Requires-Dist: mkdocs>=1.6.1
16
+ Requires-Dist: mkdocs-material>=9.6.21
17
+ Requires-Dist: mkdocs-swagger-ui-tag>=0.7.2
18
+ Requires-Dist: websockets>=15.0.1
19
+ Requires-Dist: ruff>=0.14.5
20
+ Dynamic: license-file
21
+
1
22
  <p align="center">
2
23
  <img src="https://blocket-api.se/blocket-api.png" />
3
24
  </p>
@@ -60,6 +81,9 @@ Some calls require a `bearerToken`. However, some calls are public and don't req
60
81
  | [`api.get_user_by_id()`](#get_user_by_id) | 👏 No | Yes | Get user data from id.
61
82
  | [`api.save_ad()`](#save_ad) | 🔐 Yes | - | Save ad to "Saved ads".
62
83
  | [`api.get_saved_ads()`](#get_saved_ads) | 🔐 Yes | - | List all saved ads.
84
+ | [`api.get_threads()`](#get_threads) | 🔐 Yes | - | List all threads (messages).
85
+ | [`api.get_messages_from_thread()`](#get_messages_from_thread) | 🔐 Yes | - | Get all messages from a thread.
86
+ | [`api.unread_messages_count()`](#unread_messages_count) | 🔐 Yes | Yes | Get the total count of unread messages.
63
87
 
64
88
 
65
89
  ## 🤓 Detailed usage
@@ -348,6 +372,70 @@ Returns all of your saved ads from `https://www.blocket.se/sparade/annonser`.
348
372
 
349
373
  ```
350
374
 
375
+ ### get_threads
376
+ Returns all of your threads/conversations from Blocket. This api requires a token.
377
+
378
+ - `limit` (`int`, optional) - Number of threads to return, default is 15.
379
+
380
+ ```py
381
+ >>> api = BlocketAPI("YourBlocketTokenHere")
382
+ >>> api.get_threads()
383
+ {
384
+ "channels": [
385
+ {
386
+ "channel_url": "sendbird_group_channel_123",
387
+ "name": "from:user-id,ad:1214281684,to:c1-user-id",
388
+ "data": {
389
+ "ad": {
390
+ "id": "1214281684",
391
+ "advertiser_name": "Kalle",
392
+ "subject": "Elsparkcykel",
393
+ "status": "active",
394
+ "url": "https://www.blocket.se/annons/1214281684",
395
+ "price": "2 400 kr"
396
+ },
397
+ },
398
+ },
399
+ ...
400
+ ],
401
+ }
402
+ ```
403
+
404
+
405
+
406
+ ### get_messages_from_thread
407
+ Returns all of your messages from an existing thread/conversation.
408
+
409
+ - `channel_id` (`str`, required) - The channel id to fetch messages from.
410
+
411
+ ```py
412
+ >>> api = BlocketAPI("YourBlocketTokenHere")
413
+ >>> api.get_messages_from_thread("sendbird_group_channel_123")
414
+ {
415
+ "messages": [
416
+ {
417
+ "type": "MESG",
418
+ "message_id": 1354913370,
419
+ "message": "Hey, is your thing still for sale?",
420
+ ...
421
+ },
422
+ ],
423
+ }
424
+ ```
425
+
426
+ ### unread_messages_count
427
+ Returns the count of unread messages.
428
+
429
+ ```py
430
+ >>> api = BlocketAPI("YourBlocketTokenHere")
431
+ >>> api.unread_messages_count()
432
+ {
433
+ "unread_count": 5
434
+ }
435
+ ```
436
+
437
+
438
+
351
439
  ## 🔐 Blocket API token
352
440
 
353
441
  There are two ways to acquire your token:
@@ -7,6 +7,7 @@ blocket_api/constants.py
7
7
  blocket_api/decorators.py
8
8
  blocket_api/models.py
9
9
  blocket_api/qasa.py
10
+ blocket_api/sendbird.py
10
11
  blocket_api.egg-info/PKG-INFO
11
12
  blocket_api.egg-info/SOURCES.txt
12
13
  blocket_api.egg-info/dependency_links.txt
@@ -7,4 +7,5 @@ pydantic>=2.11.7
7
7
  mkdocs>=1.6.1
8
8
  mkdocs-material>=9.6.21
9
9
  mkdocs-swagger-ui-tag>=0.7.2
10
- ruff>=0.14.4
10
+ websockets>=15.0.1
11
+ ruff>=0.14.5
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "blocket_api"
3
- version = "0.3.5"
3
+ version = "0.3.7"
4
4
  description = "A python API wrapper for blocket.se"
5
5
  authors = [{name = "dunderrrrrr", email = "emil.bjorkroth@gmail.com"}]
6
6
  readme = "README.md"
@@ -15,7 +15,8 @@ dependencies = [
15
15
  "mkdocs>=1.6.1",
16
16
  "mkdocs-material>=9.6.21",
17
17
  "mkdocs-swagger-ui-tag>=0.7.2",
18
- "ruff>=0.14.4",
18
+ "websockets>=15.0.1",
19
+ "ruff>=0.14.5",
19
20
  ]
20
21
 
21
22
 
File without changes
File without changes