Browse Source

Merge pull request #531 from inkhey/jitsi-meet_integ_demo

Damien Accorsi 6 years ago
parent
commit
f9c53a4a0d
No account linked to committer's email

+ 1 - 0
install/requirements.txt View File

69
 email_reply_parser==0.5.9
69
 email_reply_parser==0.5.9
70
 filelock==2.0.13
70
 filelock==2.0.13
71
 imapclient==1.1.0
71
 imapclient==1.1.0
72
+PyJWT==1.5.3

+ 22 - 0
tracim/development.ini.base View File

35
 # Fallback language if browser and tracim can't find one they agree on.
35
 # Fallback language if browser and tracim can't find one they agree on.
36
 i18n.lang = en
36
 i18n.lang = en
37
 
37
 
38
+tracim_instance.uuid = 79160afe-e324-473a-9aef-7b93f69c1c1b
39
+
38
 cache_dir = %(here)s/data
40
 cache_dir = %(here)s/data
39
 # preview generator cache directory
41
 # preview generator cache directory
40
 preview_cache_dir = /tmp/tracim/preview/
42
 preview_cache_dir = /tmp/tracim/preview/
235
 # it's just an empty file use to prevent concurrent access to imap unseen mail
237
 # it's just an empty file use to prevent concurrent access to imap unseen mail
236
 email.reply.lockfile_path = %(here)s/email_fetcher.lock
238
 email.reply.lockfile_path = %(here)s/email_fetcher.lock
237
 
239
 
240
+## Jitsi_meet_integration
241
+jitsi_meet.activated = False
242
+# Domain/IP where jitsi_meet is located, example: "meet.jit.si", "127.0.0.1"
243
+jitsi_meet.domain = your_jitsi_meet_instance
244
+# Choose between token and anonymous auth, anonymous is default one.
245
+# for token auth config, see : https://github.com/jitsi/lib-jitsi-meet/blob/52eb3decf6542413c739ce2209456fac728a89d5/doc/tokens.md
246
+jitsi_meet.use_token = False
247
+# If token mode is choosen, you need to provide a token generator.
248
+# "local" value mean that tracim himself will create token for jitsi-meet
249
+# Distant token generator are not yet implemented.
250
+jitsi_meet.token_generator = local
251
+# identifier share between jitsi_meet and local token_generator
252
+jitsi_meet.token_generator.local.app_id = tracim
253
+# Secret share between jitsi-meet and local token_generator
254
+jitsi_meet.token_generator.local.secret = my_secret
255
+# Algorithm used to sign JWT token
256
+jitsi_meet.token_generator.local.algorithm = HS256
257
+# Duration of token validity in second. 5 minutes (300) is a good choice.
258
+jitsi_meet.token_generator.local.duration = 300
259
+
238
 ## Radical (CalDav server) configuration
260
 ## Radical (CalDav server) configuration
239
 # radicale.server.host = 0.0.0.0
261
 # radicale.server.host = 0.0.0.0
240
 # radicale.server.port = 5232
262
 # radicale.server.port = 5232

+ 1 - 0
tracim/setup.py View File

24
     'nose',
24
     'nose',
25
     'coverage',
25
     'coverage',
26
     'gearbox',
26
     'gearbox',
27
+    'mock',
27
 ]
28
 ]
28
 
29
 
29
 install_requires = [
30
 install_requires = [

+ 33 - 0
tracim/tracim/config/app_cfg.py View File

217
         """Parse configuration file."""
217
         """Parse configuration file."""
218
         mandatory_msg = \
218
         mandatory_msg = \
219
             'ERROR: {} configuration is mandatory. Set it before continuing.'
219
             'ERROR: {} configuration is mandatory. Set it before continuing.'
220
+        self.TRACIM_INSTANCE_UUID = tg.config.get(
221
+            'tracim_instance.uuid',
222
+        )
220
         self.DEPOT_STORAGE_DIR = tg.config.get(
223
         self.DEPOT_STORAGE_DIR = tg.config.get(
221
             'depot_storage_dir',
224
             'depot_storage_dir',
222
         )
225
         )
436
             # ContentType.Folder -- Folder is skipped
439
             # ContentType.Folder -- Folder is skipped
437
         ]
440
         ]
438
 
441
 
