Skip to content

Commit 8bbb7f7

Browse files
author
Sebastian Molenda
authored
Message actions and PAM properly tested (#219)
* Message actions properly tested * Linter * Improved grant/revoke token tests with usage example
1 parent 4e6d2f7 commit 8bbb7f7

33 files changed

+3609
-94
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import os
2+
from typing import Dict, Any
3+
from pubnub.models.consumer.message_actions import PNMessageAction
4+
from pubnub.pnconfiguration import PNConfiguration
5+
from pubnub.pubnub import PubNub
6+
7+
8+
# snippet.init_pubnub
9+
def initialize_pubnub(
10+
publish_key: str,
11+
subscribe_key: str,
12+
user_id: str
13+
) -> PubNub:
14+
"""
15+
Initialize a PubNub instance with the provided configuration.
16+
17+
Args:
18+
publish_key (str): PubNub publish key
19+
subscribe_key (str): PubNub subscribe key
20+
user_id (str): User identifier for PubNub
21+
22+
Returns:
23+
PubNub: Configured PubNub instance ready for publishing and subscribing
24+
"""
25+
pnconfig = PNConfiguration()
26+
27+
# Configure keys with provided values
28+
pnconfig.publish_key = publish_key
29+
pnconfig.subscribe_key = subscribe_key
30+
pnconfig.user_id = user_id
31+
32+
return PubNub(pnconfig)
33+
# snippet.end
34+
35+
36+
# snippet.publish_message
37+
def publish_message(pubnub: PubNub, channel: str, message: Any) -> Dict:
38+
"""
39+
Publish a message to a specific channel.
40+
41+
Args:
42+
pubnub (PubNub): PubNub instance
43+
channel (str): Channel to publish to
44+
message (Any): Message content to publish
45+
46+
Returns:
47+
Dict: Publish operation result containing timetoken
48+
"""
49+
envelope = pubnub.publish().channel(channel).message(message).sync()
50+
return envelope.result
51+
# snippet.end
52+
53+
54+
# snippet.publish_reaction
55+
def publish_reaction(
56+
pubnub: PubNub,
57+
channel: str,
58+
message_timetoken: str,
59+
reaction_type: str,
60+
reaction_value: str,
61+
user_id: str
62+
63+
) -> Dict:
64+
"""
65+
Publish a reaction to a specific message.
66+
67+
Args:
68+
pubnub (PubNub): PubNub instance
69+
channel (str): Channel where the original message was published
70+
message_timetoken (str): Timetoken of the message to react to
71+
reaction_type (str): Type of reaction (e.g. "smile", "thumbs_up")
72+
73+
Returns:
74+
Dict: Reaction publish operation result
75+
"""
76+
message_action = PNMessageAction().create(
77+
type=reaction_type,
78+
value=reaction_value,
79+
message_timetoken=message_timetoken,
80+
user_id=user_id
81+
)
82+
envelope = pubnub.add_message_action().channel(channel).message_action(message_action).sync()
83+
84+
return envelope.result
85+
# snippet.end
86+
87+
88+
# snippet.get_reactions
89+
def get_reactions(pubnub: PubNub, channel: str, start_timetoken: str, end_timetoken: str, limit: str) -> Dict:
90+
"""
91+
Get reactions for a specific message.
92+
93+
Args:
94+
pubnub (PubNub): PubNub instance
95+
channel (str): Channel where the original message was published
96+
start_timetoken (str): Start timetoken of the message to get reactions for
97+
end_timetoken (str): End timetoken of the message to get reactions for
98+
limit (str): Limit the number of reactions to return
99+
Returns:
100+
Dict: Reactions for the message
101+
"""
102+
envelope = pubnub.get_message_actions() \
103+
.channel(channel) \
104+
.start(start_timetoken) \
105+
.end(end_timetoken) \
106+
.limit(limit) \
107+
.sync()
108+
return envelope.result
109+
# snippet.end
110+
111+
112+
# snippet.remove_reaction
113+
def remove_reaction(pubnub: PubNub, channel: str, message_timetoken: str, action_timetoken: str) -> Dict:
114+
"""
115+
Remove a reaction from a specific message.
116+
117+
Args:
118+
pubnub (PubNub): PubNub instance
119+
channel (str): Channel where the original message was published
120+
message_timetoken (str): Timetoken of the message to react to
121+
action_timetoken (str): Timetoken of the reaction to remove
122+
"""
123+
envelope = pubnub.remove_message_action() \
124+
.channel(channel) \
125+
.message_timetoken(message_timetoken) \
126+
.action_timetoken(action_timetoken) \
127+
.sync()
128+
return envelope.result
129+
# snippet.end
130+
131+
132+
def main() -> None:
133+
"""
134+
Main execution function.
135+
"""
136+
# Get configuration from environment variables or use defaults
137+
publish_key = os.getenv('PUBLISH_KEY', 'demo')
138+
subscribe_key = os.getenv('SUBSCRIBE_KEY', 'demo')
139+
user_id = os.getenv('USER_ID', 'example-user')
140+
141+
# snippet.usage_example
142+
# Initialize PubNub instance with configuration
143+
# If environment variables are not set, demo keys will be used
144+
pubnub = initialize_pubnub(
145+
publish_key=publish_key,
146+
subscribe_key=subscribe_key,
147+
user_id=user_id
148+
)
149+
150+
# Channel where all the communication will happen
151+
channel = "my_channel"
152+
153+
# Message that will receive reactions
154+
message = "Hello, PubNub!"
155+
156+
# Step 1: Publish initial message
157+
# The timetoken is needed to add reactions to this specific message
158+
result = publish_message(pubnub, channel, message)
159+
message_timetoken = result.timetoken
160+
assert result.timetoken is not None, "Message publish failed - no timetoken returned"
161+
assert isinstance(result.timetoken, (int, str)) and str(result.timetoken).isnumeric(), "Invalid timetoken format"
162+
print(f"Published message with timetoken: {result.timetoken}")
163+
164+
# Step 2: Add different types of reactions from different users
165+
# First reaction: text-based reaction from guest_1
166+
reaction_type = "text"
167+
reaction_value = "Hello"
168+
first_reaction = publish_reaction(pubnub, channel, message_timetoken, reaction_type, reaction_value, "guest_1")
169+
print(f"Added first reaction {first_reaction.__dict__}")
170+
assert first_reaction is not None, "Reaction publish failed - no result returned"
171+
assert isinstance(first_reaction, PNMessageAction), "Invalid reaction result type"
172+
173+
# Second reaction: emoji-based reaction from guest_2
174+
reaction_type = "emoji"
175+
reaction_value = "👋"
176+
second_reaction = publish_reaction(pubnub, channel, message_timetoken, reaction_type, reaction_value, "guest_2")
177+
print(f"Added second reaction {second_reaction.__dict__}")
178+
assert second_reaction is not None, "Reaction publish failed - no result returned"
179+
assert isinstance(second_reaction, PNMessageAction), "Invalid reaction result type"
180+
181+
# Step 3: Fetch the message with its reactions from history
182+
fetch_result = pubnub.fetch_messages()\
183+
.channels(channel)\
184+
.include_message_actions(True)\
185+
.count(1)\
186+
.sync()
187+
188+
messages = fetch_result.result.channels[channel]
189+
print(f"Fetched message with reactions: {messages[0].__dict__}")
190+
assert len(messages) == 1, "Message not found in history"
191+
assert hasattr(messages[0], 'actions'), "Message actions not included in response"
192+
assert len(messages[0].actions) == 2, "Unexpected number of actions in history"
193+
194+
# Step 4: Retrieve all reactions for the message
195+
# We use a time window around the message timetoken to fetch reactions
196+
# The window is 1000 time units before and after the message
197+
start_timetoken = str(int(message_timetoken) - 1000)
198+
end_timetoken = str(int(message_timetoken) + 1000)
199+
reactions = get_reactions(pubnub, channel, start_timetoken, end_timetoken, "100")
200+
print(f"Reactions found: {len(reactions.actions)}")
201+
assert len(reactions.actions) == 2, "Unexpected number of reactions"
202+
203+
# Step 5: Display and remove each reaction
204+
for reaction in reactions.actions:
205+
print(f" Reaction: {reaction.__dict__}")
206+
# Remove the reaction and confirm removal
207+
remove_reaction(pubnub, channel, reaction.message_timetoken, reaction.action_timetoken)
208+
print(f"Removed reaction {reaction.__dict__}")
209+
210+
# Step 6: Verify reactions were removed
211+
# Fetch reactions again - should be empty now
212+
reactions = get_reactions(pubnub, channel, start_timetoken, end_timetoken, "100")
213+
print(f"Reactions found: {len(reactions.actions)}")
214+
assert len(reactions.actions) == 0, "Unexpected number of reactions"
215+
# snippet.end
216+
217+
218+
if __name__ == '__main__':
219+
main()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import os
2+
import time
3+
from pubnub.exceptions import PubNubException
4+
from pubnub.models.consumer.v3.channel import Channel
5+
from pubnub.pubnub import PubNub
6+
from pubnub.pnconfiguration import PNConfiguration
7+
8+
# We are using keyset with Access Manager enabled.
9+
# Admin has superpowers and can grant tokens, access to all channels, etc. Notice admin has secret key.
10+
admin_config = PNConfiguration()
11+
admin_config.publish_key = os.environ.get('PUBLISH_PAM_KEY', 'demo')
12+
admin_config.subscribe_key = os.environ.get('SUBSCRIBE_PAM_KEY', 'demo')
13+
admin_config.secret_key = os.environ.get('SECRET_PAM_KEY', 'demo')
14+
admin_config.uuid = "example_admin"
15+
16+
# User also has the same keyset as admin.
17+
# User has limited access to the channels they are granted access to. Notice user has no secret key.
18+
user_config = PNConfiguration()
19+
user_config.publish_key = os.environ.get('PUBLISH_PAM_KEY', 'demo')
20+
user_config.subscribe_key = os.environ.get('SUBSCRIBE_PAM_KEY', 'demo')
21+
user_config.uuid = "example_user"
22+
23+
admin = PubNub(admin_config)
24+
user = PubNub(user_config)
25+
26+
try:
27+
user.publish().channel("test_channel").message("test message").sync()
28+
except PubNubException as e:
29+
print(f"User cannot publish to test_channel as expected.\nError: {e}")
30+
31+
# admin can grant tokens to users
32+
grant_envelope = admin.grant_token() \
33+
.channels([Channel.id("test_channel").read().write().manage().update().join().delete()]) \
34+
.authorized_uuid("example_user") \
35+
.ttl(1) \
36+
.sync()
37+
assert grant_envelope.status.status_code == 200
38+
39+
token = grant_envelope.result.get_token()
40+
assert token is not None
41+
42+
user.set_token(token)
43+
user.publish().channel("test_channel").message("test message").sync()
44+
45+
# admin can revoke tokens
46+
revoke_envelope = admin.revoke_token(token).sync()
47+
assert revoke_envelope.status.status_code == 200
48+
49+
# We have to wait for the token revoke to propagate.
50+
time.sleep(10)
51+
52+
# user cannot publish to test_channel after token is revoked
53+
try:
54+
user.publish().channel("test_channel").message("test message").sync()
55+
except PubNubException as e:
56+
print(f"User cannot publish to test_channel any more.\nError: {e}")

pubnub/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ class PNOperationType(object):
127127
PNRemoveSpaceUsersOperation = 82
128128
PNFetchUserMembershipsOperation = 85
129129
PNFetchSpaceMembershipsOperation = 86
130+
# NOTE: remember to update PubNub.managers.TelemetryManager.endpoint_name_for_operation() when adding operations
130131

131132

132133
class PNHeartbeatNotificationOptions(object):

pubnub/managers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ def endpoint_name_for_operation(operation_type):
470470
endpoint = {
471471
PNOperationType.PNPublishOperation: 'pub',
472472
PNOperationType.PNFireOperation: 'pub',
473+
PNOperationType.PNSendFileNotification: "pub",
473474

474475
PNOperationType.PNHistoryOperation: 'hist',
475476
PNOperationType.PNHistoryDeleteOperation: 'hist',
@@ -534,6 +535,29 @@ def endpoint_name_for_operation(operation_type):
534535
PNOperationType.PNDownloadFileAction: 'file',
535536
PNOperationType.PNSendFileAction: 'file',
536537

538+
539+
PNOperationType.PNFetchMessagesOperation: "hist",
540+
541+
PNOperationType.PNCreateSpaceOperation: "obj",
542+
PNOperationType.PNUpdateSpaceOperation: "obj",
543+
PNOperationType.PNFetchSpaceOperation: "obj",
544+
PNOperationType.PNFetchSpacesOperation: "obj",
545+
PNOperationType.PNRemoveSpaceOperation: "obj",
546+
547+
PNOperationType.PNCreateUserOperation: "obj",
548+
PNOperationType.PNUpdateUserOperation: "obj",
549+
PNOperationType.PNFetchUserOperation: "obj",
550+
PNOperationType.PNFetchUsersOperation: "obj",
551+
PNOperationType.PNRemoveUserOperation: "obj",
552+
553+
PNOperationType.PNAddUserSpacesOperation: "obj",
554+
PNOperationType.PNAddSpaceUsersOperation: "obj",
555+
PNOperationType.PNUpdateUserSpacesOperation: "obj",
556+
557+
PNOperationType.PNUpdateSpaceUsersOperation: "obj",
558+
PNOperationType.PNFetchUserMembershipsOperation: "obj",
559+
PNOperationType.PNFetchSpaceMembershipsOperation: "obj",
560+
537561
}[operation_type]
538562

539563
return endpoint

pubnub/models/consumer/message_actions.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
class PNMessageAction(object):
1+
class PNMessageAction:
22
def __init__(self, message_action=None):
33
if message_action is not None:
44
self.type = message_action['type']
@@ -13,6 +13,23 @@ def __init__(self, message_action=None):
1313
self.uuid = None
1414
self.action_timetoken = None
1515

16+
def create(self, *, type: str = None, value: str = None, message_timetoken: str = None,
17+
user_id: str = None) -> 'PNMessageAction':
18+
"""
19+
Create a new message action convenience method.
20+
21+
:param type: Type of the message action
22+
:param value: Value of the message action
23+
:param message_timetoken: Timetoken of the message
24+
:param user_id: User ID of the message
25+
"""
26+
27+
self.type = type
28+
self.value = value
29+
self.message_timetoken = message_timetoken
30+
self.uuid = user_id
31+
return self
32+
1633
def __str__(self):
1734
return "Message action with tt: %s for uuid %s with value %s " % (self.action_timetoken, self.uuid, self.value)
1835

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
# flake8: noqa
22
from examples.native_sync.file_handling import main as test_file_handling
3+
4+
from examples.native_sync.message_reactions import main as test_message_reactions

0 commit comments

Comments
 (0)