Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make possible for apps to define their own avatar types #24579

Closed

Conversation

danxuliu
Copy link
Member

@danxuliu danxuliu commented Dec 7, 2020

Requires #24578

I am really sorry to come with this change so late in the cycle. It is perfectly fine if it is deferred to Nextcloud 22 :-) (or if it is a bad idea and it is never merged ;-) ). However, while working on the Group conversation picture feature for Talk I thought that it would be good to generalize the avatars and use them for that. This would make possible, for example, to show the avatar of a room in the list of shares without needing to use a Talk endpoint.

I guess it could be used too for unifying the avatars between clients or differentiating user and group avatars (although that would not be something to be done for Nextcloud 21).

Before this pull request the avatars were limited to users and guests, and there was no OCS endpoint for getting and setting the avatars. Now, besides the default user and guest types, an app can register its own avatar type(s), and any avatar can be got or set using a OCS endpoint (and the old ones are still available, of course).

In order to set an avatar with the old endpoints a temporary image needs to be posted, and then a request to crop that temporary image is needed to finally set the avatar. When using the OCS endpoint, on the other hand, the image needs to be uploaded already with the desired size (and it can not be selected from the internal files, the client setting the avatar needs to download it first, crop it as needed and then upload it again). If the avatar does not support the aspect ratio (for example, the user avatar must be squared) then the request fails.

Pending:

  • Add IAvatarProvider (or something like that) and registration of providers to IAvatarManager
  • Prevent user avatars from being modified by other users
    • Should it be done in UserAvatar? Or should be prevented only in the controller (for example, to allow an admin to modify the avatar from an OCC command, or something like that)? In case it needs to be done in the controller, how? Add something like canAvatarBeModifiedByUser($userId) to IAvatar and check it from the controller?
  • Use different cache values in the controller depending on the avatar type
    • Where to specify the cache value to use for each type? With "getCache()" in the AvatarProvider? With a more generic "getAvatarOptions()" (or something similar) that returns a map of keys and values so IAvatarProvider does not need to be extended? In the IAvatar itself so it could even change depending on the avatar instance?
  • Add a method to get the avatar version to IAvatarProvider, similar to getCacheTimeToLive(IAvatar)

Future:

  • Create a UserAvatarProvider and move code specific to user avatars from AvatarManager to UserAvatarProvider (not done yet to minimize the changes). - UserAvatarProvider has been created already, as getting the cache will be done from IAvatarProvider, although the code specific to user avatars was not moved yet to the provider.

Until now requests always had "auth" headers either for an admin or a
regular user, depending on the value of "currentUser". Now, if
"currentUser" starts by "anonymous" no "auth" header is sent, which
makes possible to also test requests with users not logged in.

Signed-off-by: Daniel Calviño Sánchez <[email protected]>
"sendingAToWithRequesttoken" needs to be used to test some non OCS
endpoints which require the request token to be sent in the request. Now
it is possible to specify the body (or, rather, additional contents
beside the cookies and the request token) for those requests, as it will
be needed for example to upload an avatar.

Signed-off-by: Daniel Calviño Sánchez <[email protected]>
Signed-off-by: Daniel Calviño Sánchez <[email protected]>
Even on solid color images the resizing can cause some small artifacts
that slightly modify the color of certain pixels. Due to this now the
color comparison is no longer strict but fuzzy.

Signed-off-by: Daniel Calviño Sánchez <[email protected]>
Signed-off-by: Daniel Calviño Sánchez <[email protected]>
Signed-off-by: Daniel Calviño Sánchez <[email protected]>
"sendingToWith" is used to test OCS endpoints. Now it is possible to
specify the body (or, rather, additional contents beside the cookies and
the request token) for those requests, as it will be needed for example
to upload an avatar.

Signed-off-by: Daniel Calviño Sánchez <[email protected]>
@danxuliu danxuliu added this to the Nextcloud 21 milestone Dec 7, 2020
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
Signed-off-by: Daniel Calviño Sánchez <[email protected]>
Signed-off-by: Daniel Calviño Sánchez <[email protected]>
Signed-off-by: Daniel Calviño Sánchez <[email protected]>
Signed-off-by: Daniel Calviño Sánchez <[email protected]>
@rullzer rullzer mentioned this pull request Dec 14, 2020
59 tasks
Copy link
Member

@ChristophWurst ChristophWurst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this a lot because we can also use this to unify/replace https://github.com/nextcloud/mail/blob/master/lib/Controller/AvatarsController.php

