2017-02-04 00:08:12 +00:00
|
|
|
from datetime import datetime
|
2018-02-25 10:44:02 +00:00
|
|
|
from typing import Any, Optional, Union, List, Dict, Callable
|
|
|
|
import re
|
2017-02-04 00:08:12 +00:00
|
|
|
import sqlalchemy as sa
|
|
|
|
from szurubooru import config, db, model, errors, rest
|
|
|
|
from szurubooru.func import auth, util, serialization, files, images
|
2016-04-03 20:03:58 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
|
|
|
class UserNotFoundError(errors.NotFoundError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class UserAlreadyExistsError(errors.ValidationError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidUserNameError(errors.ValidationError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidEmailError(errors.ValidationError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidPasswordError(errors.ValidationError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidRankError(errors.ValidationError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidAvatarError(errors.ValidationError):
|
|
|
|
pass
|
|
|
|
|
2016-04-16 13:07:33 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def get_avatar_path(user_name: str) -> str:
|
2016-08-14 09:50:48 +00:00
|
|
|
return 'avatars/' + user_name.lower() + '.png'
|
2016-06-17 19:25:09 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def get_avatar_url(user: model.User) -> str:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2016-04-24 07:47:58 +00:00
|
|
|
if user.avatar_style == user.AVATAR_GRAVATAR:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user.email or user.name
|
2016-07-30 11:39:28 +00:00
|
|
|
return 'https://gravatar.com/avatar/%s?d=retro&s=%d' % (
|
2016-04-30 21:17:08 +00:00
|
|
|
util.get_md5((user.email or user.name).lower()),
|
|
|
|
config.config['thumbnails']['avatar_width'])
|
2017-02-03 20:42:15 +00:00
|
|
|
assert user.name
|
|
|
|
return '%s/avatars/%s.png' % (
|
|
|
|
config.config['data_url'].rstrip('/'), user.name.lower())
|
2016-04-24 07:47:58 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def get_email(
|
|
|
|
user: model.User,
|
|
|
|
auth_user: model.User,
|
|
|
|
force_show_email: bool) -> Union[bool, str]:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2016-08-14 08:53:19 +00:00
|
|
|
assert auth_user
|
2016-05-30 20:07:44 +00:00
|
|
|
if not force_show_email \
|
2016-08-14 08:53:19 +00:00
|
|
|
and auth_user.user_id != user.user_id \
|
|
|
|
and not auth.has_privilege(auth_user, 'users:edit:any:email'):
|
2016-05-30 20:07:44 +00:00
|
|
|
return False
|
|
|
|
return user.email
|
2016-04-24 07:47:58 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def get_liked_post_count(
|
|
|
|
user: model.User, auth_user: model.User) -> Union[bool, int]:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2016-08-14 08:53:19 +00:00
|
|
|
assert auth_user
|
|
|
|
if auth_user.user_id != user.user_id:
|
2016-06-03 19:18:17 +00:00
|
|
|
return False
|
|
|
|
return user.liked_post_count
|
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def get_disliked_post_count(
|
|
|
|
user: model.User, auth_user: model.User) -> Union[bool, int]:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2016-08-14 08:53:19 +00:00
|
|
|
assert auth_user
|
|
|
|
if auth_user.user_id != user.user_id:
|
2016-06-03 19:18:17 +00:00
|
|
|
return False
|
|
|
|
return user.disliked_post_count
|
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
class UserSerializer(serialization.BaseSerializer):
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
user: model.User,
|
|
|
|
auth_user: model.User,
|
2017-04-24 21:30:53 +00:00
|
|
|
force_show_email: bool = False) -> None:
|
2017-02-04 00:08:12 +00:00
|
|
|
self.user = user
|
|
|
|
self.auth_user = auth_user
|
|
|
|
self.force_show_email = force_show_email
|
|
|
|
|
|
|
|
def _serializers(self) -> Dict[str, Callable[[], Any]]:
|
|
|
|
return {
|
|
|
|
'name': self.serialize_name,
|
|
|
|
'creationTime': self.serialize_creation_time,
|
|
|
|
'lastLoginTime': self.serialize_last_login_time,
|
|
|
|
'version': self.serialize_version,
|
|
|
|
'rank': self.serialize_rank,
|
|
|
|
'avatarStyle': self.serialize_avatar_style,
|
|
|
|
'avatarUrl': self.serialize_avatar_url,
|
|
|
|
'commentCount': self.serialize_comment_count,
|
|
|
|
'uploadedPostCount': self.serialize_uploaded_post_count,
|
|
|
|
'favoritePostCount': self.serialize_favorite_post_count,
|
|
|
|
'likedPostCount': self.serialize_liked_post_count,
|
|
|
|
'dislikedPostCount': self.serialize_disliked_post_count,
|
|
|
|
'email': self.serialize_email,
|
|
|
|
}
|
|
|
|
|
|
|
|
def serialize_name(self) -> Any:
|
|
|
|
return self.user.name
|
|
|
|
|
|
|
|
def serialize_creation_time(self) -> Any:
|
|
|
|
return self.user.creation_time
|
|
|
|
|
|
|
|
def serialize_last_login_time(self) -> Any:
|
|
|
|
return self.user.last_login_time
|
|
|
|
|
|
|
|
def serialize_version(self) -> Any:
|
|
|
|
return self.user.version
|
|
|
|
|
|
|
|
def serialize_rank(self) -> Any:
|
|
|
|
return self.user.rank
|
|
|
|
|
|
|
|
def serialize_avatar_style(self) -> Any:
|
|
|
|
return self.user.avatar_style
|
|
|
|
|
|
|
|
def serialize_avatar_url(self) -> Any:
|
|
|
|
return get_avatar_url(self.user)
|
|
|
|
|
|
|
|
def serialize_comment_count(self) -> Any:
|
|
|
|
return self.user.comment_count
|
|
|
|
|
|
|
|
def serialize_uploaded_post_count(self) -> Any:
|
|
|
|
return self.user.post_count
|
|
|
|
|
|
|
|
def serialize_favorite_post_count(self) -> Any:
|
|
|
|
return self.user.favorite_post_count
|
|
|
|
|
|
|
|
def serialize_liked_post_count(self) -> Any:
|
|
|
|
return get_liked_post_count(self.user, self.auth_user)
|
|
|
|
|
|
|
|
def serialize_disliked_post_count(self) -> Any:
|
|
|
|
return get_disliked_post_count(self.user, self.auth_user)
|
|
|
|
|
|
|
|
def serialize_email(self) -> Any:
|
|
|
|
return get_email(self.user, self.auth_user, self.force_show_email)
|
|
|
|
|
|
|
|
|
|
|
|
def serialize_user(
|
|
|
|
user: Optional[model.User],
|
|
|
|
auth_user: model.User,
|
2017-04-24 21:30:53 +00:00
|
|
|
options: List[str] = [],
|
|
|
|
force_show_email: bool = False) -> Optional[rest.Response]:
|
2017-02-04 00:08:12 +00:00
|
|
|
if not user:
|
|
|
|
return None
|
|
|
|
return UserSerializer(user, auth_user, force_show_email).serialize(options)
|
|
|
|
|
|
|
|
|
|
|
|
def serialize_micro_user(
|
|
|
|
user: Optional[model.User],
|
|
|
|
auth_user: model.User) -> Optional[rest.Response]:
|
2016-06-03 18:10:25 +00:00
|
|
|
return serialize_user(
|
2017-02-04 00:08:12 +00:00
|
|
|
user, auth_user=auth_user, options=['name', 'avatarUrl'])
|
2016-06-03 18:10:25 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def get_user_count() -> int:
|
|
|
|
return db.session.query(model.User).count()
|
2016-04-18 20:41:39 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def try_get_user_by_name(name: str) -> Optional[model.User]:
|
2017-04-24 21:30:53 +00:00
|
|
|
return (
|
|
|
|
db.session
|
|
|
|
.query(model.User)
|
|
|
|
.filter(sa.func.lower(model.User.name) == sa.func.lower(name))
|
|
|
|
.one_or_none())
|
2016-04-16 13:07:33 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def get_user_by_name(name: str) -> model.User:
|
2016-04-24 12:24:41 +00:00
|
|
|
user = try_get_user_by_name(name)
|
|
|
|
if not user:
|
|
|
|
raise UserNotFoundError('User %r not found.' % name)
|
|
|
|
return user
|
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def try_get_user_by_name_or_email(name_or_email: str) -> Optional[model.User]:
|
2017-02-03 20:42:15 +00:00
|
|
|
return (
|
|
|
|
db.session
|
2017-02-04 00:08:12 +00:00
|
|
|
.query(model.User)
|
2016-04-16 13:07:33 +00:00
|
|
|
.filter(
|
2017-02-04 00:08:12 +00:00
|
|
|
(sa.func.lower(model.User.name) == sa.func.lower(name_or_email)) |
|
|
|
|
(sa.func.lower(model.User.email) == sa.func.lower(name_or_email)))
|
2016-08-14 12:22:53 +00:00
|
|
|
.one_or_none())
|
|
|
|
|
2016-04-24 12:24:41 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def get_user_by_name_or_email(name_or_email: str) -> model.User:
|
2016-04-24 12:24:41 +00:00
|
|
|
user = try_get_user_by_name_or_email(name_or_email)
|
|
|
|
if not user:
|
|
|
|
raise UserNotFoundError('User %r not found.' % name_or_email)
|
|
|
|
return user
|
2016-04-16 13:07:33 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def create_user(name: str, password: str, email: str) -> model.User:
|
|
|
|
user = model.User()
|
2016-05-08 15:03:55 +00:00
|
|
|
update_user_name(user, name)
|
2016-04-30 21:53:50 +00:00
|
|
|
update_user_password(user, password)
|
|
|
|
update_user_email(user, email)
|
2016-04-18 20:41:39 +00:00
|
|
|
if get_user_count() > 0:
|
2016-05-10 09:58:55 +00:00
|
|
|
user.rank = util.flip(auth.RANK_MAP)[config.config['default_rank']]
|
2016-04-18 20:41:39 +00:00
|
|
|
else:
|
2017-02-04 00:08:12 +00:00
|
|
|
user.rank = model.User.RANK_ADMINISTRATOR
|
|
|
|
user.creation_time = datetime.utcnow()
|
|
|
|
user.avatar_style = model.User.AVATAR_GRAVATAR
|
2016-04-03 20:03:58 +00:00
|
|
|
return user
|
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def update_user_name(user: model.User, name: str) -> None:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2016-04-19 09:50:47 +00:00
|
|
|
if not name:
|
|
|
|
raise InvalidUserNameError('Name cannot be empty.')
|
2017-02-04 00:08:12 +00:00
|
|
|
if util.value_exceeds_column_size(name, model.User.name):
|
2016-04-19 09:50:47 +00:00
|
|
|
raise InvalidUserNameError('User name is too long.')
|
2016-04-03 20:03:58 +00:00
|
|
|
name = name.strip()
|
2016-04-06 18:38:45 +00:00
|
|
|
name_regex = config.config['user_name_regex']
|
2016-04-03 20:03:58 +00:00
|
|
|
if not re.match(name_regex, name):
|
2016-04-19 09:50:47 +00:00
|
|
|
raise InvalidUserNameError(
|
2016-04-16 13:07:33 +00:00
|
|
|
'User name %r must satisfy regex %r.' % (name, name_regex))
|
2016-08-14 09:50:48 +00:00
|
|
|
other_user = try_get_user_by_name(name)
|
|
|
|
if other_user and other_user.user_id != user.user_id:
|
|
|
|
raise UserAlreadyExistsError('User %r already exists.' % name)
|
|
|
|
if user.name and files.has(get_avatar_path(user.name)):
|
|
|
|
files.move(get_avatar_path(user.name), get_avatar_path(name))
|
2016-04-03 20:03:58 +00:00
|
|
|
user.name = name
|
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def update_user_password(user: model.User, password: str) -> None:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2016-04-19 09:50:47 +00:00
|
|
|
if not password:
|
|
|
|
raise InvalidPasswordError('Password cannot be empty.')
|
2016-04-06 18:38:45 +00:00
|
|
|
password_regex = config.config['password_regex']
|
2016-04-03 20:03:58 +00:00
|
|
|
if not re.match(password_regex, password):
|
2016-04-16 13:07:33 +00:00
|
|
|
raise InvalidPasswordError(
|
2016-04-03 20:03:58 +00:00
|
|
|
'Password must satisfy regex %r.' % password_regex)
|
|
|
|
user.password_salt = auth.create_password()
|
2018-02-25 05:45:00 +00:00
|
|
|
password_hash, revision = auth.get_password_hash(
|
|
|
|
user.password_salt, password)
|
|
|
|
user.password_hash = password_hash
|
|
|
|
user.password_revision = revision
|
2016-04-03 20:03:58 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def update_user_email(user: model.User, email: str) -> None:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2017-02-04 00:08:12 +00:00
|
|
|
email = email.strip()
|
|
|
|
if util.value_exceeds_column_size(email, model.User.email):
|
2016-04-16 13:07:33 +00:00
|
|
|
raise InvalidEmailError('Email is too long.')
|
2016-04-20 09:59:30 +00:00
|
|
|
if not util.is_valid_email(email):
|
2016-04-16 13:07:33 +00:00
|
|
|
raise InvalidEmailError('E-mail is invalid.')
|
2017-02-04 00:08:12 +00:00
|
|
|
user.email = email or None
|
2016-04-03 20:03:58 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def update_user_rank(
|
|
|
|
user: model.User, rank: str, auth_user: model.User) -> None:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2016-04-19 09:50:47 +00:00
|
|
|
if not rank:
|
|
|
|
raise InvalidRankError('Rank cannot be empty.')
|
2016-05-10 09:58:55 +00:00
|
|
|
rank = util.flip(auth.RANK_MAP).get(rank.strip(), None)
|
|
|
|
all_ranks = list(auth.RANK_MAP.values())
|
|
|
|
if not rank:
|
2016-04-16 13:07:33 +00:00
|
|
|
raise InvalidRankError(
|
2016-05-10 09:58:55 +00:00
|
|
|
'Rank can be either of %r.' % all_ranks)
|
2017-02-04 00:08:12 +00:00
|
|
|
if rank in (model.User.RANK_ANONYMOUS, model.User.RANK_NOBODY):
|
2016-05-10 09:58:55 +00:00
|
|
|
raise InvalidRankError('Rank %r cannot be used.' % auth.RANK_MAP[rank])
|
2016-08-14 08:53:19 +00:00
|
|
|
if all_ranks.index(auth_user.rank) \
|
2016-05-10 09:58:55 +00:00
|
|
|
< all_ranks.index(rank) and get_user_count() > 0:
|
2016-04-09 19:41:10 +00:00
|
|
|
raise errors.AuthError('Trying to set higher rank than your own.')
|
2016-04-06 17:16:44 +00:00
|
|
|
user.rank = rank
|
2016-04-03 20:03:58 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def update_user_avatar(
|
|
|
|
user: model.User,
|
|
|
|
avatar_style: str,
|
2017-04-24 21:30:53 +00:00
|
|
|
avatar_content: Optional[bytes] = None) -> None:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2016-04-09 19:41:10 +00:00
|
|
|
if avatar_style == 'gravatar':
|
|
|
|
user.avatar_style = user.AVATAR_GRAVATAR
|
|
|
|
elif avatar_style == 'manual':
|
|
|
|
user.avatar_style = user.AVATAR_MANUAL
|
2016-06-19 17:43:23 +00:00
|
|
|
avatar_path = 'avatars/' + user.name.lower() + '.png'
|
2016-04-09 19:41:10 +00:00
|
|
|
if not avatar_content:
|
2016-06-19 17:43:23 +00:00
|
|
|
if files.has(avatar_path):
|
|
|
|
return
|
2016-04-16 13:07:33 +00:00
|
|
|
raise InvalidAvatarError('Avatar content missing.')
|
2016-04-09 19:41:10 +00:00
|
|
|
image = images.Image(avatar_content)
|
|
|
|
image.resize_fill(
|
|
|
|
int(config.config['thumbnails']['avatar_width']),
|
|
|
|
int(config.config['thumbnails']['avatar_height']))
|
2016-06-19 17:43:23 +00:00
|
|
|
files.save(avatar_path, image.to_png())
|
2016-04-09 19:41:10 +00:00
|
|
|
else:
|
2016-04-16 13:07:33 +00:00
|
|
|
raise InvalidAvatarError(
|
|
|
|
'Avatar style %r is invalid. Valid avatar styles: %r.' % (
|
|
|
|
avatar_style, ['gravatar', 'manual']))
|
2016-04-09 19:41:10 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def bump_user_login_time(user: model.User) -> None:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2017-02-04 00:08:12 +00:00
|
|
|
user.last_login_time = datetime.utcnow()
|
2016-04-03 20:03:58 +00:00
|
|
|
|
2016-08-14 12:22:53 +00:00
|
|
|
|
2017-02-04 00:08:12 +00:00
|
|
|
def reset_user_password(user: model.User) -> str:
|
2016-08-14 08:45:00 +00:00
|
|
|
assert user
|
2016-04-03 20:03:58 +00:00
|
|
|
password = auth.create_password()
|
|
|
|
user.password_salt = auth.create_password()
|
2018-02-25 05:45:00 +00:00
|
|
|
password_hash, revision = auth.get_password_hash(
|
|
|
|
user.password_salt, password)
|
|
|
|
user.password_hash = password_hash
|
|
|
|
user.password_revision = revision
|
2016-04-03 20:03:58 +00:00
|
|
|
return password
|