Skip to content

Commit 1a551f7

Browse files
authored
Merge pull request #213 from arenaxr/private-objs
Add private message support
2 parents 810b1eb + d44d399 commit 1a551f7

File tree

3 files changed

+137
-11
lines changed

3 files changed

+137
-11
lines changed

arena/objects/arena_object.py

+45-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Object(BaseObject):
1313
type = "object"
1414
object_type = "entity"
1515
all_objects = {} # dict of all objects created so far
16+
private_objects = {} # dict of all private objects created so far
1617

1718
def __init__(self, evt_handler=None, update_handler=None, **kwargs):
1819
# "object_id" is required in kwargs, defaulted to random uuid4
@@ -31,6 +32,12 @@ def __init__(self, evt_handler=None, update_handler=None, **kwargs):
3132
ttl = kwargs.get("ttl", None)
3233
if "ttl" in kwargs: del kwargs["ttl"]
3334

35+
private = kwargs.get("private", False)
36+
if "private" in kwargs: del kwargs["private"]
37+
38+
private_userid = kwargs.get("private_userid", None)
39+
if "private_userid" in kwargs: del kwargs["private_userid"]
40+
3441
# remove timestamp, if exists
3542
if "timestamp" in kwargs: del kwargs["timestamp"]
3643

@@ -71,12 +78,24 @@ def __init__(self, evt_handler=None, update_handler=None, **kwargs):
7178
if ttl:
7279
self.ttl = ttl
7380

81+
# This is with regard to its interaction
82+
if private:
83+
# Note: public objects *can* have private clicks, mouseover, etc.
84+
self.private = private
85+
86+
if private_userid:
87+
self._private_userid = private_userid # None is public
88+
self.private = True # private objects are always private interaction
89+
7490
self.evt_handler = evt_handler
7591
self.update_handler = update_handler
7692
self.animations = []
7793

7894
# add current object to all_objects dict
7995
Object.add(self)
96+
# If private, add to private_objects dict
97+
if private_userid:
98+
Object.add_private(self)
8099

81100
self.delayed_prop_tasks = {} # dict of delayed property tasks
82101

@@ -90,10 +109,22 @@ def update_attributes(self, evt_handler=None, update_handler=None, **kwargs):
90109
if "data" not in self:
91110
return
92111

93-
# update "persist", and "ttl"
94-
self.persist = kwargs.get("persist", self.persist)
95-
if "ttl" in self:
96-
self.ttl = kwargs.get("ttl", self.ttl)
112+
if "persist" in kwargs:
113+
del kwargs["persist"]
114+
self.persist = kwargs.get("private")
115+
116+
if "ttl" in kwargs:
117+
del kwargs["ttl"]
118+
self.ttl = kwargs.get("ttl")
119+
120+
if "private" in kwargs:
121+
del kwargs["private"]
122+
self.private = kwargs.get("private")
123+
124+
if "private_userid" in kwargs:
125+
del kwargs["private_userid"]
126+
self._private_userid = kwargs.get("private_userid")
127+
self.private = True
97128

98129
data = self.data
99130
Data.update_data(data, kwargs)
@@ -122,7 +153,7 @@ def clickable(self):
122153

123154
def json_preprocess(self, **kwargs):
124155
# kwargs are for additional param to add to json, like "action":"create"
125-
skipped_keys = ["evt_handler", "update_handler", "animations", "delayed_prop_tasks"]
156+
skipped_keys = ["evt_handler", "update_handler", "animations", "delayed_prop_tasks", "_private_userid"]
126157
json_payload = {k: v for k, v in vars(self).items() if k not in skipped_keys}
127158
json_payload.update(kwargs)
128159
return json_payload
@@ -194,6 +225,15 @@ def add(cls, obj):
194225
object_id = obj.object_id
195226
Object.all_objects[object_id] = obj
196227

228+
@classmethod
229+
def add_private(cls, obj):
230+
private_userid = getattr(obj, "_private_userid", None)
231+
if private_userid is None:
232+
raise ValueError("No private user id specified")
233+
if private_userid not in Object.private_objects:
234+
raise ValueError(f"User {private_userid} does not exist")
235+
Object.private_objects[private_userid][obj.object_id] = obj
236+
197237
@classmethod
198238
def remove(cls, obj):
199239
object_id = obj.object_id

arena/scene.py

+38-6
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ async def process_message(self):
291291
elif action == "leave":
292292
# special user presence message, new way to remove user
293293
if object_id in self.users:
294+
self.delete_user_objects(object_id)
294295
if self.user_left_callback:
295296
self.callback_wrapper(self.user_left_callback, self.users[object_id], payload)
296297
del self.users[object_id]
@@ -347,6 +348,7 @@ async def process_message(self):
347348
self.users[object_id] = obj
348349
else:
349350
self.users[object_id] = Camera(**payload)
351+
self.reset_private_objects(object_id)
350352

351353
if self.user_join_callback:
352354
self.callback_wrapper(self.user_join_callback, self.users[object_id], payload)
@@ -463,10 +465,24 @@ def all_objects(self):
463465
"""Returns all the objects in a scene"""
464466
return Object.all_objects
465467