* @return DataResponse|FileDisplayResponse
*/
public function getAvatar(string $avatarType, string $avatarId, int $size): Response {
// min/max size
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can even use the min/max functions for this 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but not for <= 0 use 64.

On the other side @rullzer always wanted to limit the formats anyway.
I would suggest to only allow: 64, 128, 256 and 512 as sizes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I have realized that this (which is a shameless copy/paste from the old controller :-P ) does not allow to use -1 to get an avatar with the original size (which is documented in IAvatar). Should we allow that in the new controller?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only 64, 128, 256, 512 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no real preference on the set of sizes, although limiting it to only a specific set sounds like a good idea to prevent an unneeded use of disk space if a "funny" user does something like for i in {64..512}; do curl .../avatar/user0/$i; done. Thus I have added a commit for that.

I still wonder about the "-1" to get the original size; it could be used for example if a user wants to check her current avatar exactly like it was uploaded.

'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()
]
);
} catch (\Exception $e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This is a bit generic – is it possible to catch a more specific exception?
  • If catching that generic I would highly recommend logging the exception to help our futures selves debug any issues with this code :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit generic – is it possible to catch a more specific exception?

We could catch OCP\Files\NotFoundException as it is thrown by getFile(), InvalidArgumentException as it is thrown by IAvatarManager and whatever is thrown by IAvatarProvider::getAvatar(). I wonder whether other exceptions should be catched or just left to bubble up and generate an error 500 (maybe even the exception thrown by IAvatarProvider::getAvatar() if it ends being just a generic Exception).

If catching that generic I would highly recommend logging the exception to help our futures selves debug any issues with this code :)

👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of AvatarException I named it AvatarProviderException, as that is more specific and gives room to add in the future an AvatarException for avatar specific errors if needed.

In the end I removed the log, as now a generic exception is no longer caught, but an AvatarProviderException, which would be thrown if the provider can not get an avatar for the given id. I think that belongs to the realm of expected errors that do not need to be logged (but if I am wrong it can be added back, of course ;-) ).

core/Controller/GenericAvatarController.php Outdated Show resolved Hide resolved
lib/private/Avatar/AvatarManager.php Outdated Show resolved Hide resolved

if ($context === null) {
// Too early, nothing to do yet
throw new \InvalidArgumentException('Unknown avatar type: ' . $type);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue that we are lying here. The argument isn't invalid, but the code can't handle the request. Another exception would be more appropriate IMO.

Of course this is highly unlikely to happen because we do the registration before anything else. Hence failing hard here could be an option.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue that we are lying here. The argument isn't invalid, but the code can't handle the request. Another exception would be more appropriate IMO.

I agree, although I used InvalidArgumentException because I did not find any suitable exception and adding a new exception just for this seemed unnecessary (given that in practice it will not happen). So exception suggestions welcome :-)

Of course this is highly unlikely to happen because we do the registration before anything else. Hence failing hard here could be an option.

Failing hard as in exit()? Having an exit there would feel very out of place in my opinion. Or do you mean throw new Error()? Or something else? :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, although I used InvalidArgumentException because I did not find any suitable exception and adding a new exception just for this seemed unnecessary (given that in practice it will not happen). So exception suggestions welcome :-)

In https://github.com/nextcloud/server/pull/24702/files#diff-7bfcce99ae84563547086798084d75003c7099fedeabb795d2c01962293b33fcR88 I went with \RuntimeException.

Failing hard as in exit()? Having an exit there would feel very out of place in my opinion. Or do you mean throw new Error()? Or something else? :-)

I meant letting the exception bubble up, without any explicit handling :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In https://github.com/nextcloud/server/pull/24702/files#diff-7bfcce99ae84563547086798084d75003c7099fedeabb795d2c01962293b33fcR88 I went with \RuntimeException.

\RuntimeException it is then.

Failing hard as in exit()? Having an exit there would feel very out of place in my opinion. Or do you mean throw new Error()? Or something else? :-)

I meant letting the exception bubble up, without any explicit handling :)

Ok, that makes a lot more sense :-P

* @return IAvatar
* @throws \InvalidArgumentException if the type is not known or the id is
* not valid
* @throws \Exception if getting the avatar failed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see a more specific exception thrown here. If this is just about any possible error that we didn't expect then I wouldn't document the exception

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be the same exception thrown by IAvatarProvider::getAvatar(). Should AvatarException be introduced to wrap other specific exceptions that could be thrown by providers? For example, in the case of Talk, the avatar provider could throw RoomNotFoundException if the avatar id does not match any room token, but of course that exception makes little sense in a generic context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should AvatarException be introduced to wrap other specific exceptions

Sounds good to me! Then we can transport any more specific exception as previous.

