Преглед на файлове

Merge pull request #531 from inkhey/jitsi-meet_integ_demo

Damien Accorsi преди 6 години
родител
ревизия
f9c53a4a0d
No account linked to committer's email

+ 1 - 0
install/requirements.txt Целия файл

@@ -69,3 +69,4 @@ markdown==2.6.9
69 69
 email_reply_parser==0.5.9
70 70
 filelock==2.0.13
71 71
 imapclient==1.1.0
72
+PyJWT==1.5.3

+ 22 - 0
tracim/development.ini.base Целия файл

@@ -35,6 +35,8 @@ full_stack = true
35 35
 # Fallback language if browser and tracim can't find one they agree on.
36 36
 i18n.lang = en
37 37
 
38
+tracim_instance.uuid = 79160afe-e324-473a-9aef-7b93f69c1c1b
39
+
38 40
 cache_dir = %(here)s/data
39 41
 # preview generator cache directory
40 42
 preview_cache_dir = /tmp/tracim/preview/
@@ -235,6 +237,26 @@ email.reply.use_txt_parsing = true
235 237
 # it's just an empty file use to prevent concurrent access to imap unseen mail
236 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 260
 ## Radical (CalDav server) configuration
239 261
 # radicale.server.host = 0.0.0.0
240 262
 # radicale.server.port = 5232

+ 1 - 0
tracim/setup.py Целия файл

@@ -24,6 +24,7 @@ testpkgs = [
24 24
     'nose',
25 25
     'coverage',
26 26
     'gearbox',
27
+    'mock',
27 28
 ]
28 29
 
29 30
 install_requires = [

+ 33 - 0
tracim/tracim/config/app_cfg.py Целия файл

@@ -217,6 +217,9 @@ class CFG(object):
217 217
         """Parse configuration file."""
218 218
         mandatory_msg = \
219 219
             'ERROR: {} configuration is mandatory. Set it before continuing.'
220
+        self.TRACIM_INSTANCE_UUID = tg.config.get(
221
+            'tracim_instance.uuid',
222
+        )
220 223
         self.DEPOT_STORAGE_DIR = tg.config.get(
221 224
             'depot_storage_dir',
222 225
         )
@@ -436,6 +439,36 @@ class CFG(object):
436 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 472
         self.RADICALE_SERVER_HOST = tg.config.get(
440 473
             'radicale.server.host',
441 474
             '127.0.0.1',

+ 79 - 0
tracim/tracim/controllers/jitsi_meet.py Целия файл

@@ -0,0 +1,79 @@
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 Целия файл

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

+ 4 - 2
tracim/tracim/controllers/workspace.py Целия файл

@@ -9,6 +9,7 @@ from tracim.config.app_cfg import CFG
9 9
 
10 10
 from tracim.controllers import TIMRestController
11 11
 from tracim.controllers.content import UserWorkspaceFolderRestController
12
+from tracim.controllers.jitsi_meet import JitsiMeetController
12 13
 
13 14
 from tracim.lib.helpers import convert_id_into_instances
14 15
 from tracim.lib.content import ContentApi
@@ -29,6 +30,7 @@ class UserWorkspaceRestController(TIMRestController):
29 30
     allow_only = not_anonymous()
30 31
 
31 32
     folders = UserWorkspaceFolderRestController()
33
+    videoconference = JitsiMeetController()
32 34
 
33 35
     @property
34 36
     def _base_url(self):
@@ -99,11 +101,10 @@ class UserWorkspaceRestController(TIMRestController):
99 101
                 show_archived=show_archived,
100 102
             )
101 103
         )
104
+        videoconf_enabled = CFG.get_instance().JITSI_MEET_ACTIVATED
102 105
 
103 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 108
         webdav_url = CFG.get_instance().WSGIDAV_CLIENT_BASE_URL
108 109
         website_protocol = urlparse(CFG.get_instance().WEBSITE_BASE_URL).scheme
109 110
         dav_protocol = 'dav'
@@ -114,6 +115,7 @@ class UserWorkspaceRestController(TIMRestController):
114 115
             result=dictified_workspace,
115 116
             fake_api=fake_api,
116 117
             webdav_url=webdav_url,
118
+            videoconf_enabled=videoconf_enabled,
117 119
             website_protocol = website_protocol,
118 120
             dav_protocol = dav_protocol,
119 121
             show_deleted=show_deleted,

+ 0 - 0
tracim/tracim/lib/jitsi_meet/__init__.py Целия файл


+ 155 - 0
tracim/tracim/lib/jitsi_meet/room.py Целия файл

@@ -0,0 +1,155 @@
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 Целия файл

@@ -0,0 +1,133 @@
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 Целия файл

@@ -17,6 +17,7 @@ from tg.util import LazyString as BaseLazyString
17 17
 from tg.util import lazify
18 18
 from redis import Redis
19 19
 from rq import Queue
20
+from unidecode import unidecode
20 21
 
21 22
 from wsgidav.middleware import BaseMiddleware
22 23
 from tracim.lib.base import logger
@@ -142,6 +143,18 @@ def str_as_bool(string: str) -> bool:
142 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 158
 class LazyString(BaseLazyString):
146 159
     pass
147 160
 

+ 0 - 0
tracim/tracim/templates/videoconf/__init__.py Целия файл


+ 22 - 0
tracim/tracim/templates/videoconf/invite.mak Целия файл

@@ -0,0 +1,22 @@
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 Целия файл

@@ -0,0 +1,80 @@
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 Целия файл

@@ -0,0 +1,17 @@
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 Целия файл

@@ -20,7 +20,7 @@
20 20
 </%def>
21 21
 
22 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 24
 </%def>
25 25
 
26 26
 <%def name="REQUIRED_DIALOGS()">

+ 15 - 0
tracim/tracim/templates/workspace/toolbar.mak Целия файл

@@ -19,6 +19,21 @@
19 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 20
             </div>
21 21
         % endif
22
+            ${JITSI_MEET_BUTTON(current_user, workspace)}
22 23
     </div> <!-- # End of side bar right -->
23 24
     ## SIDEBAR RIGHT [END]
24 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 Целия файл

@@ -0,0 +1,128 @@
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 Целия файл

@@ -0,0 +1,185 @@
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
+        }