< Summary

Class:DCL.Social.Friends.RPCSocialApiBridge
Assembly:FriendsController
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/FriendsController/RPCSocialApiBridge.cs
Covered lines:116
Uncovered lines:37
Coverable lines:153
Total lines:500
Line coverage:75.8% (116 of 153)
Covered branches:0
Total branches:0
Covered methods:19
Total methods:23
Method coverage:82.6% (19 of 23)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
RPCSocialApiBridge(...)0%110100%
Dispose()0%6200%
Initialize()0%110100%
<Initialize()0%5.275077.78%
OnTransportError()0%42600%
InitializeClient()0%4.683042.86%
GetInitializationInformationAsync()0%62.1317046.15%
GetAllFriendRequests()0%13.9412076.19%
GetAllFriends()0%26.2515063.16%
WaitForAccessTokenAsync()0%330100%
RejectFriendshipAsync()0%3.583060%
CancelFriendshipAsync()0%3.583060%
AcceptFriendshipAsync()0%3.583060%
DeleteFriendshipAsync()0%3.583060%
RequestFriendshipAsync()0%3.213071.43%
SubscribeToIncomingFriendshipEvents()0%35.6814052%
ProcessIncomingFriendshipEvent()0%8.147071.43%
ProcessIncomingFriendshipEvent()0%14.7714084.21%
LogIncomingFriendshipUpdateEventError(...)0%550100%
GetFriendRequestId(...)0%110100%
UpdateFriendship()0%38.514050%
GetFriendshipUpdateErrorMessage(...)0%56700%
WaitForSocialClient(...)0%2100%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/FriendsController/RPCSocialApiBridge.cs