interface IAvatarProvider {

/**
* Returns an IAvatar instance for the given id.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no . at the end of phpdoc lines :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove all them, except at the end of It is allowed to register more than one provider per app. (IRegistrationContext::registerAvatarProvider), as:

  • I guess the rule does not apply to the "body" of a documentation block (but if it does please let me know ;-) )
  • it is consistent with the rest of the documentation in IRegistrationContext

But if that should be removed too let me know ;-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no hard rule, I guess. For the body it seems fine. The first line (title?) is a bit strange.

But this is just nitpicking. You may also just ignore my remark ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may also just ignore my remark ;)

Too late ;-)

@ChristophWurst
Copy link
Member

Should it be done in UserAvatar? Or should be prevented only in the controller (for example, to allow an admin to modify the avatar from an OCC command, or something like that)? In case it needs to be done in the controller, how? Add something like canAvatarBeModifiedByUser($userId) to IAvatar and check it from the controller?

We could also make this part of the new avatar provider, right? And then the controller asks the avatar service, which delegates to the provider (or falls back to false or whatever)

But now I'm also thinking about whether a avatar is just identified by (type, id) or if there should be an optional third param for uid. Then we only let users update their avatars? Or the provider internally checks whether the "current" user is allowed to change that avatar? Like if a user updates an avatar of a talk conversation, the Talk provider could know if that UID is moderator and only then it tells the service/provider that modification is fine.

Alternatively we don't even ask for permission but just let the providers throw a dedicated exception when the operation isn't allowed. But again we will likely need the UID as context.

Where to specify the cache value to use for each type? With "getCache()" in the AvatarProvider? With a more generic "getAvatarOptions()" (or something similar) that returns a map of keys and values so IAvatarProvider does not need to be extended? In the IAvatar itself so it could even change depending on the avatar instance?

A IAvatar::getAvatarOptions could give us lots of flexibility with the price of no type guarantees. Also if this interface is implemented by any app, the app will break with this change.

Therefore I'm more in favor of the IAvatarProvider::getCache. Maybe like a IAvatarProvider::getCacheTtl(IAvatar): ?int so the provider can have the depending on the avatar instance flexibility or ignore the parameter. Returning null could signal that there shouldn't be any caching.

@danxuliu
Copy link
Member Author

We could also make this part of the new avatar provider, right? And then the controller asks the avatar service, which delegates to the provider (or falls back to false or whatever)

But now I'm also thinking about whether a avatar is just identified by (type, id) or if there should be an optional third param for uid. Then we only let users update their avatars? Or the provider internally checks whether the "current" user is allowed to change that avatar? Like if a user updates an avatar of a talk conversation, the Talk provider could know if that UID is moderator and only then it tells the service/provider that modification is fine.

Alternatively we don't even ask for permission but just let the providers throw a dedicated exception when the operation isn't allowed. But again we will likely need the UID as context.

Wouldn't it be possible to inject the current user in the avatar provider constructor? Or that is possible only for controllers? In that case the extra parameter would be unneeded, as the IAvatarProvider subclass would define everything it needs to know if the current user can modify (or even get) an avatar or not. Even if the current user is not a real user, but for example a guest (in Talk currently only moderators would be able to set the avatar of a conversation, but technically that could be possible also for a guest moderator).

Therefore I'm more in favor of the IAvatarProvider::getCache. Maybe like a IAvatarProvider::getCacheTtl(IAvatar): ?int so the provider can have the depending on the avatar instance flexibility or ignore the parameter. Returning null could signal that there shouldn't be any caching.

Fully agree, except maybe in the name. Personally I prefer to avoid abbreviatures in API names (or in code names in general), so I would call it getCacheTimeToLive(IAvatar) instead.

@skjnldsv skjnldsv added this to the Nextcloud 24 milestone Oct 21, 2021
@skjnldsv skjnldsv mentioned this pull request Mar 24, 2022
@blizzz blizzz mentioned this pull request Mar 31, 2022
@blizzz blizzz mentioned this pull request Apr 7, 2022
@danxuliu danxuliu modified the milestones: Nextcloud 24, Nextcloud 25 Apr 13, 2022
@skjnldsv skjnldsv mentioned this pull request Aug 12, 2022
@danxuliu danxuliu modified the milestones: Nextcloud 25, Nextcloud 26 Aug 16, 2022
@blizzz blizzz mentioned this pull request Feb 1, 2023
@skjnldsv skjnldsv mentioned this pull request Feb 23, 2023
@danxuliu danxuliu modified the milestones: Nextcloud 26, Nextcloud 27 Feb 24, 2023
This was referenced May 3, 2023
@skjnldsv skjnldsv modified the milestones: Nextcloud 27, Nextcloud 28 May 9, 2023
@skjnldsv skjnldsv mentioned this pull request Nov 1, 2023
This was referenced Nov 6, 2023
This was referenced Nov 14, 2023
@blizzz blizzz modified the milestones: Nextcloud 28, Nextcloud 29 Nov 23, 2023
@skjnldsv skjnldsv removed this from the Nextcloud 29 milestone Feb 27, 2024
@skjnldsv skjnldsv removed the 2. developing Work in progress label Feb 27, 2024
@skjnldsv
Copy link
Member

As this sounds like a nice feature, the requests for this are quite low. Currently there a no plans to implement such a feature. Thus I will close this ticket for now. This does not mean we don't want this feature, but it is simply not on our roadmap for the near future.

@skjnldsv skjnldsv closed this Feb 27, 2024
@skjnldsv skjnldsv deleted the make-possible-for-apps-to-define-their-own-avatar-types branch March 14, 2024 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants