Ver código fonte

Refactoring of Jitsi-Meet integration : better organisation and context from jitsi token support

Guénaël Muller 6 anos atrás
pai
commit
226ccaef4b

+ 15 - 32
tracim/tracim/controllers/jitsi_meet.py Ver arquivo

@@ -1,3 +1,4 @@
1
+import typing
1 2
 import tg
2 3
 from tg import abort
3 4
 from tg import expose
@@ -6,11 +7,11 @@ from tg.predicates import not_anonymous
6 7
 from tracim.lib.predicates import current_user_is_reader
7 8
 from sqlalchemy.orm.exc import NoResultFound
8 9
 
9
-from tracim.lib.utils import str_as_alpha_num_str
10
-from tracim.lib.jitsi_meet.jitsi_meet import JitsiMeetRoom
11
-from tracim.lib.jitsi_meet.jitsi_meet import JitsiTokenConfig
10
+from tracim.lib.jitsi_meet.room import JitsiMeetRoom
11
+from tracim.lib.jitsi_meet.token import JitsiMeetUser
12 12
 from tracim.config.app_cfg import CFG
13 13
 
14
+from tracim.model.data import User
14 15
 from tracim.model.serializers import Context, CTX, DictLikeClass
15 16
 from tracim.controllers import TIMRestController, TIMRestPathContextSetup
16 17
 
@@ -25,18 +26,22 @@ class JitsiMeetController(TIMRestController):
25 26
         except NoResultFound:
26 27
             abort(404)
27 28
 
28
-
29 29
     @tg.require(current_user_is_reader())
30 30
     @expose('tracim.templates.videoconf.jitsi_meet')
31 31
     def get(self):
32
-        return self._process()
32
+        user = tmpl_context.current_user
33
+        return self._jitsi_room(jitsi_user=user)
33 34
 
34 35
     @tg.require(current_user_is_reader())
35 36
     @expose('tracim.templates.videoconf.invite')
36 37
     def invite(self):
37
-        return self._process()
38
+        return self._jitsi_room()
38 39
 
39
-    def _process(self):
40
+    @classmethod
41
+    def _jitsi_room(
42
+            cls,
43
+            jitsi_user: typing.Union[JitsiMeetUser, User, None]=None,
44
+    ):
40 45
         cfg = CFG.get_instance()
41 46
         if not cfg.JITSI_MEET_ACTIVATED:
42 47
             abort(404)
@@ -50,32 +55,10 @@ class JitsiMeetController(TIMRestController):
50 55
         dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace,
51 56
                                                             'workspace')
52 57
 
53
-        # TODO - G.M - 18-01-2017 -
54
-        # allow to set specific room name from workspace object ?
55
-        room = "{uuid}{workspace_id}{workspace_label}".format(
56
-            uuid=cfg.TRACIM_INSTANCE_UUID,
57
-            workspace_id=workspace.workspace_id,
58
-            workspace_label=workspace.label)
59
-
60
-        # Jitsi-Meet doesn't like specials_characters
61
-        room = str_as_alpha_num_str(room)
62
-
63
-        token = None
64
-        if cfg.JITSI_MEET_USE_TOKEN:
65
-            if cfg.JITSI_MEET_TOKEN_GENERATOR == 'local':
66
-                token = JitsiTokenConfig(
67
-                    app_id=cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_APP_ID,
68
-                    secret=cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_SECRET,
69
-                    alg=cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_ALG,
70
-                    duration=cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_DURATION,
71
-                )
72
-            else:
73
-                abort(400)
74
-
75 58
         jitsi_meet_room = JitsiMeetRoom(
76
-            room=room,
77
-            domain=cfg.JITSI_MEET_DOMAIN,
78
-            token_config=token)
59
+            issuer=jitsi_user,
60
+            receivers=workspace,
61
+        )
79 62
 