#LineLine coverage
 1using Cysharp.Threading.Tasks;
 2using DCL.Tasks;
 3using Decentraland.Social.Friendships;
 4using MainScripts.DCL.Controllers.FriendsController;
 5using NUnit.Framework;
 6using System;
 7using System.Collections.Generic;
 8using System.Threading;
 9using UnityEngine;
 10
 11namespace DCL.Social.Friends
 12{
 13    public class RPCSocialApiBridge : ISocialApiBridge
 14    {
 15        private const int REQUEST_TIMEOUT = 30;
 16        private const int MAX_MINUTES_WAIT_TIME = 3;
 17        private const int MAX_RECONNECT_RETRIES = 3;
 18
 19        private readonly IMatrixInitializationBridge matrixInitializationBridge;
 20        private readonly IUserProfileBridge userProfileWebInterfaceBridge;
 21        private readonly ISocialClientProvider socialClientProvider;
 22
 23        private string accessToken;
 24        private IClientFriendshipsService socialClient;
 25        private UniTaskCompletionSource<AllFriendsInitializationMessage> initializationInformationTask;
 43126        private CancellationTokenSource initializationCancellationToken = new ();
 43127        private CancellationTokenSource incomingEventsSubscriptionCancellationToken = new ();
 28
 29        public event Action<FriendRequest> OnIncomingFriendRequestAdded;
 30        public event Action<FriendRequest> OnOutgoingFriendRequestAdded;
 31        public event Action<string> OnFriendRequestAccepted;
 32        public event Action<string> OnFriendRequestRejected;
 33        public event Action<string> OnFriendRequestCanceled;
 34        public event Action<string> OnDeletedByFriend;
 35
 36        private int transportFailures = 0;
 37
 43138        public RPCSocialApiBridge(IMatrixInitializationBridge matrixInitializationBridge,
 39            IUserProfileBridge userProfileWebInterfaceBridge,
 40            ISocialClientProvider socialClientProvider)
 41        {
 43142            this.matrixInitializationBridge = matrixInitializationBridge;
 43143            this.userProfileWebInterfaceBridge = userProfileWebInterfaceBridge;
 43144            this.socialClientProvider = socialClientProvider;
 43145            this.socialClientProvider.OnTransportError += OnTransportError;
 43146        }
 47
 48        public void Dispose()
 49        {
 050            initializationInformationTask?.TrySetCanceled();
 051            initializationCancellationToken.SafeCancelAndDispose();
 052            incomingEventsSubscriptionCancellationToken.SafeCancelAndDispose();
 053        }
 54
 55        public void Initialize()
 56        {
 857            accessToken = matrixInitializationBridge.AccessToken;
 858            matrixInitializationBridge.OnReceiveMatrixAccessToken += token => accessToken = token;
 59
 60            async UniTaskVoid InitializeAsync(CancellationToken cancellationToken)
 61            {
 862                await InitializeClient(cancellationToken);
 2463                await WaitForAccessTokenAsync(cancellationToken);
 64
 865                incomingEventsSubscriptionCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellati
 66
 67                // this is an endless task that's why is forgotten
 868                SubscribeToIncomingFriendshipEvents(incomingEventsSubscriptionCancellationToken.Token).Forget();
 869            }
 70
 871            initializationCancellationToken = initializationCancellationToken.SafeRestart();
 872            InitializeAsync(initializationCancellationToken.Token).Forget();
 873        }
 74
 75        private async void OnTransportError()
 76        {
 077            socialClient = null;
 78
 079            incomingEventsSubscriptionCancellationToken = incomingEventsSubscriptionCancellationToken.SafeRestartLinked(
 80
 081            if (transportFailures >= MAX_RECONNECT_RETRIES)
 82            {
 083                Debug.LogError("Max reconnect retries reached");
 084                return;
 85            }
 86
 087            while (transportFailures < MAX_RECONNECT_RETRIES)
 88            {
 089                Debug.Log("Reconnecting to Social service");
 90
 091                transportFailures++;
 92
 093                CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(
 94                    TimeSpan.FromSeconds(Math.Pow(5, transportFailures) + REQUEST_TIMEOUT)
 95                );
 96
 097                await InitializeClient(cancellationTokenSource.Token);
 98
 099                if (socialClient != null)
 100                {
 101                    // this is an endless task that's why is forgotten
 0102                    SubscribeToIncomingFriendshipEvents(incomingEventsSubscriptionCancellationToken.Token).Forget();
 0103                    transportFailures = 0;
 104
 0105                    return;
 106                }
 107            }
 0108        }
 109
 110        private async UniTask InitializeClient(CancellationToken cancellationToken = default)
 111        {
 16112            try { socialClient = await socialClientProvider.Provide(cancellationToken); }
 0113            catch (Exception e) { Debug.LogException(e); }
 8114        }
 115
 116        public async UniTask<AllFriendsInitializationMessage> GetInitializationInformationAsync(CancellationToken cancel
 117        {
 1118            if (initializationInformationTask != null) return await initializationInformationTask.Task.AttachExternalCan
 119
 1120            initializationInformationTask = new UniTaskCompletionSource<AllFriendsInitializationMessage>();
 121
 1122            if (socialClient == null) { await WaitForSocialClient(cancellationToken); }
 123
 3124            await WaitForAccessTokenAsync(cancellationToken);
 125
 126            // TODO: the bridge should not fetch all friends at start, its a responsibility/design issue.
 127            // It should be fetched by its request function accordingly
 1128            List<string> allFriends = await GetAllFriends(cancellationToken);
 129
 130            // TODO: the bridge should not fetch all friend requests at start, its a responsibility/design issue.
 131            // It should be fetched by its request function accordingly
 1132            (List<FriendRequest> incoming, List<FriendRequest> outgoing) = await GetAllFriendRequests(cancellationToken)
 133
 1134            await UniTask.SwitchToMainThread(cancellationToken);
 135
 1136            initializationInformationTask.TrySetResult(new AllFriendsInitializationMessage(
 137                allFriends, incoming, outgoing));
 138
 1139            return await initializationInformationTask.Task.AttachExternalCancellation(cancellationToken);
 1140        }
 141
 142        private async UniTask<(List<FriendRequest> incoming, List<FriendRequest> outgoing)> GetAllFriendRequests(
 143            CancellationToken cancellationToken = default)
 144        {
 1145            List<FriendRequest> incoming = new ();
 1146            List<FriendRequest> outgoing = new ();
 147
 1148            cancellationToken.ThrowIfCancellationRequested();
 149
 1150            if (socialClient == null) { await WaitForSocialClient(cancellationToken); }
 151
 1152            var requestEvents = await socialClient.GetRequestEvents(new Payload
 153                { SynapseToken = accessToken });
 154
 1155            cancellationToken.ThrowIfCancellationRequested();
 156
 8157            foreach (var friendRequest in requestEvents.Events.Incoming.Items)
 158            {
 3159                incoming.Add(new FriendRequest(
 160                    GetFriendRequestId(friendRequest.User.Address, friendRequest.CreatedAt),
 161
 162                    // timestamps comes in seconds instead of milliseconds, so do the conversion
 163                    DateTimeOffset.FromUnixTimeMilliseconds(friendRequest.CreatedAt * 1000L).DateTime,
 164                    friendRequest.User.Address,
 165                    userProfileWebInterfaceBridge.GetOwn().userId,
 166                    friendRequest.Message));
 167            }
 168
 10169            foreach (var friendRequest in requestEvents.Events.Outgoing.Items)
 170            {
 4171                outgoing.Add(new FriendRequest(
 172                    GetFriendRequestId(friendRequest.User.Address, friendRequest.CreatedAt),
 173
 174                    // timestamps comes in seconds instead of milliseconds, so do the conversion
 175                    DateTimeOffset.FromUnixTimeMilliseconds(friendRequest.CreatedAt * 1000L).DateTime,
 176                    userProfileWebInterfaceBridge.GetOwn().userId,
 177                    friendRequest.User.Address,
 178                    friendRequest.Message));
 179            }
 180
 1181            return (incoming: incoming, outgoing: outgoing);
 1182        }
 183
 184        private async UniTask<List<string>> GetAllFriends(CancellationToken cancellationToken)
 185        {
 1186            List<string> result = new ();
 187
 1188            if (socialClient == null) { await WaitForSocialClient(cancellationToken); }
 189
 1190            var friendsStream = socialClient.GetFriends(new Payload
 191                { SynapseToken = accessToken });
 192
 6193            await foreach (var friends in friendsStream.WithCancellation(cancellationToken))
 194            {
 12195                foreach (User friend in friends.Users.Users_)
 4196                    result.Add(friend.Address);
 197            }
 198
 1199            return result;
 1200        }
 201
 202        private async UniTask WaitForAccessTokenAsync(CancellationToken cancellationToken)
 203        {
 27204            await UniTask.WaitUntil(() =>
 9205                    !string.IsNullOrEmpty(accessToken),
 206                PlayerLoopTiming.Update,
 207                cancellationToken);
 9208        }
 209
 210        public async UniTask RejectFriendshipAsync(string friendId, CancellationToken cancellationToken = default)
 211        {
 1212            var updateFriendshipPayload = new UpdateFriendshipPayload
 213            {
 214                Event =
 215                    new FriendshipEventPayload
 216                    {
 217                        Reject = new RejectPayload
 218                        {
 219                            User = new User
 220                                { Address = friendId },
 221                        }
 222                    },
 223                AuthToken = new Payload
 224                {
 225                    SynapseToken = accessToken,
 226                }
 227            };
 228
 1229            await this.UpdateFriendship(updateFriendshipPayload, friendId, cancellationToken);
 1230        }
 231
 232        public async UniTask CancelFriendshipAsync(string friendId, CancellationToken cancellationToken = default)
 233        {
 1234            var updateFriendshipPayload = new UpdateFriendshipPayload
 235            {
 236                Event =
 237                    new FriendshipEventPayload
 238                    {
 239                        Cancel = new CancelPayload
 240                        {
 241                            User = new User
 242                                { Address = friendId }
 243                        }
 244                    },
 245                AuthToken = new Payload
 246                {
 247                    SynapseToken = accessToken,
 248                }
 249            };
 250
 1251            await this.UpdateFriendship(updateFriendshipPayload, friendId, cancellationToken);
 1252        }
 253
 254        public async UniTask AcceptFriendshipAsync(string friendId, CancellationToken cancellationToken = default)
 255        {
 1256            var updateFriendshipPayload = new UpdateFriendshipPayload
 257            {
 258                Event =
 259                    new FriendshipEventPayload
 260                    {
 261                        Accept = new AcceptPayload
 262                        {
 263                            User = new User
 264                                { Address = friendId }
 265                        }
 266                    },
 267                AuthToken = new Payload
 268                {
 269                    SynapseToken = accessToken,
 270                }
 271            };
 272
 1273            await this.UpdateFriendship(updateFriendshipPayload, friendId, cancellationToken);
 1274        }
 275
 276        public async UniTask DeleteFriendshipAsync(string friendId, CancellationToken cancellationToken = default)
 277        {
 1278            var updateFriendshipPayload = new UpdateFriendshipPayload
 279            {
 280                Event =
 281                    new FriendshipEventPayload
 282                    {
 283                        Delete = new DeletePayload
 284                        {
 285                            User = new User
 286                                { Address = friendId }
 287                        }
 288                    },
 289                AuthToken = new Payload
 290                {
 291                    SynapseToken = accessToken,
 292                }
 293            };
 294
 1295            await this.UpdateFriendship(updateFriendshipPayload, friendId, cancellationToken);
 1296        }
 297
 298        public async UniTask<FriendRequest> RequestFriendshipAsync(string friendId, string messageBody, CancellationToke
 299        {
 1300            var updateFriendshipPayload = new UpdateFriendshipPayload
 301            {
 302                Event = new FriendshipEventPayload
 303                {
 304                    Request = new RequestPayload
 305                    {
 306                        Message = messageBody,
 307                        User = new User
 308                            { Address = friendId },
 309                    }
 310                },
 311                AuthToken = new Payload
 312                {
 313                    SynapseToken = accessToken,
 314                }
 315            };
 316
 1317            FriendshipEventResponse @event = await UpdateFriendship(updateFriendshipPayload, friendId, cancellationToken
 1318            RequestResponse response = @event.Request;
 319
 1320            return new FriendRequest(
 321                GetFriendRequestId(response.User.Address, response.CreatedAt),
 322
 323                // timestamps comes in seconds instead of milliseconds, so do the conversion
 324                DateTimeOffset.FromUnixTimeMilliseconds(response.CreatedAt * 1000L).DateTime,
 325                userProfileWebInterfaceBridge.GetOwn().userId,
 326                response.User.Address,
 327                response.Message);
 1328        }
 329
 330        private async UniTask SubscribeToIncomingFriendshipEvents(CancellationToken cancellationToken = default)
 331        {
 332            try
 333            {
 8334                IUniTaskAsyncEnumerable<SubscribeFriendshipEventsUpdatesResponse> stream = socialClient
 335                   .SubscribeFriendshipEventsUpdates(new Payload { SynapseToken = accessToken });
 336
 34337                await foreach (var friendshipEventResponse in stream.WithCancellation(cancellationToken))
 338                {
 6339                    cancellationToken.ThrowIfCancellationRequested();
 340
 6341                    switch (friendshipEventResponse.ResponseCase)
 342                    {
 343                        case SubscribeFriendshipEventsUpdatesResponse.ResponseOneofCase.InternalServerError:
 344                        case SubscribeFriendshipEventsUpdatesResponse.ResponseOneofCase.UnauthorizedError:
 345                        case SubscribeFriendshipEventsUpdatesResponse.ResponseOneofCase.ForbiddenError:
 346                        case SubscribeFriendshipEventsUpdatesResponse.ResponseOneofCase.TooManyRequestsError:
 4347                            LogIncomingFriendshipUpdateEventError(friendshipEventResponse);
 4348                            break;
 349                        case SubscribeFriendshipEventsUpdatesResponse.ResponseOneofCase.Events:
 4350                            try { await ProcessIncomingFriendshipEvent(friendshipEventResponse.Events, cancellationToken
 0351                            catch (OperationCanceledException) { throw; }
 352                            catch (Exception e)
 353                            {
 354                                // just log the exception so we keep receiving updates in the loop
 0355                                Debug.LogException(e);
 0356                            }
 357
 358                            break;
 359                        default:
 360                        {
 0361                            Debug.LogErrorFormat("Subscription to friendship events got invalid response {0}", friendshi
 362                            break;
 363                        }
 364                    }
 365                }
 2366            }
 0367            catch (Exception ex) { Debug.LogException(ex); }
 2368        }
 369
 370        private async UniTask ProcessIncomingFriendshipEvent(FriendshipEventResponses friendshipEventsUpdatesResponse,
 371            CancellationToken cancellationToken = default)
 372        {
 16373            foreach (var friendshipEvent in friendshipEventsUpdatesResponse.Responses)
 6374                await ProcessIncomingFriendshipEvent(friendshipEvent, cancellationToken);
 2375        }
 376
 377        private async UniTask ProcessIncomingFriendshipEvent(FriendshipEventResponse friendshipEvent, CancellationToken 
 378        {
 6379            await UniTask.SwitchToMainThread(cancellationToken);
 380
 6381            switch (friendshipEvent.BodyCase)
 382            {
 383                case FriendshipEventResponse.BodyOneofCase.Request:
 1384                    var response = friendshipEvent.Request;
 385
 1386                    var request = new FriendRequest(
 387                        GetFriendRequestId(response.User.Address, response.CreatedAt),
 388
 389                        // timestamps comes in seconds instead of milliseconds, so do the conversion
 390                        DateTimeOffset.FromUnixTimeMilliseconds(response.CreatedAt * 1000L).DateTime,
 391                        response.User.Address,
 392                        userProfileWebInterfaceBridge.GetOwn().userId,
 393                        response.Message);
 394
 1395                    OnIncomingFriendRequestAdded?.Invoke(request);
 1396                    break;
 397                case FriendshipEventResponse.BodyOneofCase.Accept:
 1398                    cancellationToken.ThrowIfCancellationRequested();
 399
 1400                    OnFriendRequestAccepted?.Invoke(friendshipEvent.Accept.User.Address);
 1401                    break;
 402                case FriendshipEventResponse.BodyOneofCase.Reject:
 2403                    OnFriendRequestRejected?.Invoke(friendshipEvent.Reject.User.Address);
 2404                    break;
 405                case FriendshipEventResponse.BodyOneofCase.Delete:
 1406                    OnDeletedByFriend?.Invoke(friendshipEvent.Delete.User.Address);
 1407                    break;
 408                case FriendshipEventResponse.BodyOneofCase.Cancel:
 1409                    OnFriendRequestCanceled?.Invoke(friendshipEvent.Cancel.User.Address);
 1410                    break;
 411                default:
 0412                    Debug.LogErrorFormat("Invalid friendship event {0}", friendshipEvent);
 413                    break;
 414            }
 6415        }
 416
 417        private void LogIncomingFriendshipUpdateEventError(SubscribeFriendshipEventsUpdatesResponse error)
 418        {
 4419            switch (error.ResponseCase)
 420            {
 421                case SubscribeFriendshipEventsUpdatesResponse.ResponseOneofCase.InternalServerError:
 1422                    Debug.LogErrorFormat("Subscription to friendship events got internal server error {0}", error.Intern
 1423                    break;
 424                case SubscribeFriendshipEventsUpdatesResponse.ResponseOneofCase.UnauthorizedError:
 1425                    Debug.LogErrorFormat("Subscription to friendship events got Unauthorized error {0}", error.Unauthori
 1426                    break;
 427                case SubscribeFriendshipEventsUpdatesResponse.ResponseOneofCase.ForbiddenError:
 1428                    Debug.LogErrorFormat("Subscription to friendship events got Forbidden error {0}", error.ForbiddenErr
 1429                    break;
 430                case SubscribeFriendshipEventsUpdatesResponse.ResponseOneofCase.TooManyRequestsError:
 1431                    Debug.LogErrorFormat("Subscription to friendship events got Too many requests error {0}", error.TooM
 432                    break;
 433            }
 1434        }
 435
 436        private static string GetFriendRequestId(string userId, long createdAt) =>
 9437            $"{userId}-{createdAt}";
 438
 439        private async UniTask<FriendshipEventResponse> UpdateFriendship(
 440            UpdateFriendshipPayload updateFriendshipPayload,
 441            string friendId,
 442            CancellationToken cancellationToken = default)
 443        {
 444            try
 445            {
 5446                cancellationToken.ThrowIfCancellationRequested();
 447
 5448                if (socialClient == null) { await WaitForSocialClient(cancellationToken); }
 449
 450                // TODO: pass cancellation token to rpc client when is supported
 5451                var response = await socialClient
 452                                    .UpdateFriendshipEvent(updateFriendshipPayload)
 453                                    .Timeout(TimeSpan.FromSeconds(REQUEST_TIMEOUT));
 454
 5455                cancellationToken.ThrowIfCancellationRequested();
 456
 5457                switch (response.ResponseCase)
 458                {
 459                    case UpdateFriendshipResponse.ResponseOneofCase.Event:
 5460                        return response.Event;
 461                    case UpdateFriendshipResponse.ResponseOneofCase.InternalServerError:
 462                    case UpdateFriendshipResponse.ResponseOneofCase.UnauthorizedError:
 463                    case UpdateFriendshipResponse.ResponseOneofCase.ForbiddenError:
 464                    case UpdateFriendshipResponse.ResponseOneofCase.TooManyRequestsError:
 465                    case UpdateFriendshipResponse.ResponseOneofCase.BadRequestError:
 466                    default:
 0467                        throw new Exception(GetFriendshipUpdateErrorMessage(response, friendId));
 468                }
 469            }
 5470            finally { await UniTask.SwitchToMainThread(); }
 5471        }
 472
 473        private string GetFriendshipUpdateErrorMessage(UpdateFriendshipResponse error, string friendId)
 474        {
 0475            switch (error.ResponseCase)
 476            {
 477                case UpdateFriendshipResponse.ResponseOneofCase.InternalServerError:
 0478                    return $"Got internal server error while trying to update friendship {friendId} {error.InternalServe
 479                case UpdateFriendshipResponse.ResponseOneofCase.UnauthorizedError:
 0480                    return $"Got Unauthorized error while trying to update friendship {friendId} {error.UnauthorizedErro
 481                case UpdateFriendshipResponse.ResponseOneofCase.ForbiddenError:
 0482                    return $"Got Forbidden error while trying to update friendship {friendId} {error.ForbiddenError.Mess
 483                case UpdateFriendshipResponse.ResponseOneofCase.TooManyRequestsError:
 0484                    return $"Got Too many requests error {friendId} {error.TooManyRequestsError.Message} while trying to
 485                case UpdateFriendshipResponse.ResponseOneofCase.BadRequestError:
 0486                    return $"Got Bad request {friendId} {error.BadRequestError.Message} while trying to update friendshi
 487                default:
 0488                    return $"Unsupported friendship error {error.ResponseCase}";
 489            }
 490        }
 491
 492        private UniTask WaitForSocialClient(CancellationToken cancellationToken)
 493        {
 0494            CancellationTokenSource timeoutCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(can
 0495            timeoutCancellationTokenSource.CancelAfterSlim(TimeSpan.FromMinutes(MAX_MINUTES_WAIT_TIME));
 496
 0497            return UniTask.WaitUntil(() => socialClient != null, cancellationToken: timeoutCancellationTokenSource.Token
 498        }
 499    }
 500}