442
+        self.JITSI_MEET_ACTIVATED = asbool(tg.config.get(
443
+            'jitsi_meet.activated',
444
+            False,
445
+        ))
446
+        self.JITSI_MEET_DOMAIN = tg.config.get(
447
+            'jitsi_meet.domain'
448
+        )
449
+        self.JITSI_MEET_USE_TOKEN = asbool(tg.config.get(
450
+            'jitsi_meet.use_token',
451
+            False,
452
+        ))
453
+        self.JITSI_MEET_TOKEN_GENERATOR = tg.config.get(
454
+            'jitsi_meet.token_generator',
455
+            'local'
456
+        )
457
+        self.JITSI_MEET_TOKEN_GENERATOR_LOCAL_APP_ID = tg.config.get(
458
+            'jitsi_meet.token_generator.local.app_id'
459
+        )
460
+        self.JITSI_MEET_TOKEN_GENERATOR_LOCAL_SECRET = tg.config.get(
461
+            'jitsi_meet.token_generator.local.secret'
462
+        )
463
+        self.JITSI_MEET_TOKEN_GENERATOR_LOCAL_ALGORITHM = tg.config.get(
464
+            'jitsi_meet.token_generator.local.algorithm',
465
+            'HS256'
466
+        )
467
+        self.JITSI_MEET_TOKEN_GENERATOR_LOCAL_DURATION = int(tg.config.get(
468
+            'jitsi_meet.token_generator.local.duration',
469
+            60
470
+        ))
471
+
439
         self.RADICALE_SERVER_HOST = tg.config.get(
472
         self.RADICALE_SERVER_HOST = tg.config.get(
440
             'radicale.server.host',
473
             'radicale.server.host',
441
             '127.0.0.1',
474
             '127.0.0.1',

+ 79 - 0
tracim/tracim/controllers/jitsi_meet.py View File

1
+import typing
2
+import tg
3
+from tg import abort
4
+from tg import expose
5
+from tg import tmpl_context
6
+from tg.predicates import not_anonymous
7
+from tracim.lib.predicates import current_user_is_reader
8
+from sqlalchemy.orm.exc import NoResultFound
9
+
10
+from tracim.lib.jitsi_meet.room import JitsiMeetRoom
11
+from tracim.lib.jitsi_meet.token import JitsiMeetUser
12
+from tracim.config.app_cfg import CFG
13
+
14
+from tracim.model.data import User
15
+from tracim.model.serializers import Context, CTX, DictLikeClass
16
+from tracim.controllers import TIMRestController, TIMRestPathContextSetup
17
+
18
+class JitsiMeetController(TIMRestController):
19
+
20
+    allow_only = not_anonymous()
21
+
22
+    def _before(self, *args, **kw) -> None:
23
+        TIMRestPathContextSetup.current_user()
24
+        try:
25
+            TIMRestPathContextSetup.current_workspace()
26
+        except NoResultFound:
27
+            abort(404)
28
+
29
+    @tg.require(current_user_is_reader())
30
+    @expose('tracim.templates.videoconf.jitsi_meet')
31
+    def get(self) -> DictLikeClass:
32
+        """
33
+        Jitsi-Meet Room page
34
+        """
35
+        user = tmpl_context.current_user
36
+        return self._jitsi_room(jitsi_user=user)
37
+
38
+    @tg.require(current_user_is_reader())
39
+    @expose('tracim.templates.videoconf.invite')
40
+    def invite(self) -> DictLikeClass:
41
+        """
42
+        Modal windows : Invitation to Jitsi-Meet room
43
+        """
44
+        # TODO - G.M - 14-02-2017 - Allow to invite not Anonymous user ?
45
+        # Jitsi-Meet allow to set user info through token
46
+        # invite already "named" user should be possible
47
+        return self._jitsi_room()
48
+
49
+    @classmethod
50
+    def _jitsi_room(
51
+            cls,
52
+            jitsi_user: typing.Union[JitsiMeetUser, User, None]=None,
53
+    )-> DictLikeClass:
54
+        """
55
+        Get all infos to generate DictLikeClass usable for JitsiMeetRoom
56
+        Templates.
57
+        :param jitsi_user: User who access to room
58
+        """
59
+        cfg = CFG.get_instance()
60
+        if not cfg.JITSI_MEET_ACTIVATED:
61
+            abort(404)
62
+        user = tmpl_context.current_user
63
+        workspace = tmpl_context.workspace
64
+        current_user_content = Context(CTX.CURRENT_USER).toDict(user)
65
+        fake_api = Context(CTX.CURRENT_USER).toDict(
66
+            {'current_user': current_user_content,
67
+             }
68
+        )
69
+        dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace,
70
+                                                            'workspace')
71
+
72
+        jitsi_meet_room = JitsiMeetRoom(
73
+            issuer=jitsi_user,
74
+            receivers=workspace,
75
+        )
76
+
77
+        return DictLikeClass(fake_api=fake_api,
78
+                             result=dictified_workspace,
79
+                             jitsi_meet_room=jitsi_meet_room)

+ 0 - 1
tracim/tracim/controllers/root.py View File

33
 from tracim.model.serializers import CTX
33
 from tracim.model.serializers import CTX
34
 from tracim.model.serializers import DictLikeClass
34
 from tracim.model.serializers import DictLikeClass
35
 
35
 
36
-
37
 class RootController(StandardController):
36
 class RootController(StandardController):
38
     """
37
     """
39
     The root controller for the tracim application.
38
     The root controller for the tracim application.

+ 4 - 2
tracim/tracim/controllers/workspace.py View File

9
 
9
 
10
 from tracim.controllers import TIMRestController
10
 from tracim.controllers import TIMRestController
11
 from tracim.controllers.content import UserWorkspaceFolderRestController
11
 from tracim.controllers.content import UserWorkspaceFolderRestController
12
+from tracim.controllers.jitsi_meet import JitsiMeetController
12
 
13
 
13
 from tracim.lib.helpers import convert_id_into_instances
14
 from tracim.lib.helpers import convert_id_into_instances
14
 from tracim.lib.content import ContentApi
15
 from tracim.lib.content import ContentApi
29
     allow_only = not_anonymous()
30
     allow_only = not_anonymous()
30
 
31
 
31
     folders = UserWorkspaceFolderRestController()
32
     folders = UserWorkspaceFolderRestController()
33
+    videoconference = JitsiMeetController()
32
 
34
 
33
     @property
35
     @property
34
     def _base_url(self):
36
     def _base_url(self):
99
                 show_archived=show_archived,
101
                 show_archived=show_archived,
100
             )