468+
def get_private_objects(self, userid=None):
469+
""" Returns all private user objects"""
470+
if userid is not None:
471+
return Object.private_objects.get(userid, None)
472+
else:
473+
return Object.private_objects
474+
475+
def reset_private_objects(self, userid):
476+
"""Resets all private user objects"""
477+
Object.private_objects[userid] = {}
478+
466479
def add_object(self, obj):
467480
"""Public function to create an object"""
468481
if not isinstance(obj, Object):
469482
raise ValueError(f"Not a valid ARENA object to add to scene: {type(obj)}")
483+
# We have to set program_id here, as only scene has access to its userid
484+
if getattr(obj, "private", True):
485+
obj.program_id = self.userid
470486
res = self._publish(obj, "create")
471487
self.run_animations(obj)
472488
return res
@@ -482,6 +498,10 @@ def update_object(self, obj: Object, **kwargs):
482498
if kwargs:
483499
obj.update_attributes(**kwargs)
484500

501+
# We have to update program_id here, as only scene has access to its userid
502+
if getattr(obj, "private", True):
503+
obj.program_id = self.userid
504+
485505
# Check if any keys in delayed_prop_tasks are pending new animations
486506
# and cancel corresponding final update tasks or, if they are in
487507
# kwarg property updates, cancel the task as well as the animation
@@ -518,6 +538,13 @@ def delete_object(self, obj):
518538
Object.remove(obj)
519539
return self._publish(payload, "delete", custom_payload=True)
520540

541+
def delete_user_objects(self, userid):
542+
"""Deletes any private user objects"""
543+
if userid in Object.private_objects:
544+
for obj in Object.private_objects[userid].keys():
545+
Object.all_objects.pop(obj, None)
546+
del Object.private_objects[userid]
547+
521548
def delete_program(self, obj):
522549
type=None
523550
try:
@@ -611,12 +638,17 @@ def _publish(self, obj: Object, action, custom_payload=False, publish_topic=PUBL
611638
with self.telemetry.start_publish_span(obj["object_id"], action, obj_type) as span:
612639
topic = publish_topic.substitute({**self.topicParams, **{"objectId": obj["object_id"]}})
613640

614-
# self.can_publish_obj indicates if we can publish on the default publish_topic (PUBLISH_TOPICS.SCENE_OBJECTS)
615-
if not self.can_publish_obj and publish_topic==PUBLISH_TOPICS.SCENE_OBJECTS:
616-
self.telemetry.set_error(
617-
f"ERROR: Publish failed! You do not have permission to publish to topic {topic} on {self.web_host}",
618-
span,
619-
)
641+
if publish_topic == PUBLISH_TOPICS.SCENE_OBJECTS:
642+
# self.can_publish_obj indicates if we can publish on the default publish_topic (PUBLISH_TOPICS.SCENE_OBJECTS)
643+
if not self.can_publish_obj:
644+
self.telemetry.set_error(
645+
f"ERROR: Publish failed! You do not have permission to publish to topic {topic} on {self.web_host}",
646+
span,
647+
)
648+
elif hasattr(obj, "_private_userid"):
649+
topic = PUBLISH_TOPICS.SCENE_OBJECTS_PRIVATE.substitute(
650+
{**self.topicParams, **{"objectId": obj['object_id'], "toUid": obj['_private_userid']}}
651+
)
620652

621653
d = datetime.utcnow().isoformat()[:-3] + "Z"
622654

examples/simple/private-objs.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from arena import *
2+
import random
3+
4+
5+
def user_leave_callback(scene, cam, msg):
6+
print("left:", cam.object_id)
7+
print("Private Objects:", scene.get_private_objects())
8+
9+
def report_click(scene, evt, msg):
10+
if evt.type == "mousedown":
11+
print(f"User {evt.object_id} clicked on {evt.data.target}")
12+
13+
def user_join_callback(scene, cam, msg):
14+
username = cam.object_id
15+
print("joined:", username)
16+
random_y = 0.5 + random.randrange(3)
17+
random_z = -1 - random.randrange(5)
18+
# Text that only user can see
19+
user_text = Text(
20+
object_id=f"text_{username}",
21+
value=f"Hello {username}!",
22+
align="center",
23+
font="mozillavr",
24+
# https://aframe.io/docs/1.4.0/components/text.html#stock-fonts
25+
position=(0, random_y, random_z),
26+
scale=(1.5, 1.5, 1.5),
27+
color=(100, 255, 255),
28+
private_userid=username,
29+
)
30+
# Clickable box that only user can see
31+
user_box = Box(
32+
object_id=f"box_{username}",
33+
position=(0, 0.75 + random_y, random_z),
34+
scale=(0.5, 0.5, 0.5),
35+
color=(100, 255, 255),
36+
private_userid=username,
37+
clickable=True,
38+
evt_handler=report_click,
39+
)
40+
# Red box that everyone can see
41+
user_public_box = Box(
42+
object_id=f"box_{username}_public",
43+
position=(0, 1.25 + random_y, random_z),
44+
scale=(0.5, 0.5, 0.5),
45+
color=(255, 0, 0),
46+
)
47+
scene.add_objects([user_text, user_box, user_public_box])
48+
print("Private Objects:", scene.get_private_objects())
49+
50+
scene = Scene(host="arenaxr.org", scene="example")
51+
scene.user_join_callback = user_join_callback
52+
scene.user_left_callback = user_leave_callback
53+
54+
scene.run_tasks()

0 commit comments

Comments
 (0)