aboutsummaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorrr-2016-04-09 21:41:10 +0200
committerrr-2016-04-09 22:38:57 +0200
commite8aeb110814e8f764a3b4d7eeaaae17b26787f88 (patch)
treeb3f4bc907c20cfd3b3b9851408b58bf3de16790c /server
parent403cfbd679aa8d8c55f666b1ae9513b13ee3632a (diff)
server/users: add avatar support
Diffstat (limited to 'server')
-rw-r--r--server/szurubooru/api/user_api.py13
-rw-r--r--server/szurubooru/app.py4
-rw-r--r--server/szurubooru/config.py9
-rw-r--r--server/szurubooru/db/user.py7
-rw-r--r--server/szurubooru/errors.py3
-rw-r--r--server/szurubooru/middleware/json_translator.py5
-rw-r--r--server/szurubooru/tests/api/test_user_api.py11
-rw-r--r--server/szurubooru/util/files.py8
-rw-r--r--server/szurubooru/util/images.py48
-rw-r--r--server/szurubooru/util/users.py27
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))

© 2015 - 2026 Jakob L. Kreuze