80 63
         return DictLikeClass(fake_api=fake_api,
81 64
                              result=dictified_workspace,

+ 0 - 83
tracim/tracim/lib/jitsi_meet/jitsi_meet.py Ver arquivo

@@ -1,83 +0,0 @@
1
-import datetime
2
-import typing
3
-
4
-import jwt
5
-
6
-class NoTokenConfigError(Exception):
7
-    pass
8
-
9
-class JitsiTokenConfig(object):
10
-
11
-    def __init__(self,
12
-                 app_id: str,
13
-                 secret: str,
14
-                 alg: str,
15
-                 duration: int,
16
-                 )-> None:
17
-        """
18
-        JWT token generator config for JitsiMeet,
19
-        :param app_id: application identifier
20
-        :param secret: secret share between token generator and XMPP server
21
-        :param alg: algorithm used
22
-        :param duration: duration of token
23
-        """
24
-        self.app_id = app_id
25
-        self.secret = secret
26
-        self.alg = alg
27
-        self.duration = duration
28
-
29
-
30
-class JitsiMeetRoom(object):
31
-
32
-    def __init__(self,
33
-                 domain: str,
34
-                 room: str,
35
-                 token_config: typing.Optional[JitsiTokenConfig],
36
-                 ) -> None:
37
-        """
38
-        JitsiMeet room Parameters
39
-        :param domain: jitsi-meet domain
40
-        :param room: room name
41
-        :param token_config: token config, None if token not used.
42
-        """
43
-        self.domain = domain
44
-        self.room = room
45
-        self.token_config = token_config
46
-
47
-    def generate_token(self) -> str:
48
-        """
49
-        Create jwt token according to room name and config
50
-        see https://github.com/jitsi/lib-jitsi-meet/blob/master/doc/tokens.md
51
-        :return: jwt encoded token as string
52
-        """
53
-        if not self.token_config:
54
-            raise NoTokenConfigError
55
-        now = datetime.datetime.utcnow()
56
-        exp = now+datetime.timedelta(seconds=self.token_config.duration)
57
-        data = {
58
-            "iss": self.token_config.app_id,  # Issuer
59
-            "room": self.room,  # Custom-param for jitsi_meet
60
-            "aud": "*",  # TODO: Understood this param
61
-            "exp": exp,  # Expiration date
62
-            "nbf": now,  # NotBefore
63
-            "iat": now,   # IssuedAt
64
-        }
65
-        jwt_token = jwt.encode(data,
66
-                               self.token_config.secret,
67
-                               algorithm=self.token_config.alg)
68
-        return jwt_token.decode("utf-8")
69
-
70
-    def generate_url(self) -> str:
71
-        """
72
-        Generate url with or without token
73
-        :return: url as string
74
-        """
75
-        if self.token_config:
76
-            token = self.generate_token()
77
-            url = "{}/{}?jwt={}".format(self.domain,
78
-                                        self.room,
79
-                                        token,)
80
-        else:
81
-            url = "{}/{}".format(self.domain,
82
-                                 self.room,)
83
-        return "https://{}".format(url)

+ 124 - 0
tracim/tracim/lib/jitsi_meet/room.py Ver arquivo

@@ -0,0 +1,124 @@
1
+import typing
2
+from tracim.lib.jitsi_meet.token import JitsiMeetToken
3
+from tracim.lib.jitsi_meet.token import JitsiMeetUser
4
+from tracim.lib.jitsi_meet.token import JitsiMeetContext
5
+from tracim.lib.utils import str_as_alpha_num_str
6
+from tracim.config.app_cfg import CFG
7
+from tracim.model.data import Workspace
8
+from tracim.model.data import User
9
+import uuid
10
+
11
+
12
+class JitsiMeetNoTokenGenerator(Exception):
13
+    pass
14
+
15
+
16
+class JitsiMeetTokenNotActivated(Exception):
17
+    pass
18
+
19
+
20
+class JitsiMeetRoom(object):
21
+    def __init__(
22
+            self,
23
+            receivers: Workspace,
24
+            issuer: typing.Union[User, JitsiMeetUser, None]=None,
25
+    ) -> None:
26
+        """
27
+        :param issuer: user who initiated Jitsi Meet talk
28
+        if None, default user is created. Can be both Tracim User or
29
+        JitsiMeetUser.
30
+        :param receivers: User or Room who can talk with sender. Now, only
31
+        Workspace are supported.
32
+        """
33
+        self._set_domain()
34
+        self._set_token_params()
35
+        self._set_context(
36
+            receivers=receivers,
37
+            issuer=issuer,
38
+        )
39
+        self.room = self._generate_room_name(receivers)
40
+
41
+    def _set_domain(self):
42
+        cfg = CFG.get_instance()
43
+        self.domain = cfg.JITSI_MEET_DOMAIN
44
+
45
+    def _set_token_params(self):
46
+        cfg = CFG.get_instance()
47
+        self.use_token = cfg.JITSI_MEET_USE_TOKEN
48
+        if self.use_token:
49
+            if cfg.JITSI_MEET_TOKEN_GENERATOR == 'local':
50
+                self.token_app_id = cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_APP_ID  # nopep8
51
+                self.token_secret = cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_SECRET  # nopep8
52
+                self.token_alg = cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_ALG
53
+                self.token_duration = cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_DURATION  # nopep8
54
+            else:
55
+                raise JitsiMeetNoTokenGenerator
56
+
57
+    def _set_context(
58
+            self,
59
+            receivers: Workspace,
60
+            issuer: typing.Union[User, JitsiMeetUser, None],
61
+    ):
62
+
63
+        # User
64
+        if isinstance(issuer, JitsiMeetUser):
65
+            user = issuer
66
+        elif isinstance(issuer, User):
67
+            user = JitsiMeetUser(
68
+                name=issuer.display_name,
69
+                avatar_url=None,
70
+                jitsi_meet_user_id=issuer.display_name,
71
+            )
72
+        else:
73
+            user = JitsiMeetUser(
74
+                jitsi_meet_user_id=str(uuid.uuid4()),
75
+            )
76
+
77
+        # Group
78
+        group = receivers.label
79
+
80
+        self.context = JitsiMeetContext(
81
+            user=user,
82
+            group=group,
83
+        )
84
+
85
+    @classmethod
86
+    def _generate_room_name(cls, workspace: Workspace):
87
+        cfg = CFG.get_instance()
88
+        room = "{uuid}{workspace_id}{workspace_label}".format(
89
+            uuid=cfg.TRACIM_INSTANCE_UUID,
90
+            workspace_id=workspace.workspace_id,
91
+            workspace_label=workspace.label)
92
+
93
+        # Jitsi-Meet doesn't like specials_characters
94
+        return str_as_alpha_num_str(room)
95
+
96
+    def generate_token(self) -> str:
97
+        if not self.use_token:
98
+            raise JitsiMeetTokenNotActivated
99
+
100
+        token = JitsiMeetToken(
101
+            domain=self.domain,
102
+            room=self.room,
103
+            app_id=self.token_app_id,
104
+            secret=self.token_secret,
105
+            alg=self.token_alg,
106
+            duration=self.token_duration,
107
+            context=self.context,
108
+        )
109
+        return token.generate()
110
+
111
+    def generate_url(self, token=None) -> str:
112
+        """
113
+        Generate url with or without token
114
+        :return: url as string
115
+        """
116
+        if token:
117
+            url = "{}/{}?jwt={}".format(self.domain,
118
+                                        self.room,
119
+                                        token)
120
+        else:
121
+            url = "{}/{}".format(self.domain,
122
+                                 self.room,
123
+                                 )
124
+        return "https://{}".format(url)

+ 117 - 0
tracim/tracim/lib/jitsi_meet/token.py Ver arquivo

@@ -0,0 +1,117 @@
1
+import datetime
2
+import typing
3
+import jwt
4
+
5
+
6
+class JitsiMeetUser(object):
7
+
8
+    def __init__(
9
+            self,
10
+            jitsi_meet_user_id: str,
11
+            name: typing.Optional[str] = None,
12
+            email: typing.Optional[str] = None,
13
+            avatar_url: typing.Optional[str] = None,
14
+    ):
15
+        """
16
+
17
+        :param avatar_url: url for user avatar_url
18
+        :param name: display name of user
19
+        :param email: email of user
20
+        :param jitsi_meet_user_id: Jitsi-Meet id of user
21
+        """
22
+        self.avatar_url = avatar_url
23
+        self.name = name
24
+        self.email = email
25
+        self.jitsi_meet_user_id = jitsi_meet_user_id
26
+
27
+    def as_dict(self) -> dict:
28
+        data = {
29
+            'id': self.jitsi_meet_user_id,
30
+        }
31
+        if self.name:
32
+            data['name'] = self.name
33
+        if self.email:
34
+            data['email'] = self.email
35
+        if self.avatar_url:
36
+            data['avatar'] = self.avatar_url
37
+        return data
38
+
39
+
40
+class JitsiMeetContext(object):
41
+
42
+    def __init__(
43
+            self,
44
+            user: typing.Optional[JitsiMeetUser]=None,
45
+            callee: typing.Optional[JitsiMeetUser]=None,
46
+            group: str="default",
47
+    ):
48
+        """
49
+        Context as in JitsiToken Payload
50
+        see https://github.com/jitsi/lib-jitsi-meet/blob/52eb3decf6542413c739ce2209456fac728a89d5/doc/tokens.md  # nopep8
51
+        :param user: Current user
52
+        :param callee: User Who respond in 1-to-1 conf
53
+        :param group: Used only for stats
54
+        """
55
+        self.user = user
56
+        self.callee = callee
57
+        self.group = group
58
+
59
+    def as_dict(self) -> dict:
60
+        data = {}
61
+        if self.callee:
62
+            data['callee'] = self.callee.as_dict()
63
+        if self.user:
64
+            data['user'] = self.user.as_dict()
65
+        if self.group:
66
+            data['group'] = self.group
67
+        return data
68
+
69
+
70
+class JitsiMeetToken(object):
71
+
72
+    def __init__(
73
+            self,
74
+            domain: str,
75
+            room: str,
76
+            app_id: str,
77
+            secret: str,
78
+            alg: str,
79
+            duration: int,
80
+            context: typing.Optional[JitsiMeetContext] = None,
81
+    )-> None:
82
+        """
83
+        JWT token generator for JitsiMeet,
84
+        :param app_id: application identifier
85
+        :param secret: secret share between token generator and XMPP server
86
+        :param alg: algorithm used
87
+        :param duration: duration of token
88
+        :param domain: jitsi-meet domain
89
+        :param room: room name
90
+        """
91
+        self.room = room
92
+        self.domain = domain
93
+        self.app_id = app_id
94
+        self.secret = secret
95
+        self.alg = alg
96
+        self.duration = duration
97
+        self.context = context
98
+
99
+    def generate(self) -> str:
100
+        now = datetime.datetime.utcnow()
101
+        exp = now+datetime.timedelta(seconds=self.duration)
102
+        data = {
103
+            "iss": self.app_id,  # Issuer
104
+            "room": self.room,  # Custom-param for jitsi_meet
105
+            "aud": "*",  # TODO: Understood this param
106
+            "exp": exp,  # Expiration date
107
+            "nbf": now,  # NotBefore
108
+            "iat": now,   # IssuedAt
109
+        }
110
+        if self.context:
111
+            data['context'] = self.context.as_dict()
112
+        jwt_token = jwt.encode(
113
+            data,
114
+            self.secret,
115
+            algorithm=self.alg
116
+        )
117
+        return jwt_token.decode("utf-8")

+ 1 - 1
tracim/tracim/templates/videoconf/invite.mak Ver arquivo

@@ -11,7 +11,7 @@
11 11
         <p>
12 12
         ${_('To invite someone from outside of tracim into this conference, share this link. This link is available for 5 minutes.')}
13 13
         </p>
14
-        <textarea readonly=readonly wrap="off" style="width: 100%;">${jitsi_meet_room.generate_url()}</textarea>
14
+        <textarea readonly=readonly wrap="off" style="width: 100%;">${jitsi_meet_room.generate_url(jitsi_meet_room.generate_token())}</textarea>
15 15
     </div>
16 16
     <div class="modal-footer">
17 17
         <button type="button" class="btn btn-default" data-dismiss="modal">${_('Close')}</button>

+ 10 - 5
tracim/tracim/templates/videoconf/jitsi_meet.mak Ver arquivo

@@ -46,7 +46,7 @@
46 46
         var options = {
47 47
 	    // jitsi-meet support now(10-2017) only one way to auto-auth, token,
48 48
 	    // which is anonymous BOSH auth with specific url (with token value in params of the url).
49
-            %if jitsi_meet_room.token_config:
49
+            %if jitsi_meet_room.use_token:
50 50
                 jwt: '${jitsi_meet_room.generate_token()}',
51 51
             %endif
52 52
             roomName : '${jitsi_meet_room.room}',
@@ -87,12 +87,17 @@
87 87
         // when others clients use resource part of the Jabber id to do it.
88 88
         // That's why displayName compat with others XMPP client is not optimal.
89 89
         // check this : https://github.com/jitsi/jitsi-meet/pull/2068
90
-        api.executeCommand('displayName', '${fake_api.current_user.name}');
91
-        // We can override also avatar.
92
-        api.executeCommand('avatarUrl', 'https://avatars0.githubusercontent.com/u/3671647');
90
+        % if jitsi_meet_room.context and jitsi_meet_room.context.user:
91
+            %if jitsi_meet_room.context.user.name:
92
+            api.executeCommand('displayName', '${jitsi_meet_room.context.user.name}');
93
+            %endif
94
+            // We can override also avatar.
95
+            %if jitsi_meet_room.context.user.avatar_url:
96
+            api.executeCommand('avatarUrl', '${jitsi_meet_room.context.user.avatar_url}');
97
+            %endif
98
+        % endif
93 99
     </script>
94 100
 </div>
95
-
96 101
 <div id="videoconf-invite-modal-dialog" class="modal bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
97 102
   <div class="modal-dialog">
98 103
     <div class="modal-content">

+ 0 - 78
tracim/tracim/tests/library/test_jitsi_meet.py Ver arquivo

@@ -1,78 +0,0 @@
1
-import datetime
2
-
3
-import jwt
4
-from nose.tools import raises
5
-
6
-from tracim.lib.jitsi_meet.jitsi_meet import JitsiTokenConfig
7
-from tracim.lib.jitsi_meet.jitsi_meet import JitsiMeetRoom
8
-from tracim.lib.jitsi_meet.jitsi_meet import NoTokenConfigError
9
-
10
-
11
-class TestJitsiMeetRoom(object):
12
-
13
-    TOKEN_CONFIG = JitsiTokenConfig(
14
-        app_id='test',
15
-        secret='secret',
16
-        alg='HS256',
17
-        duration=60,
18
-    )
19
-    ROOM = 'room'
20
-    DOMAIN = 'tracim'
21
-
22
-    def test_unit__generate_token(self) -> None:
23
-
24
-        jmr = JitsiMeetRoom(
25
-            domain=self.DOMAIN,
26
-            room=self.ROOM,
27
-            token_config=self.TOKEN_CONFIG,
28
-        )
29
-        str_token = jmr.generate_token()
30
-        decoded_token = jwt.decode(str_token,
31
-                                   key=self.TOKEN_CONFIG.secret,
32
-                                   algorithm=self.TOKEN_CONFIG.alg,
33
-                                   audience='*',
34
-                                   issuer=self.TOKEN_CONFIG.app_id,
35
-                                   )
36
-        assert decoded_token.get('room') == self.ROOM
37
-        assert decoded_token.get('iat')
38
-        assert decoded_token.get('iat') == decoded_token.get('nbf')
39
-        assert decoded_token.get('exp') == decoded_token.get('iat') + self.TOKEN_CONFIG.duration  # nopep8
40
-
41
-    @raises(NoTokenConfigError)
42
-    def test_unit__generate_token__no_token(self) -> None:
43
-        jmr = JitsiMeetRoom(
44
-            domain=self.DOMAIN,
45
-            room=self.ROOM,
46
-            token_config=None,
47
-        )
48
-        jmr.generate_token()
49
-
50
-    def test_unit__generate_url_token(self) -> None:
51
-        jmr = JitsiMeetRoom(
52
-            domain=self.DOMAIN,
53
-            room=self.ROOM,
54
-            token_config=self.TOKEN_CONFIG,
55
-        )
56
-
57
-        def generate_token():
58
-            return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNzA3NzksImlzcyI6InRlc3QiLCJhdWQiOiIqIiwibmJmIjoxNTE2MjcwNzE5LCJyb29tIjoicm9vbSIsImlhdCI6MTUxNjI3MDcxOX0.zqFBUcPGjlCfXTjuFP7brqalY8TKlgcg6DUE72KhCx0'  # nopep8
59
-
60
-        jmr.generate_token = generate_token
61
-        url = jmr.generate_url()
62
-        assert url == 'https://{domain}/{room}?jwt={token}'.format(
63
-            domain=self.DOMAIN,
64
-            room=self.ROOM,
65
-            token=generate_token()
66
-        )
67
-
68
-    def test_unit__generate_url_no_token(self) -> None:
69
-        jmr = JitsiMeetRoom(
70
-            domain=self.DOMAIN,
71
-            room=self.ROOM,
72
-            token_config=None,
73
-        )
74
-        url = jmr.generate_url()
75
-        assert url == 'https://{domain}/{room}'.format(
76
-            domain=self.DOMAIN,
77
-            room=self.ROOM,
78
-        )

+ 39 - 0
tracim/tracim/tests/library/test_jitsi_meet_token.py Ver arquivo

@@ -0,0 +1,39 @@
1
+import jwt
2
+from tracim.lib.jitsi_meet.token import JitsiMeetToken
3
+from tracim.lib.jitsi_meet.token import JitsiMeetContext
4
+from tracim.lib.jitsi_meet.token import JitsiMeetUser
5
+
6
+
7
+class TestJitsiMeetToken(object):
8
+
9
+    TOKEN_APP_ID = 'test'
10
+    TOKEN_SECRET = 'secret'
11
+    TOKEN_ALG = 'HS256'
12
+    TOKEN_DURATION = 60
13
+    TOKEN_USER = JitsiMeetUser(name='john', email='john@doe', jitsi_meet_user_id='0')
14
+    TOKEN_CONTEXT = JitsiMeetContext(user=TOKEN_USER, group='test')
15
+    ROOM = 'room'
16
+    DOMAIN = 'tracim'
17
+
18
+    def test_unit__generate_token(self) -> None:
19
+
20
+        token = JitsiMeetToken(
21
+            domain=self.DOMAIN,
22
+            room=self.ROOM,
23
+            app_id=self.TOKEN_APP_ID,
24
+            secret=self.TOKEN_SECRET,
25
+            alg=self.TOKEN_ALG,
26
+            duration=self.TOKEN_DURATION,
27
+            context=self.TOKEN_CONTEXT,
28
+        )
29
+        str_token = token.generate()
30
+        decoded_token = jwt.decode(str_token,
31
+                                   key=self.TOKEN_SECRET,
32
+                                   algorithm=self.TOKEN_ALG,
33
+                                   audience='*',
34
+                                   issuer=self.TOKEN_APP_ID,
35
+                                   )
36
+        assert decoded_token.get('room') == self.ROOM
37
+        assert decoded_token.get('iat')
38
+        assert decoded_token.get('iat') == decoded_token.get('nbf')
39
+        assert decoded_token.get('exp') == decoded_token.get('iat') + self.TOKEN_DURATION  # nopep8