@ -23,8 +23,13 @@ import math
import random
import string
from collections import OrderedDict
from pathlib import Path # watcha+ op488
# watcha+
from pathlib import Path
from requests import get , post , delete , auth , HTTPError
from requests . auth import HTTPBasicAuth
# +watcha
from typing import TYPE_CHECKING , Any , Awaitable , Dict , List , Optional , Tuple
from urllib . parse import parse_qs , urlparse # watcha+ op486
from synapse . api . constants import (
EventTypes ,
@ -52,6 +57,7 @@ from synapse.types import (
UserID ,
create_requester ,
)
from synapse . types import get_localpart_from_id # watcha+ op544
from synapse . util import stringutils
from synapse . util . async_helpers import Linearizer
from synapse . util . caches . response_cache import ResponseCache
@ -1411,6 +1417,373 @@ class WatchaRoomHandler(BaseHandler):
self . store = hs . get_datastore ( )
self . event_creation_handler = hs . get_event_creation_handler ( )
# Nextcloud Integration config :
self . keycloak_server = hs . config . keycloak_serveur
self . keycloak_realm = hs . config . keycloak_realm
self . nextcloud_shared_secret = hs . config . nextcloud_shared_secret
self . nextcloud_server = hs . config . nextcloud_server
self . service_account_name = hs . config . service_account_name
self . service_account_password = hs . config . service_account_password
self . keycloak_access_token = " "
async def delete_room_mapping_with_nextcloud_directory ( self , room_id , requester_id ) :
""" Delete a mapping between a room and an Nextcloud folder.
Args :
room_id : the id of the room .
requester_id : the user_id of the requester .
"""
directory_path = await self . store . get_nextcloud_directory_path_from_roomID (
room_id
)
try :
self . keycloak_access_token = await self . get_keycloak_access_token ( )
except HTTPError :
raise SynapseError (
400 ,
" Unable to retrieve the Keycloak access token of realm {} " . format (
self . keycloak_realm
) ,
)
try :
nextcloud_username = await self . get_nextcloud_username (
get_localpart_from_id ( requester_id )
)
except HTTPError :
raise SynapseError (
400 ,
" Unable to retrieve the corresponding Nextcloud username of user {} . " . format (
requester_id
) ,
)
try :
all_shares = await self . get_sharing_of_nextcloud_directory (
nextcloud_username , directory_path
)
except HTTPError as e :
if e . response . status_code == 404 :
raise SynapseError (
400 ,
" The user {} doesn ' t have the access right to the folder {} . " . format (
requester_id , directory_path
) ,
Codes . NEXTCLOUD_FOLDER_ACCESS_FORBIDDEN ,
)
raise SynapseError (
400 ,
" Unable to get shares on the folder {} . " . format (
directory_path
) ,
)
group_share_id = " "
for share in all_shares :
if share [ " share_with " ] == room_id :
group_share_id = share [ " id " ]
break
if not group_share_id :
raise SynapseError (
400 ,
" Unable to retrieve share id between Watcha room {} and Nextcloud directory {} " . format (
room_id , directory_path
) ,
)
try :
await self . delete_existing_nextcloud_share ( nextcloud_username , group_share_id )
except HTTPError :
raise SynapseError (
400 ,
" Unable to delete the share on the nextcloud folder {} for the nextcloud group {} . " . format (
room_id , directory_path
) ,
)
await self . store . deleted_room_mapping_with_nextcloud_directory ( room_id )
async def add_room_mapping_with_nextcloud_directory (
self , room_id , requester_id , nextcloud_URL
) :
""" Add a mapping between a room and an Nextcloud folder.
Args :
room_id : the id of the room which must be linked with the Nextcloud folder .
requester_id : the user_id of the requester .
nextcloud_URL : an URL pointing on the Nextcloud folder to link with the room .
"""
nextcloud_URL_query = parse_qs ( urlparse ( nextcloud_URL ) . query )
if " dir " not in nextcloud_URL_query :
raise SynapseError ( 400 , " The url doesn ' t point to a valid directory path. " )
nextcloud_directory_path = nextcloud_URL_query [ " dir " ] [ 0 ]
try :
self . keycloak_access_token = await self . get_keycloak_access_token ( )
except HTTPError :
raise SynapseError (
400 ,
" Unable to retrieve the Keycloak access token of realm {} " . format (
self . keycloak_realm
) ,
)
try :
nextcloud_username = await self . get_nextcloud_username (
get_localpart_from_id ( requester_id )
)
except HTTPError :
raise SynapseError (
400 ,
" Unable to retrieve the corresponding Nextcloud username of user {} . " . format (
requester_id
) ,
)
try :
await self . get_sharing_of_nextcloud_directory (
nextcloud_username , nextcloud_directory_path
)
except HTTPError as e :
if e . response . status_code == 404 :
raise SynapseError (
400 ,
" The user {} doesn ' t have the access right to the folder {} . " . format (
requester_id , nextcloud_directory_path
) ,
Codes . NEXTCLOUD_FOLDER_ACCESS_FORBIDDEN ,
)
raise SynapseError (
400 ,
" Unable to get shares on the folder {} . " . format (
nextcloud_directory_path
) ,
)
try :
group_exists = await self . nextcloud_room_group_exists ( room_id )
except HTTPError :
raise SynapseError (
400 ,
" Unable to retrieve to know if the nextcloud group {} exists or not. " . format (
room_id
) ,
)
if not group_exists :
try :
await self . create_nextcloud_group ( room_id )
except HTTPError :
raise SynapseError (
400 , " Unable to create the Nextcloud group {} . " . format ( room_id )
)
try :
await self . create_new_nextcloud_share (
nextcloud_username , nextcloud_directory_path , room_id
)
except HTTPError :
raise SynapseError (
400 ,
" Unable to create a share for the nextcloud group {} on the nextcloud folder {} . " . format (
room_id , nextcloud_directory_path
) ,
)
await self . store . set_room_mapping_with_nextcloud_directory (
room_id , nextcloud_directory_path
)
async def get_nextcloud_username ( self , user_localpart ) :
""" Get the corresponding Nextcloud username of the synapse user from Keycloak.
Args :
user_localpart : the synapse user localpart .
Returns :
The Nextcloud username .
"""
request = get (
" {} /admin/realms/ {} /users " . format ( self . keycloak_server , self . keycloak_realm ) ,
headers = { " Authorization " : " Bearer {} " . format ( self . keycloak_access_token ) } ,
params = { " username " : user_localpart } ,
)
request . raise_for_status ( )
return request . json ( ) [ 0 ] [ " id " ]
async def get_keycloak_access_token ( self ) :
""" Get the realm Keycloak access token in order to use Keycloak Admin API.
Returns :
The realm Keycloak access token .
"""
request = post (
" {} /realms/ {} /protocol/openid-connect/token " . format (
self . keycloak_server , self . keycloak_realm
) ,
data = {
" client_id " : " admin-cli " ,
" username " : self . service_account_name ,
" password " : self . service_account_password ,
" grant_type " : " password " ,
} ,
)
request . raise_for_status ( )
return request . json ( ) [ " access_token " ]
async def get_sharing_of_nextcloud_directory ( self , username , directory_path ) :
""" Get share for the requester and all reshares on the folder.
Args :
username : the Nextcloud username of the requester .
directory : the directory path of the folder concerned by the share search .
Returns :
A list which contains all shares on the folder .
Each share is a dict that contains lot of information about the share ( id , share_with , owner_uid . . . )
Raises :
HTTPError 401 : Wrong Basic Auth .
HTTPError 404 : Couldn ' t fetch shares. Most likely due to the fact that the user doesn ' t have a share on the folder or the folder doesn ' t exists.
"""
request = get (
" {} /ocs/v2.php/apps/files_sharing/api/v1/shares " . format ( self . nextcloud_server ) ,
headers = { " OCS-APIRequest " : " true " } ,
auth = HTTPBasicAuth ( username , self . nextcloud_shared_secret ) ,
params = { " path " : directory_path , " reshares " : " true " , " format " : " json " } ,
)
request . raise_for_status ( )
return request . json ( ) [ " ocs " ] [ " data " ]
async def nextcloud_room_group_exists ( self , group_name ) :
""" Ask Nextcloud if the group name exist or not.
Args :
group_name : the name of Nextcloud group .
Returns :
True if the group exists , else False .
"""
request = get (
" {} /ocs/v1.php/cloud/groups " . format ( self . nextcloud_server ) ,
headers = { " OCS-APIRequest " : " true " } ,
auth = HTTPBasicAuth ( self . service_account_name , self . service_account_password ) ,
params = { " search " : group_name , " format " : " json " } ,
)
request . raise_for_status ( )
response = request . json ( ) [ " ocs " ] [ " data " ]
return (
True
if " groups " in response and len ( response [ " groups " ] ) > 0
else False
)
async def create_nextcloud_group ( self , room_id ) :
""" Create an Nextcloud group named as room_id and add all users in the room into the new Nextcloud group.
Args :
room_id : the room_id of the room which Nextcloud directory is linked .
"""
request = post (
" {} /ocs/v1.php/cloud/groups " . format ( self . nextcloud_server ) ,
headers = { " OCS-APIRequest " : " true " } ,
auth = HTTPBasicAuth ( self . service_account_name , self . service_account_password ) ,
data = { " groupid " : room_id , " format " : " json " } ,
)
request . raise_for_status ( )
users = await self . store . get_users_in_room ( room_id )
for user in users :
try :
nextcloud_username = await self . get_nextcloud_username (
get_localpart_from_id ( user )
)
await self . add_user_to_nextcloud_groups ( nextcloud_username , room_id )
except HTTPError :
logger . warn (
" An error occured during the addition of the user {} in the Nextcloud group {} . " . format (
user , room_id
)
)
continue
async def add_user_to_nextcloud_groups ( self , username , group_name ) :
""" Add user to the Nextcloud group named as room_id.
Args :
username : the room_id of the room which Nextcloud directory is linked .
group_name : the Nextcloud group name , equivalent to room_id of the room linked .
"""
request = post (
" {} /ocs/v1.php/cloud/users/ {} /groups " . format ( self . nextcloud_server , username ) ,
headers = { " OCS-APIRequest " : " true " } ,
auth = HTTPBasicAuth ( self . service_account_name , self . service_account_password ) ,
data = { " groupid " : group_name , " format " : " json " } ,
)
request . raise_for_status ( )
async def create_new_nextcloud_share ( self , requester , directory_path , group_name ) :
""" Create a share on Nextcloud folder for the specified Nextcloud group.
Post arguments :
permission = 31 - > give all right on the folder ( read , update , create , deleted and share )
shareType = 1 - > a group share
Args :
requester : the Nextcloud username of the requester who want to create the new share .
directory_path : the path of the folder to share .
group_name : the Nextcloud group id .
"""
request = post (
" {} /ocs/v2.php/apps/files_sharing/api/v1/shares " . format ( self . nextcloud_server ) ,
headers = { " OCS-APIRequest " : " true " } ,
auth = HTTPBasicAuth ( requester , self . service_account_password ) ,
data = {
" path " : directory_path ,
" shareType " : 1 ,
" shareWith " : group_name ,
" permissions " : 31 ,
" format " : " json " ,
} ,
)
request . raise_for_status ( )
async def delete_existing_nextcloud_share ( self , requester , share_id ) :
""" Delete an existing share (corresponding to the share id) on the folder.
Args :
requester : the Nextcloud username of the requester who want to delete an existing share .
share_id : the id of the share to delete .
"""
request = delete (
" {} /ocs/v2.php/apps/files_sharing/api/v1/shares/ {} " . format (
self . nextcloud_server , share_id
) ,
headers = { " OCS-APIRequest " : " true " } ,
auth = HTTPBasicAuth ( requester , self . service_account_password ) ,
)
request . raise_for_status ( )
async def get_room_list_to_send_NC_notification (
self , directory , limit_of_notification_propagation
) :
@ -1428,7 +1801,7 @@ class WatchaRoomHandler(BaseHandler):
directories . append ( directory )
for directory in directories :
room = await self . store . get_room_to_send_NC_notification ( directory )
room = await self . store . get_roomID_from_nextcloud_directory_path ( directory )
if room :
rooms . append ( room )
@ -1444,7 +1817,9 @@ class WatchaRoomHandler(BaseHandler):
result = await self . store . get_room_creator ( room_id )
return result
async def send_NC_notification_to_rooms ( self , rooms , file_name , file_url , file_operation ) :
async def send_NC_notification_to_rooms (
self , rooms , file_name , file_url , file_operation
) :
content = {
" body " : file_operation ,