blocket-api 0.3.5__tar.gz → 0.3.6__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.6
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,6 +15,7 @@ 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: websockets>=15.0.1
18
19
  Requires-Dist: ruff>=0.14.4
19
20
  Dynamic: license-file
20
21
 
@@ -80,6 +81,8 @@ 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.
83
86
 
84
87
 
85
88
  ## 🤓 Detailed usage
@@ -368,6 +371,59 @@ Returns all of your saved ads from `https://www.blocket.se/sparade/annonser`.
368
371
 
369
372
  ```
370
373
 
374
+ ### get_threads
375
+ Returns all of your threads/conversations from Blocket. This api requires a token.
376
+
377
+ - `limit` (`int`, optional) - Number of threads to return, default is 15.
378
+
379
+ ```py
380
+ >>> api = BlocketAPI("YourBlocketTokenHere")
381
+ >>> api.get_threads()
382
+ {
383
+ "channels": [
384
+ {
385
+ "channel_url": "sendbird_group_channel_123",
386
+ "name": "from:user-id,ad:1214281684,to:c1-user-id",
387
+ "data": {
388
+ "ad": {
389
+ "id": "1214281684",
390
+ "advertiser_name": "Kalle",
391
+ "subject": "Elsparkcykel",
392
+ "status": "active",
393
+ "url": "https://www.blocket.se/annons/1214281684",
394
+ "price": "2 400 kr"
395
+ },
396
+ },
397
+ },
398
+ ...
399
+ ],
400
+ }
401
+ ```
402
+
403
+
404
+
405
+ ### get_messages_from_thread
406
+ Returns all of your messages from an existing thread/conversation.
407
+
408
+ - `channel_id` (`str`, required) - The channel id to fetch messages from.
409
+
410
+ ```py
411
+ >>> api = BlocketAPI("YourBlocketTokenHere")
412
+ >>> api.get_messages_from_thread("sendbird_group_channel_123")
413
+ {
414
+ "messages": [
415
+ {
416
+ "type": "MESG",
417
+ "message_id": 1354913370,
418
+ "message": "Hey, is your thing still for sale?",
419
+ ...
420
+ },
421
+ ],
422
+ }
423
+ ```
424
+
425
+
426
+
371
427
  ## 🔐 Blocket API token
372
428
 
373
429
  There are two ways to acquire your token:
@@ -60,6 +60,8 @@ Some calls require a `bearerToken`. However, some calls are public and don't req
60
60
  | [`api.get_user_by_id()`](#get_user_by_id) | 👏 No | Yes | Get user data from id.
61
61
  | [`api.save_ad()`](#save_ad) | 🔐 Yes | - | Save ad to "Saved ads".
62
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.
63
65
 
64
66
 
65
67
  ## 🤓 Detailed usage
@@ -348,6 +350,59 @@ Returns all of your saved ads from `https://www.blocket.se/sparade/annonser`.
348
350
 
