diff options
| author | rr- | 2016-04-09 21:41:10 +0200 |
|---|---|---|
| committer | rr- | 2016-04-09 22:38:57 +0200 |
| commit | e8aeb110814e8f764a3b4d7eeaaae17b26787f88 (patch) | |
| tree | b3f4bc907c20cfd3b3b9851408b58bf3de16790c /server | |
| parent | 403cfbd679aa8d8c55f666b1ae9513b13ee3632a (diff) | |
server/users: add avatar support
Diffstat (limited to 'server')
| -rw-r--r-- | server/szurubooru/api/user_api.py | 13 | ||||
| -rw-r--r-- | server/szurubooru/app.py | 4 | ||||
| -rw-r--r-- | server/szurubooru/config.py | 9 | ||||
| -rw-r--r-- | server/szurubooru/db/user.py | 7 | ||||
| -rw-r--r-- | server/szurubooru/errors.py | 3 | ||||
| -rw-r--r-- | server/szurubooru/middleware/json_translator.py | 5 | ||||
| -rw-r--r-- | server/szurubooru/tests/api/test_user_api.py | 11 | ||||
| -rw-r--r-- | server/szurubooru/util/files.py | 8 | ||||
| -rw-r--r-- | server/szurubooru/util/images.py | 48 | ||||
| -rw-r--r-- | server/szurubooru/util/users.py | 27 |
10 files changed, 110 insertions, 25 deletions
diff --git a/server/szurubooru/api/user_api.py b/server/szurubooru/api/user_api.py index bae3cb6..227c6b1 100644 --- a/server/szurubooru/api/user_api.py +++ b/server/szurubooru/api/user_api.py @@ -19,8 +19,10 @@ def _serialize_user(authenticated_user, user): md5.update((user.email or user.name).lower().encode('utf-8')) digest = md5.hexdigest() ret['avatarUrl'] = 'http://gravatar.com/avatar/%s?s=%d' % ( - digest, config.config['avatar_thumbnail_size']) - # TODO: else construct a link + digest, config.config['thumbnails']['avatar_width']) + else: + ret['avatarUrl'] = '%s/avatars/%s.jpg' % ( + config.config['data_url'].rstrip('/'), user.name.lower()) if authenticated_user.user_id == user.user_id: ret['email'] = user.email @@ -107,7 +109,12 @@ class UserDetailApi(BaseApi): auth.verify_privilege(context.user, 'users:edit:%s:rank' % infix) users.update_rank(user, context.request['rank'], context.user) - # TODO: avatar + if 'avatar_style' in context.request: + auth.verify_privilege(context.user, 'users:edit:%s:avatar' % infix) + users.update_avatar( + user, + context.request['avatar_style'], + context.files.get('avatar') or None) context.session.commit() return {'user': _serialize_user(context.user, user)} diff --git a/server/szurubooru/app.py b/server/szurubooru/app.py index 1d33e08..bd870ac 100644 --- a/server/szurubooru/app.py +++ b/server/szurubooru/app.py @@ -39,6 +39,9 @@ def _on_integrity_error(ex, _request, _response, _params): def _on_not_found_error(ex, _request, _response, _params): raise falcon.HTTPNotFound(title='Not found', description=str(ex)) +def _on_processing_error(ex, _request, _response, _params): + raise falcon.HTTPNotFound(title='Processing error', description=str(ex)) + def create_app(): ''' Create a WSGI compatible App object. ''' engine = sqlalchemy.create_engine( @@ -71,6 +74,7 @@ def create_app(): app.add_error_handler(errors.ValidationError, _on_validation_error) app.add_error_handler(errors.SearchError, _on_search_error) app.add_error_handler(errors.NotFoundError, _on_not_found_error) + app.add_error_handler(errors.ProcessingError, _on_processing_error) app.add_route('/users/', user_list_api) app.add_route('/user/{user_name}', user_detail_api) diff --git a/server/szurubooru/config.py b/server/szurubooru/config.py index 38637c3..bc9c641 100644 --- a/server/szurubooru/config.py +++ b/server/szurubooru/config.py @@ -44,6 +44,15 @@ class Config(object): 'Default rank %r is not on the list of known ranks' % ( self['default_rank'])) + for key in ['base_url', 'api_url', 'data_url', 'data_dir']: + if not self[key]: + raise errors.ConfigError( + 'Service is not configured: %r is missing' % key) + + if not os.path.isabs(self['data_dir']): + raise errors.ConfigError( + 'data_dir must be an absolute path') + for key in ['schema', 'host', 'port', 'user', 'pass', 'name']: if not self['database'][key]: raise errors.ConfigError( diff --git a/server/szurubooru/db/user.py b/server/szurubooru/db/user.py index 663ddc8..a8bb004 100644 --- a/server/szurubooru/db/user.py +++ b/server/szurubooru/db/user.py @@ -4,8 +4,8 @@ from szurubooru.db.base import Base class User(Base): __tablename__ = 'user' - AVATAR_GRAVATAR = 1 - AVATAR_MANUAL = 2 + AVATAR_GRAVATAR = 'gravatar' + AVATAR_MANUAL = 'manual' user_id = sa.Column('id', sa.Integer, primary_key=True) name = sa.Column('name', sa.String(50), nullable=False, unique=True) @@ -15,4 +15,5 @@ class User(Base): rank = sa.Column('rank', sa.String(32), nullable=False) creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) last_login_time = sa.Column('last_login_time', sa.DateTime) - avatar_style = sa.Column('avatar_style', sa.String(32), nullable=False) + avatar_style = sa.Column( + 'avatar_style', sa.String(32), nullable=False, default=AVATAR_GRAVATAR) diff --git a/server/szurubooru/errors.py b/server/szurubooru/errors.py index 90b8c65..c5481df 100644 --- a/server/szurubooru/errors.py +++ b/server/szurubooru/errors.py @@ -15,3 +15,6 @@ class SearchError(RuntimeError): class NotFoundError(RuntimeError): ''' Error thrown when a resource (usually DB) couldn't be found. ''' + +class ProcessingError(RuntimeError): + ''' Error thrown by things such as thumbnail generator. ''' diff --git a/server/szurubooru/middleware/json_translator.py b/server/szurubooru/middleware/json_translator.py index c16e8a3..a852ee4 100644 --- a/server/szurubooru/middleware/json_translator.py +++ b/server/szurubooru/middleware/json_translator.py @@ -28,9 +28,8 @@ class JsonTranslator(object): form = cgi.FieldStorage(fp=request.stream, environ=request.env) for key in form: if key != 'metadata': - request.context.files[key] = ( - form.getvalue(key), - getattr(form[key], 'filename', None)) + _original_file_name = getattr(form[key], 'filename', None) + request.context.files[key] = form.getvalue(key) body = form.getvalue('metadata') else: body = request.stream.read().decode('utf-8') diff --git a/server/szurubooru/tests/api/test_user_api.py b/server/szurubooru/tests/api/test_user_api.py index 5a5dab8..4614f3b 100644 --- a/server/szurubooru/tests/api/test_user_api.py +++ b/server/szurubooru/tests/api/test_user_api.py @@ -11,7 +11,7 @@ class TestRetrievingUsers(DatabaseTestCase): 'privileges': { 'users:list': 'regular_user', }, - 'avatar_thumbnail_size': 200, + 'thumbnails': {'avatar_width': 200}, 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], 'rank_names': {}, }) @@ -55,7 +55,7 @@ class TestRetrievingUser(DatabaseTestCase): 'privileges': { 'users:view': 'regular_user', }, - 'avatar_thumbnail_size': 200, + 'thumbnails': {'avatar_width': 200}, 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], 'rank_names': {}, }) @@ -73,7 +73,7 @@ class TestRetrievingUser(DatabaseTestCase): self.assertEqual(result['user']['rank'], 'regular_user') self.assertEqual(result['user']['creationTime'], datetime(1997, 1, 1)) self.assertEqual(result['user']['lastLoginTime'], None) - self.assertEqual(result['user']['avatarStyle'], 1) # i.e. integer + self.assertEqual(result['user']['avatarStyle'], 'gravatar') def test_retrieving_non_existing(self): self.context.user.rank = 'regular_user' @@ -137,7 +137,7 @@ class TestCreatingUser(DatabaseTestCase): 'user_name_regex': '.{3,}', 'password_regex': '.{3,}', 'default_rank': 'regular_user', - 'avatar_thumbnail_size': 200, + 'thumbnails': {'avatar_width': 200}, 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], 'rank_names': {}, 'privileges': { @@ -214,7 +214,7 @@ class TestUpdatingUser(DatabaseTestCase): 'secret': '', 'user_name_regex': '.{3,}', 'password_regex': '.{3,}', - 'avatar_thumbnail_size': 200, + 'thumbnails': {'avatar_width': 200}, 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], 'rank_names': {}, 'privileges': { @@ -222,7 +222,6 @@ class TestUpdatingUser(DatabaseTestCase): 'users:edit:self:pass': 'regular_user', 'users:edit:self:email': 'regular_user', 'users:edit:self:rank': 'mod', - 'users:edit:any:name': 'mod', 'users:edit:any:pass': 'mod', 'users:edit:any:email': 'mod', diff --git a/server/szurubooru/util/files.py b/server/szurubooru/util/files.py new file mode 100644 index 0000000..7091f88 --- /dev/null +++ b/server/szurubooru/util/files.py @@ -0,0 +1,8 @@ +import os +from szurubooru import config + +def save(path, content): + full_path = os.path.join(config.config['data_dir'], path) + os.makedirs(os.path.dirname(full_path), exist_ok=True) + with open(full_path, 'wb') as handle: + handle.write(content) diff --git a/server/szurubooru/util/images.py b/server/szurubooru/util/images.py new file mode 100644 index 0000000..17b2a74 --- /dev/null +++ b/server/szurubooru/util/images.py @@ -0,0 +1,48 @@ +import subprocess +from szurubooru import errors + +_SCALE_FIT_FMT = \ + r'scale=iw*max({width}/iw\,{height}/ih):ih*max({width}/iw\,{height}/ih)' + +class Image(object): + def __init__(self, content): + self.content = content + + def resize_fill(self, width, height): + self.content = self._execute([ + '-i', '-', + '-f', 'image2', + '-vf', _SCALE_FIT_FMT.format(width=width, height=height), + '-vframes', '1', + '-vcodec', 'png', + '-', + ]) + + def to_png(self): + return self._execute([ + '-i', '-', + '-f', 'image2', + '-vframes', '1', + '-vcodec', 'png', + '-', + ]) + + def to_jpeg(self): + return self._execute([ + '-i', '-', + '-f', 'image2', + '-vframes', '1', + '-vcodec', 'mjpeg', + '-', + ]) + + def _execute(self, cli): + proc = subprocess.Popen( + ['ffmpeg'] + cli, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = proc.communicate(input=self.content) + if proc.returncode != 0: + raise errors.ConversionError(err) + return out diff --git a/server/szurubooru/util/users.py b/server/szurubooru/util/users.py index 7b7d52f..1f1765b 100644 --- a/server/szurubooru/util/users.py +++ b/server/szurubooru/util/users.py @@ -2,10 +2,9 @@ import re from datetime import datetime from sqlalchemy import func from szurubooru import config, db, errors -from szurubooru.util import auth, misc +from szurubooru.util import auth, misc, files, images def create_user(session, name, password, email): - ''' Create an user with given parameters and returns it. ''' user = db.User() update_name(user, name) update_password(user, password) @@ -19,7 +18,6 @@ def create_user(session, name, password, email): return user def update_name(user, name): - ''' Validate and update user's name. ''' name = name.strip() name_regex = config.config['user_name_regex'] if not re.match(name_regex, name): @@ -28,7 +26,6 @@ def update_name(user, name): user.name = name def update_password(user, password): - ''' Validate and update user's password. ''' password_regex = config.config['password_regex'] if not re.match(password_regex, password): raise errors.ValidationError( @@ -37,7 +34,6 @@ def update_password(user, password): user.password_hash = auth.get_password_hash(user.password_salt, password) def update_email(user, email): - ''' Validate and update user's email. ''' email = email.strip() or None if not misc.is_valid_email(email): raise errors.ValidationError( @@ -52,28 +48,39 @@ def update_rank(user, rank, authenticated_user): 'Bad rank %r. Valid ranks: %r' % (rank, available_ranks)) if available_ranks.index(authenticated_user.rank) \ < available_ranks.index(rank): - raise errors.AuthError('Trying to set higher rank than your own') + raise errors.AuthError('Trying to set higher rank than your own.') user.rank = rank +def update_avatar(user, avatar_style, avatar_content): + if avatar_style == 'gravatar': + user.avatar_style = user.AVATAR_GRAVATAR + elif avatar_style == 'manual': + user.avatar_style = user.AVATAR_MANUAL + if not avatar_content: + raise errors.ValidationError('Avatar content missing.') + image = images.Image(avatar_content) + image.resize_fill( + int(config.config['thumbnails']['avatar_width']), + int(config.config['thumbnails']['avatar_height'])) + files.save('avatars/' + user.name.lower() + '.jpg', image.to_jpeg()) + else: + raise errors.ValidationError('Unknown avatar style: %r' % avatar_style) + def bump_login_time(user): - ''' Update user's login time to current date. ''' user.last_login_time = datetime.now() def reset_password(user): - ''' Reset password for an user. ''' password = auth.create_password() user.password_salt = auth.create_password() user.password_hash = auth.get_password_hash(user.password_salt, password) return password def get_by_name(session, name): - ''' Retrieve an user by its name. ''' return session.query(db.User) \ .filter(func.lower(db.User.name) == func.lower(name)) \ .first() def get_by_name_or_email(session, name_or_email): - ''' Retrieve an user by its name or email. ''' return session.query(db.User) \ .filter( (func.lower(db.User.name) == func.lower(name_or_email)) |