102
             )
101
         )
103
         )
104
+        videoconf_enabled = CFG.get_instance().JITSI_MEET_ACTIVATED
102
 
105
 
103
         dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace, 'workspace')
106
         dictified_workspace = Context(CTX.WORKSPACE).toDict(workspace, 'workspace')
104
 
107
 
105
-        # INFO - G.M - 15-02-2018 - Deal with url scheme for dav link
106
-        # TODO - G.M - 15-02-2018 - Find better solution to deal with url ?
107
         webdav_url = CFG.get_instance().WSGIDAV_CLIENT_BASE_URL
108
         webdav_url = CFG.get_instance().WSGIDAV_CLIENT_BASE_URL
108
         website_protocol = urlparse(CFG.get_instance().WEBSITE_BASE_URL).scheme
109
         website_protocol = urlparse(CFG.get_instance().WEBSITE_BASE_URL).scheme
109
         dav_protocol = 'dav'
110
         dav_protocol = 'dav'
114
             result=dictified_workspace,
115
             result=dictified_workspace,
115
             fake_api=fake_api,
116
             fake_api=fake_api,
116
             webdav_url=webdav_url,
117
             webdav_url=webdav_url,
118
+            videoconf_enabled=videoconf_enabled,
117
             website_protocol = website_protocol,
119
             website_protocol = website_protocol,
118
             dav_protocol = dav_protocol,
120
             dav_protocol = dav_protocol,
119
             show_deleted=show_deleted,
121
             show_deleted=show_deleted,

+ 0 - 0
tracim/tracim/lib/jitsi_meet/__init__.py View File


+ 155 - 0
tracim/tracim/lib/jitsi_meet/room.py View File

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 JitsiMeetRoom(object):
13
+    def __init__(
14
+            self,
15
+            receivers: Workspace,
16
+            issuer: typing.Union[User, JitsiMeetUser, None]=None,
17
+    ) -> None:
18
+        """
19
+        This class set all to create a JitsiMeetRoom according
20
+        to current config.
21
+        :param issuer: user who initiated Jitsi Meet talk
22
+        if None, default user is created. Can be both Tracim User or
23
+        JitsiMeetUser.
24
+        :param receivers: User or Room who can talk with sender. Now, only
25
+        Workspace are supported.
26
+        """
27
+        self.tracim_cfg = CFG.get_instance()
28
+        self._set_domain()
29
+        self._set_token_params()
30
+        self._set_context(
31
+            workspace=receivers,
32
+            issuer=issuer,
33
+        )
34
+        self.room = self._generate_room_name(receivers)
35
+
36
+    def _set_domain(self) -> None:
37
+        """
38
+        Set domain according to config
39
+        :return:
40
+        """
41
+        self.domain = self.tracim_cfg.JITSI_MEET_DOMAIN
42
+
43
+    def _set_token_params(self) -> None:
44
+        """
45
+        Set params related to token according to config.
46
+        :return: nothing
47
+        """
48
+        self.use_token = self.tracim_cfg.JITSI_MEET_USE_TOKEN
49
+        if self.use_token:
50
+            if self.tracim_cfg.JITSI_MEET_TOKEN_GENERATOR != 'local':
51
+                raise JitsiMeetNoTokenGenerator
52
+
53
+            self.token_app_id = self.tracim_cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_APP_ID  # nopep8
54
+            self.token_secret = self.tracim_cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_SECRET  # nopep8
55
+            self.token_alg = self.tracim_cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_ALGORITHM   # nopep8
56
+            self.token_duration = self.tracim_cfg.JITSI_MEET_TOKEN_GENERATOR_LOCAL_DURATION  # nopep8
57
+
58
+    def _set_context(
59
+            self,
60
+            workspace: Workspace,
61
+            issuer: typing.Union[User, JitsiMeetUser, None],
62
+    ) -> None:
63
+        """
64
+        Set context of JWT token for Jitsi Meet
65
+        :param issuer: user who initiated Jitsi Meet talk
66
+        if None, default user is created. Can be both Tracim User or
67
+        JitsiMeetUser.
68
+        :param workspace: User or Room who can talk with sender. Now, only
69
+        Workspace are supported.
70
+        :return: nothing.
71
+        """
72
+
73
+        # INFO - G.M - 13-02-2018 - Convert all issuers values as JitsiMeetUser
74
+        if isinstance(issuer, JitsiMeetUser):
75
+            user = issuer
76
+        elif isinstance(issuer, User):
77
+            user = JitsiMeetUser(
78
+                name=issuer.display_name,
79
+                avatar_url=None,
80
+                jitsi_meet_user_id=issuer.display_name,
81
+            )
82
+        else:
83
+            user = JitsiMeetUser(
84
+                # INFO - G.M - 13-02-2018 - create unique id for anonymous user
85
+                jitsi_meet_user_id=str(uuid.uuid4()),
86
+            )
87
+
88
+        # INFO - G.M - 13-02-2018 - Associate
89
+        group = workspace.label
90
+
91
+        self.context = JitsiMeetContext(
92
+            user=user,
93
+            group=group,
94
+        )
95
+
96
+    def _generate_room_name(self, workspace: Workspace) -> str:
97
+        """
98
+        Generate Jitsi-Meet room name related to workspace
99
+        that should be unique, always the same for same workspace in same Tracim
100
+        instance but should also no contains any special characters
101
+        :param workspace: Tracim Workspace
102
+        :return: room name as str.
103
+        """
104
+        room = "{uuid}{workspace_id}{workspace_label}".format(
105
+            uuid=self.tracim_cfg.TRACIM_INSTANCE_UUID,
106
+            workspace_id=workspace.workspace_id,
107
+            workspace_label=workspace.label)
108
+
109
+        # Jitsi-Meet doesn't like specials_characters
110
+        return str_as_alpha_num_str(room)
111
+
112
+    def generate_jwt_token(self) -> str:
113
+        """
114
+        Generate Jitsi-Meet related JWT token
115
+        :return: JWT token as str
116
+        """
117
+        if not self.use_token:
118
+            raise JitsiMeetTokenNotActivated
119
+
120
+        token = JitsiMeetToken(
121
+            domain=self.domain,
122
+            room=self.room,
123
+            app_id=self.token_app_id,
124
+            secret=self.token_secret,
125
+            alg=self.token_alg,
126
+            duration=self.token_duration,
127
+            context=self.context,
128
+        )
129
+        return token.generate()
130
+
131
+    def generate_url(self, token=None) -> str:
132
+        """
133
+        Generate Jitsi-Meet url with or without token
134
+        examples :
135
+         - https://myjitsiinstance/myroom
136
+         - https://mysecurejitsiinstance/myroom?jwt=[very_long_jwt_token]
137
+        :return: url as string
138
+        """
139
+        if token:
140
+            url = "{}/{}?jwt={}".format(self.domain,
141
+                                        self.room,
142
+                                        token)
143
+        else:
144
+            url = "{}/{}".format(self.domain,
145
+                                 self.room,
146
+                                 )
147
+        return "https://{}".format(url)
148
+
149
+
150
+class JitsiMeetNoTokenGenerator(Exception):
151
+    pass
152
+
153
+
154
+class JitsiMeetTokenNotActivated(Exception):
155
+    pass

+ 133 - 0
tracim/tracim/lib/jitsi_meet/token.py View File

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

+ 13 - 0
tracim/tracim/lib/utils.py View File

17
 from tg.util import lazify
17
 from tg.util import lazify
18
 from redis import Redis
18
 from redis import Redis
19
 from rq import Queue
19
 from rq import Queue
20
+from unidecode import unidecode
20
 
21
 
21
 from wsgidav.middleware import BaseMiddleware
22
 from wsgidav.middleware import BaseMiddleware
22
 from tracim.lib.base import logger
23
 from tracim.lib.base import logger
142
     return bool(string)
143
     return bool(string)
143
 
144
 
144
 
145
 
146
+def str_as_alpha_num_str(unicode_string: str) -> str:
147
+    """
148
+    convert unicode string to alpha_num-only string.
149
+    convert also accented character to ascii equivalent.
150
+    :param unicode_string:
151
+    :return:
152
+    """
153
+    ascii_string = unidecode(unicode_string)
154
+    alpha_num_string = ''.join(e for e in ascii_string if e.isalnum())
155
+    return alpha_num_string
156
+
157
+
145
 class LazyString(BaseLazyString):
158
 class LazyString(BaseLazyString):
146
     pass
159
     pass
147
 
160
 

+ 0 - 0
tracim/tracim/templates/videoconf/__init__.py View File


+ 22 - 0
tracim/tracim/templates/videoconf/invite.mak View File

1
+<%namespace name="TIM" file="tracim.templates.pod"/>
2
+<%namespace name="ICON" file="tracim.templates.widgets.icon"/>
3
+<%def name="title()">${_('Invite someone to video-conference')}</%def>
4
+
5
+<%def name="content(jitsi_meet_room)">
6
+    <div class="modal-header">
7
+        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
8
+        <h4 class="modal-title">${ICON.FA('fa-share')}  ${self.title()}</h4>
9
+    </div>
10
+    <div class="modal-body">
11
+        <p>
12
+        ${_('To invite someone from outside of tracim into this conference, share this link. This link is available for 5 minutes.')}
13
+        </p>
14
+        <textarea readonly=readonly wrap="off" style="width: 100%;">${jitsi_meet_room.generate_url(jitsi_meet_room.generate_jwt_token())}</textarea>
15
+    </div>
16
+    <div class="modal-footer">
17
+        <button type="button" class="btn btn-default" data-dismiss="modal">${_('Close')}</button>
18
+    </div>
19
+        </form>
20
+</%def>
21
+${self.content(jitsi_meet_room)}
22
+

+ 80 - 0
tracim/tracim/templates/videoconf/jitsi_meet.mak View File

1
+<%inherit file="local:templates.master_authenticated_left_treeview_right_toolbar"/>
2
+<%namespace name="TIM" file="tracim.templates.pod"/>
3
+<%namespace name="ROW" file="tracim.templates.widgets.row"/>
4
+<%namespace name="TABLE_ROW" file="tracim.templates.widgets.table_row"/>
5
+<%namespace name="LEFT_MENU" file="tracim.templates.widgets.left_menu"/>
6
+<%namespace name="P" file="tracim.templates.widgets.paragraph"/>
7
+<%namespace name="TOOLBAR" file="tracim.templates.videoconf.toolbar"/>
8
+
9
+<%def name="title()">
10
+    ${_('Video conference: {workspace}').format(workspace=result.workspace.label)}
11
+</%def>
12
+
13
+<%def name="TITLE_ROW()">
14
+    <div class="content__title">
15
+    ${ROW.TITLE_ROW(
16
+    _('Video conference: {workspace}').format(workspace=result.workspace.label),
17
+    'fa-video-camera', 'content__title__subtitle-home-hidden-xs',
18
+    't-user-color', _('Welcome to video conference of {workspace}, {username}.').format(workspace=
19
+    result.workspace.label,
20
+    username=fake_api.current_user.name))}
21
+    </div>
22
+</%def>
23
+
24
+<%def name="SIDEBAR_RIGHT_CONTENT()">
25
+    ${TOOLBAR.JITSIMEETROOM(fake_api.current_user, result.workspace, jitsi_meet_room)}
26
+</%def>
27
+
28
+
29
+<%def name="SIDEBAR_LEFT_CONTENT()">
30
+    ${LEFT_MENU.TREEVIEW('sidebar-left-menu', 'workspace_{}__'.format(result.workspace.id))}
31
+</%def>
32
+
33
+<%def name="REQUIRED_DIALOGS()">
34
+</%def>
35
+
36
+<div class="content__home">
37
+    <div id="jitsi">
38
+    </div>
39
+    <script src="https://${jitsi_meet_room.domain}/libs/external_api.min.js"></script>
40
+    <script>
41
+        let domain = '${jitsi_meet_room.domain}';
42
+        let options = {
43
+	    // INFO - G.M - 14-02-2018 jitsi-meet external API
44
+        // support only one way to auto-auth due to security concern : token,
45
+	    // which is anonymous BOSH auth with specific url
46
+        // for another way to deal with auto-auth :
47
+        // see this rejected PR : https://github.com/jitsi/jitsi-meet/pull/2109
48
+            %if jitsi_meet_room.use_token:
49
+                jwt: '${jitsi_meet_room.generate_jwt_token()}',
50
+            %endif
51
+            roomName : '${jitsi_meet_room.room}',
52
+            parentNode: document.querySelector('#jitsi'),
53
+            // TODO - G.M - 14-02-2018 - Find a solution to height trouble.
54
+            // height should be related to page size
55
+            height: 700,
56
+            // TODO - G.M - 14-02-2018 Check 'no_SSL' params
57
+        };
58
+        let api = new JitsiMeetExternalAPI(domain, options);
59
+        // INFO - G.M - 14-02-2018 - About Display Name
60
+        // Display name in jitsi-meet use XEP-0172 for MUC, which is discouraged,
61
+        // when others clients use resource part of the Jabber id to do it.
62
+        // That's why displayName compat with others XMPP client is not optimal.
63
+        // check this : https://github.com/jitsi/jitsi-meet/pull/2068
64
+        % if jitsi_meet_room.context and jitsi_meet_room.context.user:
65
+            %if jitsi_meet_room.context.user.name:
66
+            api.executeCommand('displayName', '${jitsi_meet_room.context.user.name}');
67
+            %endif
68
+            // We can override also avatar.
69
+            %if jitsi_meet_room.context.user.avatar_url:
70
+            api.executeCommand('avatarUrl', '${jitsi_meet_room.context.user.avatar_url}');
71
+            %endif
72
+        % endif
73
+    </script>
74
+</div>
75
+<div id="videoconf-invite-modal-dialog" class="modal bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true">
76
+  <div class="modal-dialog">
77
+    <div class="modal-content">
78
+    </div>
79
+  </div>
80
+</div>

+ 17 - 0
tracim/tracim/templates/videoconf/toolbar.mak View File

1
+<%namespace name="ICON" file="tracim.templates.widgets.icon"/>
2
+
3
+<%namespace name="TIM" file="tracim.templates.pod"/>
4
+
5
+
6
+<%def name="JITSIMEETROOM(current_user, workspace,jitsi_meet_room)">
7
+    ## SIDEBAR RIGHT
8
+    <div>
9
+        <div class="btn-group btn-group-vertical">
10
+           ## INFO - G.M - 09-01-2018 - Share a link disabled, because
11
+           ## link doesn't refresh.
12
+           ## <a title="${_('Invite by sharing a link')}" class="btn btn-default" data-toggle="modal" data-target="#videoconf-invite-modal-dialog" data-remote="${tg.url('/workspaces/{}/videoconference/invite'.format(result.workspace.id))}" >${ICON.FA('fa-share fa-fw t-less-visible')} ${_('Invite')}</a>
13
+        </div>
14
+        <p></p>
15
+    </div> <!-- # End of side bar right -->
16
+    ## SIDEBAR RIGHT [END]
17
+</%def>

+ 1 - 1
tracim/tracim/templates/workspace/getone.mak View File

20
 </%def>
20
 </%def>
21
 
21
 
22
 <%def name="SIDEBAR_RIGHT_CONTENT()">
22
 <%def name="SIDEBAR_RIGHT_CONTENT()">
23
-##    {TOOLBAR.SECURED_FOLDER(fake_api.current_user, result.folder.workspace, result.folder)}
23
+ ${TOOLBAR.WORKSPACE_USER(fake_api.current_user, result.workspace, videoconf_enabled)}
24
 </%def>
24
 </%def>
25
 
25
 
26
 <%def name="REQUIRED_DIALOGS()">
26
 <%def name="REQUIRED_DIALOGS()">

+ 15 - 0
tracim/tracim/templates/workspace/toolbar.mak View File

19
                 <a title="${_('Delete current workspace')}" class="btn btn-default" href="${tg.url('/admin/workspaces/{}/delete'.format(result.workspace.id))}">${ICON.FA('fa-trash fa-fw t-less-visible')} ${_('Delete')}</a>
19
                 <a title="${_('Delete current workspace')}" class="btn btn-default" href="${tg.url('/admin/workspaces/{}/delete'.format(result.workspace.id))}">${ICON.FA('fa-trash fa-fw t-less-visible')} ${_('Delete')}</a>
20
             </div>
20
             </div>
21
         % endif
21
         % endif
22
+            ${JITSI_MEET_BUTTON(current_user, workspace)}
22
     </div> <!-- # End of side bar right -->
23
     </div> <!-- # End of side bar right -->
23
     ## SIDEBAR RIGHT [END]
24
     ## SIDEBAR RIGHT [END]
24
 </%def>
25
 </%def>
26
+
27
+<%def name="WORKSPACE_USER(current_user, workspace, videoconf_enabled)">
28
+    <div>
29
+        % if videoconf_enabled:
30
+        ${JITSI_MEET_BUTTON(current_user, workspace)}
31
+       % endif
32
+    </div> <!-- # End of side bar right -->
33
+</%def>
34
+
35
+<%def name="JITSI_MEET_BUTTON(current_user, workspace)">
36
+    <div class="btn-group btn-group-vertical">
37
+        <a title="${_('Video conference')}" class="btn btn-default" href="${tg.url('/workspaces/{}/videoconference'.format(result.workspace.id))}">${ICON.FA('fa-video-camera fa-fw t-less-visible')} ${_('Video Conference')}</a>
38
+    </div>
39
+</%def>

+ 128 - 0
tracim/tracim/tests/library/test_jitsi_meet_room.py View File

1
+from tracim.lib.jitsi_meet.room import JitsiMeetRoom
2
+from tracim.lib.jitsi_meet.room import JitsiMeetTokenNotActivated
3
+from tracim.lib.jitsi_meet.room import JitsiMeetNoTokenGenerator
4
+from tracim.lib.jitsi_meet.token import JitsiMeetUser
5
+from tracim.model.data import User
6
+from tracim.model.data import Workspace
7
+from nose.tools import raises
8
+from mock import patch, Mock
9
+
10
+
11
+class TestJitsiMeetRoom(object):
12
+
13
+    ROOM = 'room'
14
+    DOMAIN = 'tracim'
15
+    TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNzA3NzksImlzcyI6InRlc3QiLCJhdWQiOiIqIiwibmJmIjoxNTE2MjcwNzE5LCJyb29tIjoicm9vbSIsImlhdCI6MTUxNjI3MDcxOX0.zqFBUcPGjlCfXTjuFP7brqalY8TKlgcg6DUE72KhCx0'  # nopep8
16
+
17
+    def test_unit__generate_url_token_no_token(self) -> None:
18
+        with patch.object(JitsiMeetRoom,
19
+                          "__init__",
20
+                          lambda item, receivers: None):
21
+            jmr = JitsiMeetRoom(
22
+                receivers=Workspace()
23
+            )
24
+            jmr.domain = TestJitsiMeetRoom.DOMAIN
25
+            jmr.room = TestJitsiMeetRoom.ROOM
26
+            url = jmr.generate_url()
27
+            assert url == 'https://{domain}/{room}'.format(
28
+                domain=self.DOMAIN,
29
+                room=self.ROOM,
30
+                token=self.TOKEN
31
+            )
32
+
33
+    def test_unit__generate_url_token(self) -> None:
34
+        with patch.object(JitsiMeetRoom,
35
+                          "__init__",
36
+                          lambda item, receivers: None):
37
+            jmr = JitsiMeetRoom(
38
+                receivers=Workspace()
39
+            )
40
+            jmr.domain = TestJitsiMeetRoom.DOMAIN
41
+            jmr.room = TestJitsiMeetRoom.ROOM
42
+            url = jmr.generate_url(token=self.TOKEN)
43
+            assert url == 'https://{domain}/{room}?jwt={token}'.format(
44
+                domain=self.DOMAIN,
45
+                room=self.ROOM,
46
+                token=self.TOKEN
47
+            )
48
+
49
+    @raises(JitsiMeetNoTokenGenerator)
50
+    def test_unit__set_token_params_no_token_generator(self) -> None:
51
+        with patch.object(JitsiMeetRoom,
52
+                          "__init__",
53
+                          lambda item, receivers: None):
54
+            jmr = JitsiMeetRoom(
55
+                receivers=Workspace()
56
+            )
57
+            jmr.tracim_cfg = Mock()
58
+            jmr.tracim_cfg.JITSI_MEET_TOKEN_GENERATOR = None
59
+            jmr._set_token_params()
60
+
61
+    @raises(JitsiMeetTokenNotActivated)
62
+    def test_unit__generate_token_not_activated(self) -> None:
63
+        with patch.object(JitsiMeetRoom,
64
+                          "__init__",
65
+                          lambda item, receivers: None):
66
+            jmr = JitsiMeetRoom(
67
+                receivers=Workspace()
68
+            )
69
+            jmr.use_token = False
70
+            jmr.generate_jwt_token()
71
+
72
+    def test_unit__set_context_return_always_jitsi_meet_user(self):
73
+        with patch.object(JitsiMeetRoom,
74
+                          "__init__",
75
+                          lambda item, receivers: None):
76
+            jmr = JitsiMeetRoom(
77
+                receivers=Workspace()
78
+            )
79
+            jmr._set_context(issuer=None, workspace=Workspace())
80
+            assert jmr.context
81
+            assert hasattr(jmr.context, 'user')
82
+            assert isinstance(jmr.context.user, JitsiMeetUser)
83
+
84
+            jmr2 = JitsiMeetRoom(
85
+                receivers=Workspace()
86
+            )
87
+            jmr2._set_context(issuer=JitsiMeetUser(jitsi_meet_user_id='user'),
88
+                              workspace=Workspace())
89
+            assert jmr2.context
90
+            assert hasattr(jmr2.context, 'user')
91
+            assert isinstance(jmr2.context.user, JitsiMeetUser)
92
+
93
+            jmr3 = JitsiMeetRoom(
94
+                receivers=Workspace()
95
+            )
96
+            jmr3._set_context(issuer=User(), workspace=Workspace())
97
+            assert jmr3.context
98
+            assert hasattr(jmr3.context, 'user')
99
+            assert isinstance(jmr3.context.user, JitsiMeetUser)
100
+
101
+    def test_unit__generate_room_name(self):
102
+        with patch.object(JitsiMeetRoom,
103
+                          "__init__",
104
+                          lambda item, receivers: None):
105
+            jmr = JitsiMeetRoom(
106
+                receivers=Workspace()
107
+            )
108
+            mock = Mock()
109
+            jmr.tracim_cfg = Mock()
110
+            jmr.tracim_cfg.TRACIM_INSTANCE_UUID = 'myuuid'
111
+            mock.workspace_id = 1
112
+            mock.label = 'myroom'
113
+            assert jmr._generate_room_name(mock) == 'myuuid1myroom'
114
+
115
+    def test_unit__generate_room_name_no_special_character(self):
116
+        with patch.object(JitsiMeetRoom,
117
+                          "__init__",
118
+                          lambda item, receivers: None):
119
+            jmr = JitsiMeetRoom(
120
+                receivers=Workspace()
121
+            )
122
+            mock = Mock()
123
+            jmr.tracim_cfg = Mock()
124
+            # TODO - G.M - 14-02-2018 - Be exhaustive about special char ?
125
+            jmr.tracim_cfg.TRACIM_INSTANCE_UUID = '*%hél\/ <>."{}|+&-@"~]=lo{à!ll;:'  # nopep8
126
+            mock.workspace_id = 1
127
+            mock.label = 'myroom'
128
+            assert jmr._generate_room_name(mock) == 'helloall1myroom'

+ 185 - 0
tracim/tracim/tests/library/test_jitsi_meet_token.py View File

1
+import jwt
2
+import datetime
3
+import calendar
4
+
5
+from tracim.lib.jitsi_meet.token import JitsiMeetToken
6
+from tracim.lib.jitsi_meet.token import JitsiMeetContext
7
+from tracim.lib.jitsi_meet.token import JitsiMeetUser
8
+
9
+
10
+class TestJitsiMeetToken(object):
11
+
12
+    TOKEN_APP_ID = 'test'
13
+    TOKEN_SECRET = 'secret'
14
+    TOKEN_ALG = 'HS256'
15
+    TOKEN_DURATION = 60
16
+    TOKEN_USER = JitsiMeetUser(name='john',
17
+                               email='john@doe',
18
+                               jitsi_meet_user_id='0')
19
+    TOKEN_CONTEXT = JitsiMeetContext(user=TOKEN_USER, group='test')
20
+    ROOM = 'room'
21
+    DOMAIN = 'tracim'
22
+    ISSUE_DATE = datetime.datetime.utcfromtimestamp(0)
23
+
24
+    def test_unit__generate_token_check_value(self) -> None:
25
+        """
26
+        Test all value without checking jwt validity (timestamp 0 as issue date)
27
+        :return: Nothing
28
+        """
29
+
30
+        token = JitsiMeetToken(
31
+            domain=self.DOMAIN,
32
+            room=self.ROOM,
33
+            app_id=self.TOKEN_APP_ID,
34
+            secret=self.TOKEN_SECRET,
35
+            alg=self.TOKEN_ALG,
36
+            duration=self.TOKEN_DURATION,
37
+            context=self.TOKEN_CONTEXT,
38
+        )
39
+
40
+        str_token = token.generate(issue_date=self.ISSUE_DATE)
41
+        decoded_token = jwt.decode(str_token,
42
+                                   key=self.TOKEN_SECRET,
43
+                                   algorithm=self.TOKEN_ALG,
44
+                                   audience='*',
45
+                                   issuer=self.TOKEN_APP_ID,
46
+                                   verify=False
47
+                                   )
48
+
49
+        assert decoded_token.get('iss') == self.TOKEN_APP_ID
50
+        assert decoded_token.get('room') == self.ROOM
51
+        assert decoded_token.get('aud') == "*"
52
+        assert decoded_token.get('context')
53
+        assert decoded_token['context'].get('user')
54
+        assert decoded_token['context']['user'].get('name') == 'john'
55
+        assert decoded_token['context']['user'].get('email') == 'john@doe'
56
+        assert decoded_token['context']['user'].get('id') == '0'
57
+        assert decoded_token['context'].get('group') == 'test'
58
+        assert decoded_token.get('iat') == calendar.timegm(
59
+            self.ISSUE_DATE.utctimetuple()
60
+        )
61
+        assert decoded_token.get('iat') == decoded_token.get('nbf')
62
+        assert decoded_token.get('exp') == decoded_token.get('iat') + self.TOKEN_DURATION  # nopep8
63
+
64
+    def test_unit__generate_token_check_jwt(self) -> None:
65
+        """
66
+        Test only JWT validity and time params with default issue date
67
+        :return: Nothing
68
+        """
69
+        token = JitsiMeetToken(
70
+            domain=self.DOMAIN,
71
+            room=self.ROOM,
72
+            app_id=self.TOKEN_APP_ID,
73
+            secret=self.TOKEN_SECRET,
74
+            alg=self.TOKEN_ALG,
75
+            duration=self.TOKEN_DURATION,
76
+            context=self.TOKEN_CONTEXT,
77
+        )
78
+
79
+        str_token = token.generate()
80
+        decoded_token = jwt.decode(str_token,
81
+                                   key=self.TOKEN_SECRET,
82
+                                   algorithm=self.TOKEN_ALG,
83
+                                   audience='*',
84
+                                   issuer=self.TOKEN_APP_ID,
85
+                                   )
86
+
87
+        assert decoded_token.get('iat') == decoded_token.get('nbf')
88
+        assert decoded_token.get('exp') == decoded_token.get('iat') + self.TOKEN_DURATION  # nopep8
89
+
90
+
91
+class TestJitsiMeetUser(object):
92
+
93
+    def test_unit__as_dict(self):
94
+
95
+        user1 = JitsiMeetUser(jitsi_meet_user_id='1')
96
+        assert user1.as_dict() == {'id': '1'}
97
+
98
+        user2 = JitsiMeetUser(jitsi_meet_user_id='2', name='john')
99
+        assert user2.as_dict() == {'id': '2', 'name': 'john'}
100
+
101
+        user3 = JitsiMeetUser(jitsi_meet_user_id='3', name='john', email='a@b')
102
+        assert user3.as_dict() == {'id': '3', 'name': 'john', 'email': 'a@b'}
103
+
104
+        user4 = JitsiMeetUser(
105
+            jitsi_meet_user_id='4',
106
+            name='john',
107
+            email='a@b',
108
+            avatar_url='http://mysuperavatar/avatar.png'
109
+        )
110
+        assert user4.as_dict() == {
111
+            'id': '4',
112
+            'name': 'john',
113
+            'email': 'a@b',
114
+            'avatar': 'http://mysuperavatar/avatar.png',
115
+        }
116
+
117
+        user5 = JitsiMeetUser(
118
+            jitsi_meet_user_id='5',
119
+            name='john',
120
+            avatar_url='http://mysuperavatar/avatar.png'
121
+        )
122
+        assert user5.as_dict() == {
123
+            'id': '5',
124
+            'name': 'john',
125
+            'avatar': 'http://mysuperavatar/avatar.png',
126
+        }
127
+
128
+        user6 = JitsiMeetUser(
129
+            jitsi_meet_user_id='6',
130
+            email='a@b',
131
+            avatar_url='http://mysuperavatar/avatar.png'
132
+        )
133
+        assert user6.as_dict() == {
134
+            'id': '6',
135
+            'email': 'a@b',
136
+            'avatar': 'http://mysuperavatar/avatar.png',
137
+        }
138
+
139
+        user7 = JitsiMeetUser(
140
+            jitsi_meet_user_id='7',
141
+            email='a@b',
142
+        )
143
+        assert user7.as_dict() == {
144
+            'id': '7',
145
+            'email': 'a@b',
146
+        }
147
+
148
+        user8 = JitsiMeetUser(
149
+            jitsi_meet_user_id='8',
150
+            avatar_url='http://mysuperavatar/avatar.png'
151
+        )
152
+        assert user8.as_dict() == {
153
+            'id': '8',
154
+            'avatar': 'http://mysuperavatar/avatar.png',
155
+        }
156
+
157
+
158
+class TestJitsiMeetContext(object):
159
+
160
+    def test_unit__as_dict(self):
161
+
162
+        context = JitsiMeetContext()
163
+        assert context.as_dict() == {
164
+            'group': 'default'
165
+        }
166
+
167
+        context = JitsiMeetContext(group='group1')
168
+        assert context.as_dict() == {
169
+            'group': 'group1'
170
+        }
171
+
172
+        user = JitsiMeetUser(jitsi_meet_user_id='user')
173
+        context = JitsiMeetContext(group='group1', user=user)
174
+        assert context.as_dict() == {
175
+            'group': 'group1',
176
+            'user': user.as_dict()
177
+        }
178
+
179
+        callee = JitsiMeetUser(jitsi_meet_user_id='callee')
180
+        context = JitsiMeetContext(group='group1', user=user, callee=callee)
181
+        assert context.as_dict() == {
182
+            'group': 'group1',
183
+            'user': user.as_dict(),
184
+            'callee': callee.as_dict()
185
+        }