349
351
  ```
350
352
 
353
+ ### get_threads
354
+ Returns all of your threads/conversations from Blocket. This api requires a token.
355
+
356
+ - `limit` (`int`, optional) - Number of threads to return, default is 15.
357
+
358
+ ```py
359
+ >>> api = BlocketAPI("YourBlocketTokenHere")
360
+ >>> api.get_threads()
361
+ {
362
+ "channels": [
363
+ {
364
+ "channel_url": "sendbird_group_channel_123",
365
+ "name": "from:user-id,ad:1214281684,to:c1-user-id",
366
+ "data": {
367
+ "ad": {
368
+ "id": "1214281684",
369
+ "advertiser_name": "Kalle",
370
+ "subject": "Elsparkcykel",
371
+ "status": "active",
372
+ "url": "https://www.blocket.se/annons/1214281684",
373
+ "price": "2 400 kr"
374
+ },
375
+ },
376
+ },
377
+ ...
378
+ ],
379
+ }
380
+ ```
381
+
382
+
383
+
384
+ ### get_messages_from_thread
385
+ Returns all of your messages from an existing thread/conversation.
386
+
387
+ - `channel_id` (`str`, required) - The channel id to fetch messages from.
388
+
389
+ ```py
390
+ >>> api = BlocketAPI("YourBlocketTokenHere")
391
+ >>> api.get_messages_from_thread("sendbird_group_channel_123")
392
+ {
393
+ "messages": [
394
+ {
395
+ "type": "MESG",
396
+ "message_id": 1354913370,
397
+ "message": "Hey, is your thing still for sale?",
398
+ ...
399
+ },
400
+ ],
401
+ }
402
+ ```
403
+
404
+
405
+
351
406
  ## 🔐 Blocket API token
352
407
 
353
408
  There are two ways to acquire your token:
@@ -22,6 +22,7 @@ from blocket_api.qasa import HOME_SEARCH_ORDERING, HomeType, OrderBy, Qasa
22
22
 
23
23
  from .constants import BASE_URL, BYTBIL_URL
24
24
  from .decorators import auth_token, public_token
25
+ from .sendbird import Sendbird
25
26
 
26
27
  if TYPE_CHECKING:
27
28
  from httpx import Response
@@ -588,3 +589,23 @@ class BlocketAPI:
588
589
  url = f"{BASE_URL}/search_bff/v1/saved_content?lim={limit}"
589
590
  response = _make_request(url=f"{url}", token=self.token).json()
590
591
  return response
592
+
593
+ @auth_token
594
+ def get_threads(self, limit: int = 15) -> dict:
595
+ """
596
+ Returns set number of conversations/threads from Blocket, ordered by latest message.
597
+ """
598
+ assert self.token
599
+ sendbird = Sendbird.generate(self.token)
600
+ return sendbird.get_threads(limit=limit)
601
+
602
+ @auth_token
603
+ def get_messages_from_thread(self, channel_id: str) -> dict:
604
+ """
605
+ Returns all messages from a specific channel/thread.
606
+ Requires a channel_id which can be found by using get_threads().
607
+ Ex: sendbird_group_channel_12345_abc123abcde
608
+ """
609
+ assert self.token
610
+ sendbird = Sendbird.generate(self.token)
611
+ return sendbird.get_messages_from_thread(channel_id)
@@ -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"
@@ -0,0 +1,177 @@
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blocket_api
3
- Version: 0.3.5
3
+ Version: 0.3.6
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,6 +15,7 @@ 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: websockets>=15.0.1
18
19
  Requires-Dist: ruff>=0.14.4
19
20
  Dynamic: license-file
20
21
 
@@ -80,6 +81,8 @@ 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.
83
86
 
84
87
 
85
88
  ## 🤓 Detailed usage
@@ -368,6 +371,59 @@ Returns all of your saved ads from `https://www.blocket.se/sparade/annonser`.
368
371
 
369
372
  ```
370
373
 
374
+ ### get_threads
375
+ Returns all of your threads/conversations from Blocket. This api requires a token.
376
+
377
+ - `limit` (`int`, optional) - Number of threads to return, default is 15.
378
+
379
+ ```py
380
+ >>> api = BlocketAPI("YourBlocketTokenHere")
381
+ >>> api.get_threads()
382
+ {
383
+ "channels": [
384
+ {
385
+ "channel_url": "sendbird_group_channel_123",
386
+ "name": "from:user-id,ad:1214281684,to:c1-user-id",
387
+ "data": {
388
+ "ad": {
389
+ "id": "1214281684",
390
+ "advertiser_name": "Kalle",
391
+ "subject": "Elsparkcykel",
392
+ "status": "active",
393
+ "url": "https://www.blocket.se/annons/1214281684",
394
+ "price": "2 400 kr"
395
+ },
396
+ },
397
+ },
398
+ ...
399
+ ],
400
+ }
401
+ ```
402
+
403
+
404
+
405
+ ### get_messages_from_thread
406
+ Returns all of your messages from an existing thread/conversation.
407
+
408
+ - `channel_id` (`str`, required) - The channel id to fetch messages from.
409
+
410
+ ```py
411
+ >>> api = BlocketAPI("YourBlocketTokenHere")
412
+ >>> api.get_messages_from_thread("sendbird_group_channel_123")
413
+ {
414
+ "messages": [
415
+ {
416
+ "type": "MESG",
417
+ "message_id": 1354913370,
418
+ "message": "Hey, is your thing still for sale?",
419
+ ...
420
+ },
421
+ ],
422
+ }
423
+ ```
424
+
425
+
426
+
371
427
  ## 🔐 Blocket API token
372
428
 
373
429
  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
+ websockets>=15.0.1
10
11
  ruff>=0.14.4
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "blocket_api"
3
- version = "0.3.5"
3
+ version = "0.3.6"
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,6 +15,7 @@ dependencies = [
15
15
  "mkdocs>=1.6.1",
16
16
  "mkdocs-material>=9.6.21",
17
17
  "mkdocs-swagger-ui-tag>=0.7.2",
18
+ "websockets>=15.0.1",
18
19
  "ruff>=0.14.4",
19
20
  ]
20
21
 
File without changes
File without changes