<file_sep>from peewee import *
import json


class ObjectField(Field):
    """
    Поле-объект для базы данных (через JSON)
    """
    field_type = 'object'

    def db_value(self, value):
        return json.dumps(value)

    def python_value(self, value):
        return json.loads(value)


db = MySQLDatabase('volunteer', user='miron', password='<PASSWORD>', host='localhost', charset='utf8mb4', field_types={"object":"text"})


class BaseModel(Model):
    class Meta:
        database = db


class Photo(BaseModel):
    id = PrimaryKeyField()
    path = CharField()
    private = BooleanField()  # Можно ли получить по ссылке всем или только супер-админам


class Museum(BaseModel):
    id = PrimaryKeyField()
    name = CharField()
    description = TextField()
    login = CharField()
    password = CharField()
    photo = ForeignKeyField(Photo, null=True, backref="museum_photos", default=None)


class User(BaseModel):
    """
    Волонтеры
    """
    user_id = IntegerField(primary_key=True)

    name = CharField()  # Имя
    s_name = CharField()  # Фамилия
    f_name = CharField()  # Отчество
    b_date = DateField()  # Дата рождения
    r_date = DateField()  # Дата регистрации

    museum = ForeignKeyField(Museum, null=True, backref='volunteers', default=None)  # Музей, к которому прикреплен Волонтер

    verified = BooleanField(default=False)  # Подтвержден ли его аккаунт. В этом случае у него будет галочка у профиля. После
                                            # изменения личной инфы галочка спадает

    photo = ForeignKeyField(Photo, null=True, backref="user_photos", default=None)
    mail = CharField(unique=True, max_length=191)
    phone = CharField(unique=True, max_length=191)
    cloth_size = CharField(default="", max_length=8)  # Размер одежды, примеры: ...XXS, XS, S, M , L, XL, XXL, XXXL...

    subscribe = BooleanField(default=False)  # Подписка на mail-рассылку

    additional_study = TextField(default="")  # Образование
    additional_child_skill = TextField(default="")  # Работа с детьми
    additional_talent = TextField(default="")  # Особые таланты
    additional_allergy = TextField(default="")  # Аллергии
    # ... ... ...


class Verification(BaseModel):  # Temp.unused
    """
    Запросы на верификацию
    """
    id = PrimaryKeyField()
    user = ForeignKeyField(User, backref='verifications', unique=True)
    document = ForeignKeyField(Photo, backref="documents")  # Ссылка в файловой системе на загруженный паспорт


class Manager(BaseModel):
    """
    Расширенная система менеджмента:
    |- Админ-музей
        |- Менеджеры

    """
    user_id = IntegerField(primary_key=True)  # VK-User ID
    name = CharField()
    s_name = CharField()
    password = CharField()
    photo = ForeignKeyField(Photo, backref="manager_photos")

    manager_vk = CharField(default='')  # Ссылка на профиль ВКонтакте
    manager_phone = CharField(default='')  # Мобильный для связи

    museum = ForeignKeyField(Museum, backref='managers')  # Музей, к которому прикреплен Менеджер


class Event(BaseModel):
    """
    Мероприятие
    """
    id = PrimaryKeyField()
    name = CharField()
    description = TextField()
    manager = ObjectField()  # ForeignKeyField(Manager, backref='events')
    start_datetime = DateTimeField()  # Начало
    finish_datetime = DateTimeField()  # Окончание
    photo = ForeignKeyField(Photo, null=True, backref="event_photos", default=None)
    state = IntegerField()  # Состояние мероприятия: INIT -> ONGOING -> FINISHED
    price = IntegerField()  # Цена текущего мероприятия

    # Доп. фильтры в следующих обновлениях


class RoleForEvent(BaseModel):
    """
    Роль в мероприятии
    """
    id = PrimaryKeyField()
    name = CharField()  # Заголовок роли
    event = ForeignKeyField(Event, backref='roles')
    manager = ObjectField()  # ForeignKeyField(Manager, backref='roles')
    max_slots = IntegerField()  # Число слотов для этой роли
    message_when_participated = TextField()  # Сообщение либо с ссылкой на беседу, либо с доп. информацией по поводу того, как будет проходить волонтерство по этой роли
    vk_chat_link = CharField()  # Ссылка на беседу ВК, указывается при создании/редактировании


class Participation(BaseModel):  # При создании участия проверять на наличие участия в том же мероприятии. Через volunteer.roles => set(role.event...)
    """
    Участие в роли
    """
    participation_key = CharField(primary_key=True, max_length=191)  # Ключ участия
    role = ForeignKeyField(RoleForEvent, backref='participations')
    participant = ForeignKeyField(User, backref='participations')
    result = IntegerField(default=-1)  # Состояния: BAD, NEUTRAL, GOOD. Множитель для мерояприятия: -1, 1, 2
    checked = IntegerField(default=0)  # Состояние выполненной работы (0 - не пришел/не выполнил; 1 - только вошел, не вышел с мероприятия; 2 - успешно вышел/выполнил задание)
    comment_to_volunteer = TextField(default='')
    comment_from_volunteer = TextField(default='')


db.connect()
db.create_tables([Museum, User, Verification, Manager, Event, RoleForEvent, Participation, Photo])
db.close()


<file_sep># -*- coding: utf-8 -*-
import datetime
from random import randint, choice

from flask import *
from flask_session import Session
from flask_restful import Api, Resource, reqparse, abort
from db_model import *
import inspect
import sign_valid  # Подтверждение валидности УРЛя для парса
import constants
import os
import uuid

app = Flask(__name__)
app.secret_key = b"<KEY>"
app.config['SESSION_TYPE'] = 'filesystem'
api = Api(app)
Session(app)


@app.before_request
def _db_connect():
	db.connect()


@app.teardown_request
def _db_close(exc):
	if not db.is_closed():
		db.close()


# --------------------------------------

def participant_list_by_museum(museum_id):
	"""
	Получить всех участников, присодиненных к мероприятиям музея
	:param museum_id: ИД музея
	"""
	ptcpnts = []
	museum = get_raw_museum(museum_id)
	for event in museum.events:
		if event.state == constants.event_state.value_to_db("ONGOING"):
			for role in event.roles:
				for participation in role.participations:
					ptcpnts.append(parse_user(participation.participant))
	return ptcpnts


def get_raw_museum(museum_id):
	"""
	Получить информацию о музее (по ИД)
	:param museum_id: ИД музея
	:return: False или объект с музеем
	"""
	try:
		museum = Museum.get(Museum.id == museum_id)
		return museum
	except Museum.DoesNotExist:
		return False


def parse_role(role: RoleForEvent):
	"""
	Получить информацию о роли (по объекту БД)
	:param role: объект БД
	"""
	return {
		"name": role.name,
		"manager": role.manager,
		"max_slots": role.max_slots,
		"free_slots": (role.max_slots - len(role.participations)),
		"vk_chat_link": role.vk_chat_link
	}


def get_role(role_id):
	"""
	Получить информацию о роли (по ИД)
	:param role_id: ИД роли
	:return: False или объект с ролью
	"""
	try:
		role = RoleForEvent.get(RoleForEvent.id == role_id)
		return parse_role(role)
	except RoleForEvent.DoesNotExist:
		return False


def get_roles(event_id):
	"""
	Получить все роли текущего мероприятия
	:param event_id: ИД мероприятия
	"""
	event = get_raw_event(event_id)
	all_roles = []
	for role in event.roles:
		all_roles.append(parse_role(role))
	return all_roles


def get_raw_role(role_id)->RoleForEvent:
	"""
	Получить объект БД роли (по ИД)
	:param role_id: ИД роли
	:return: role_id или объект БД
	"""
	try:
		role = RoleForEvent.get(User.user_id == role_id)
		return role
	except User.DoesNotExist:
		return role_id


def create_participation(user_id, role_id):
	"""
	Создать новое участие для пользователя
	:param user_id: ID пользователя
	:param role_id: ID роли
	:return: распарсенный объект участия или объект с SUCCESS:FALSE и MESSAGE
	"""
	user = get_raw_user(user_id)
	role = get_raw_role(role_id)

	if role.max_slots - len(role.participations) <= 0:
		return {"success": False, "message": "slots out of range"}
	else:
		participation_data = {
			Participation.participation_key.name: str(uuid.uuid4()),  # Уникальный идентификатор участия
			Participation.role.name: role,
			Participation.participant.name: user,
		}

		new_participation = Participation.create(**participation_data)
		return parse_participation(new_participation)


def parse_event(event:Event):
	"""
	Получить информацию о мероприятии (из объекта БД)
	:param event: объект из БД
	"""
	return {
		"name":event.name,
		"description":event.description,
		"manager": event.manager,
		"start_datetime": event.start_datetime.strftime("%H:%M:%S %d/%m/%Y"),
		"finish_datetime": event.finish_datetime.strftime("%H:%M:%S %d/%m/%Y"),
		"photo": event.photo.id,
		"state":constants.event_state.db_to_value(event.state),
		"price":event.price
	}


def get_event(event_id):
	"""
	Получить информацию о мероприятии (по ИД)
	:param event_id: ИД мероприятия
	"""
	try:
		event = Event.get(Event.id == event_id)
		return parse_event(event)
	except Event.DoesNotExist:
		return False


def get_raw_event(event_id):
	"""
	Получить объект БД мероприятия (по ИД)
	:param event_id: ИД мероприятия
	:return: объект БД или ID мероприятия (если такого нет в БД)
	"""
	try:
		event = Event.get(Event.id == event_id)
		return event
	except Event.DoesNotExist:
		return event_id


def get_events():
	"""
	Получить все меропрития
	"""
	all_events = []
	for event in Event.select():
		all_events.append(parse_user(event))
	return all_events


def parse_museum(museum):
	"""
	Получить информацию о музее (из объекта БД)
	:param museum: объект из БД
	"""
	return {
		"id": museum.id,
		"name": museum.name,
		"description": museum.description,
		"photo": museum.photo.id
	}


def get_museum(museum_id):
	"""
	Получить информацию о музее (по ИД)
	:param museum_id: ИД музея
	:return: False или профиль музея
	"""
	try:
		museum = Museum.get(Museum.id == museum_id)
		return parse_museum(museum)
	except Museum.DoesNotExist:
		return False


def parse_participation(participation:Participation):
	"""
	Получить информацию о участии (по объекту БД)
	:param participation: объект БД
	"""
	return {
		"participation_key":participation.participant,
		"role": parse_role(participation.role),
		"result": constants.participation_result.db_to_value(participation.result),
		"checked": constants.participation_checked.db_to_value(participation.checked),
		"comment_to_volunteer": participation.comment_to_volunteer,
		"comment_from_volunteer": participation.comment_from_volunteer
	}


def get_raw_participation(participation_key):
	"""
	Получить информацию об участии (по ключу)
	:param participation_key: ключ участия
	:return: False или участие
	"""
	try:
		participation = Participation.get(Participation.participation_key == participation_key)
		return participation
	except Participation.DoesNotExist:
		return False


def get_participations(user):
	"""
	Получить все "участия" человека и всю инфу о них
	:param user: БД объект пользователя
	"""
	data = []
	for participation in user.participations:
		if participation.role.event.state != constants.event_state.value_to_db("FINISHED"):
			data = parse_participation(participation)
	return data


def get_experience(user):
	"""
	Посчитадть текущий опыт
	:param user: юзер, у которого ведем подсчет
	"""
	sum = 0
	participants = []
	for participation in user.participations:
		if participation.role.event.state == constants.event_state.value_to_db("FINISHED"):
			sum += participation.role.price * participation.result
			participants.append(parse_participation(participation))
	return sum


def parse_user(user):
	"""
	Получить информацию о пользователе (из объекта БД)
	:param user: объект из БД
	"""
	return {
		"user_id": user.user_id,
		"name": user.name,
		"b_date": user.b_date.strftime("%d/%m/%Y"),
		"r_date": user.r_date.strftime("%d/%m/%Y"),
		"experience": get_experience(user),
		"museum": get_museum(user.museum.id) if user.museum is not None else None,
		"verified": user.verified,
		"photo": user.photo.id if user.photo is not None else None,
		"mail": user.mail,
		"phone": user.phone,
		"cloth_size": user.cloth_size,
		"subscribe": user.subscribe,
		"additional_study": user.additional_study,
		"additional_child_skill": user.additional_child_skill,
		"additional_talent": user.additional_talent,
		"additional_allergy": user.additional_allergy
	}


def get_user(user_id):
	"""
	Получить информацию о пользователе (по ИД)
	:param user_id: ИД пользователя
	:return: False или профиль пользователя
	"""
	try:
		user = User.get(User.user_id == user_id)
		return parse_user(user)
	except User.DoesNotExist:
		return False


def get_raw_user(user_id)->any:
	"""
	Получить объект БД пользователя (по ИД)
	:param user_id: ИД пользователя
	:return: False или объект БД
	"""
	try:
		user = User.get(User.user_id == user_id)
		return user
	except User.DoesNotExist:
		return False


def get_photo(photo_id):
	"""
	Получить бинарные данные фотографии по ID
	:param photo_id: ID фотографии
	"""
	try:
		photo = Photo.get(Photo.id == photo_id)
		with open(photo.path, mode="rb") as f:
			content = f.read()
			return content
	except Photo.DoesNotExist:
		return None


def create_file(private=False):
	"""
	Создание пустого файла и нода в базе данных
	:param private: приватный файл - недоступен для просмотра через функцию (для верификации)
	"""
	today = datetime.datetime.today()
	year = str(today.year)
	month = str(today.month)
	day = str(today.day)
	hour = str(today.hour)

	path = os.path.join("", year, month, day, hour)
	if not os.path.exists(path):
		os.makedirs(path)
	file = path + "/" + str(uuid.uuid4())

	# Создаем новый файл
	new_file = Photo.create(**{
		Photo.path.name: file,
		Photo.private.name: private
	})
	return file, new_file.id

# --------------------------------------


class Route:
	"""
	Подкласс для создания роутов - путей в ЮРЛЬ
	"""
	__subtrace__ = ""


def create_resource(cls, url: str):
	res = cls.__class__()
	app.route(url, methods=["GET"])(res.get)
	app.route(url, methods=["POST"])(res.post)


class UserAuthParam(object):
	parse_args = []  # аргументы для парсинга

	def __init__(self, *parse_args):
		self.parse_args = [{"name": "Referer", "type": str, "required": True, "help": "missed url", "location": "headers"}]
		self.parse_args += parse_args

	def __call__(self, target):
		def wrapped(*args, **kwargs):
			register_parser = reqparse.RequestParser()
			for arg in self.parse_args:
				register_parser.add_argument(**arg)
			new_args = register_parser.parse_args()
			sign = sign_valid.valid_user_id(new_args['Referer'])

			if not sign["valid"]:
				abort(constants.HTTP_ERROR_AUTH_STATUS, messages=[constants.HTTP_ERROR_AUTH_MESSAGE])

			args += sign["user_id"],
			kwargs = {**kwargs, **new_args}

			result = target(*args, **kwargs)
			return result

		return wrapped


class AdminAuthParam(object):
	parse_args = []  # аргументы для парсинга

	def __init__(self, *parse_args):
		self.parse_args = [{"name": "X-admin-login", "type": str, "required": True, "help": "missed login", "location": "headers"},
		                   {"name": "X-admin-password", "type": str, "required": True, "help": "missed password", "location": "headers"}]
		self.parse_args += parse_args

	def __call__(self, target):
		def wrapped(*args, **kwargs):
			register_parser = reqparse.RequestParser()
			for arg in self.parse_args:
				register_parser.add_argument(**arg)
			new_args = register_parser.parse_args()

			museum = None
			try:
				museum = Museum.get((Museum.login == new_args['X-admin-login']) & (Museum.login == new_args['X-admin-password']))
			except Museum.DoesNotExist:
				pass

			if museum is not None:
				abort(constants.HTTP_ERROR_AUTH_STATUS, messages=[constants.HTTP_ERROR_AUTH_MESSAGE])

			args += museum.id,
			kwargs = {**kwargs, **new_args}

			result = target(*args, **kwargs)
			return result

		return wrapped


def process_methods(cls, suburl):
	for inner in cls.__dict__.values():
		if type(inner) is set:
			print(suburl, inner)


def route(url, start_point=False):
	def process_resources(cls):
		trace = cls.__subtrace__
		for inner in cls.__dict__.values():
			if inspect.isclass(inner):
				if issubclass(inner, Resource):
					process_methods(inner, url + trace + inner.__subtrace__)
					api.add_resource(inner, url + trace + inner.__subtrace__)
				if issubclass(inner, Route):
					inner.__subtrace__ = trace + inner.__subtrace__
					process_resources(inner)

	def decorator(cls):
		if start_point:
			if issubclass(cls, Resource):
				api.add_resource(cls, url)
			if issubclass(cls, Route):
				process_resources(cls)
		else:
			cls.__subtrace__ = url

		return cls

	return decorator

# --------------------------------------

class Photo:
	@route("/<int:photo_id>")
	class GetCertainImage(Resource):
		@api.representation('image/jpeg')
		def get(self, photo_id, user):
			data = None
			try:
				photo = Photo.get(Photo.id == photo_id)
				if not photo.private:
					data = get_photo(photo_id)
			except Photo.DoesNotExist:
				pass

			resp = make_response(data, 200)
			resp.headers['Cache-Control'] = 'public, max-age=31536000'
			return resp

	def post(self):
		data = request.data
		file, file_id = create_file()
		# Записываем все в файл
		with open(file, mode="bw") as f:
			f.write(data)
		return {"fileId": file_id}

	@route("/private")
	class UploadPrivateImage(Resource):
		def post(self):
			data = request.data
			file, file_id = create_file(True)
			# Записываем все в файл
			with open(file, mode="bw") as f:
				f.write(data)
			return {"fileId": file_id}

# --------------------------------------


@route("/volunteer", True)
class Volunteer(Route):

	@route("/users")
	class User(Route):
		@route("/current")
		class Current(Resource, Route):
			@UserAuthParam({"name": "name", "type": str, "required": True, "help": "missed name", "location": "json"},
			               {"name": "s_name", "type": str, "required": True, "help": "missed surname", "location": "json"},
			               {"name": "f_name", "type": str, "required": True, "help": "missed father name", "location": "json"},
			               {"name": "b_date", "type": str, "required": True, "help": "missed birthday date", "location": "json"},
			               {"name": "photo", "type": int, "help": "missed photo id", "location": "json"},
			               {"name": "mail", "type": str, "required": True, "help": "missed mail", "location": "json"},
			               {"name": "phone", "type": str, "required": True, "help": "missed phone", "location": "json"},
			               {"name": "cloth_size", "type": str, "help": "missed cloth_size", "location": "json"},
			               {"name": "subscribe", "type": bool, "required": True, "help": "missed subscribe", "location": "json"},
			               {"name": "additional_study", "type": str,"help": "missed additional_study", "location": "json"},
			               {"name": "additional_child_skill", "type": str, "help": "missed additional_child_skill", "location": "json"},
			               {"name": "additional_talent", "type": str, "help": "missed additional_talent", "location": "json"},
			               {"name": "additional_allergy", "type": str, "help": "missed additional_allergy", "location": "json"})
			def post(self, user_id, **kwargs):  # Обновить информацию о пользователе
				user = get_raw_user(user_id)

				if user is False:
					user_data = {
						User.user_id.name: user_id,
						User.name.name: kwargs["name"],
						User.s_name.name: kwargs["s_name"],
						User.f_name.name: kwargs["f_name"],
						User.subscribe.name: kwargs["subscribe"],
						User.b_date.name: datetime.datetime.strptime(kwargs["b_date"],"%d/%m/%Y").date(),
						User.r_date.name: datetime.datetime.today().date(),
						User.mail.name: kwargs["mail"],
						User.phone.name: kwargs["phone"]
					}

					user = User.create(**user_data)
				else:
					for key, value in kwargs:
						user.__setattr__(key, value)
					user.save()
				return parse_user(user)

			@UserAuthParam()
			def get(self, user_id):  # Получить информацию и пользователе
				return {"user": get_user(user_id)}

			@route("/participations")
			class Participations(Resource):
				@UserAuthParam()
				def get(self, user_id):  # Получить список активных/ожидающих участий пользователя
					user = get_user(user_id)
					return get_participations(user)

		@route("/<int:user_id>")
		class User(Resource):
			@UserAuthParam()
			def get(self, user_id, current_user_id):  # [DEBUG] Получить информацию о юзере по ID
				return {"user": get_user(user_id)}

		@route("/login")
		class Register(Resource):
			@UserAuthParam()
			def get(self, user_id, headers=None, **kwargs):  # Проверить наличие пользователя в БД / "авторизоваться"
				user = get_raw_user(user_id)
				return {"success": True, "user": parse_user(user)} if user is not False else {"success": False}

	@route("/photo")
	class UserPhotoLoad(Photo):
		pass

	@route("/events")
	class Event(Resource, Route):
		@UserAuthParam()
		def get(self):  # Список всех мероприятий
			return get_events()

		@route("/<int:event_id>")
		class GetCertainEvent(Resource, Route):
			@UserAuthParam()
			def get(self, event_id):  # Конкретное мероприятие
				return get_event(event_id)

			@route("/roles")
			class RolesForEvent(Resource, Route):
				@UserAuthParam()
				def get(self, event_id):  # Роли для конкретного мероприятия
					return get_roles(event_id)

				@route("/<int:role_id>")
				class GetCertainRole(Resource, Route):
					@UserAuthParam()
					def get(self, role_id):  # Отдельная роль
						return get_role(role_id)

					@route("/participate")
					class Participate(Resource):
						@UserAuthParam()
						def get(self, role_id, event_id, user_id):  # Принять участие в роли
							return create_participation(user_id, role_id)


# Точка входа для работы с участиями. Отметка входа/выхода, проставление оценки, отправка комментария. Пользователь + менеджер
@route("/participations", True)
class Participation(Resource, Route):

	@route("/hire")
	class Hire(Resource):
		@AdminAuthParam({"name": "qr", "type": str, "required": True, "help": "missed qr", "location": "json"})
		def post(self, museum_id, **kwargs):  # Впустить волонтера
			participation = get_raw_participation(kwargs["qr"])
			if participation.role.event.state == constants.event_state.to_db("ONGOING"):
				if participation.checked == 0:
					participation.checked = constants.participation_checked.to_db("PARTIALLY")
					participation.save()
					return {"success":True}
				else:
					return {"success":False, "message":"Пользователь уже прошел на мероприятие"}
			else:
				return {"success":False, "message":"Мероприятие не проводится в данный момент"}

	@route("/free")
	class Free(Resource):
		def post(self, museum_id):  # Выпустить волонтера
			pass

	@route("/list")
	class HandMode(Resource, Route):
		@AdminAuthParam()
		def get(self, museum_id):  # Получить список всех участников для текущего музея
			return participant_list_by_museum(museum_id)

		@route("/<int:user_id>")
		class GetCertainUser(Route):
			@route("/free")
			class FreeSelected(Resource):
				@AdminAuthParam()
				def post(self, museum_id):  # Проставить участие удаленно / без контакта с участником
					pass

# Точка входа для работы с менеджерами. Обновление информации: имя, фамилия, фото, ВК, телефон, пароль. Создание менеджеров, удаление менеджеров
# class Manager:

# Точка входа для работы с музеями. Авторизация, смена информации: название, описание, смена фотографии, создание. Администрация + музеи
# class Museum:
<file_sep># VKHT_VolunteerBack
Backend системы упрощения работы с волонтерами для музеев, написанной в рамках VkHackathon2019

# app.py
Файл приложения для Flask

# constants.py
Константы (сообщение об ошибке, значения для БД)

# db_model.py
Модель базы данных

# main.py
Основной файл с инициализацией, реализацией REST-Апи и функциями доступа к объектам из БД

# sign_valid.py
Верификация запросов с фронта
<file_sep>HTTP_ERROR_AUTH_STATUS = 403
HTTP_ERROR_AUTH_MESSAGE = "wrong sign"


# ------------

class Converter:
	db_to_value = {}  # Словарь перевода Базы данных в пользовательский
	value_to_db = {}

	def __init__(self, dictionary):
		self.db_to_value = dictionary
		self.value_to_db = dict((v, k) for k, v in dictionary.items())

	def to_db(self, value):
		"""
		Семантическое значение в значение для базы данных
		:param value: сементически понятное значение
		"""
		return self.value_to_db.get(value)

	def to_value(self, db):
		"""
		Значение из базы данных в семантически понятную единицу
		:param db: значение из БД
		"""
		return self.db_to_value(db)

	def get_values(self):
		"""
		Список возможные сементических значений
		"""
		return self.db_to_value.values()

	def get_dbs(self):
		"""
		Список возможных значений в базе данных
		"""
		return self.value_to_db.values()


event_state = Converter({0: "INIT", 1: "ONGOING", 2: "FINISHED"})  # Состояния мероприятия
participation_result = Converter({-1: "BAD", 1: "NEUTRAL", 2: "GOOD"})  # Результаты участия / множители экспы
participation_checked = Converter({0: "NONE", 1: "PARTIALLY", 2: "DONE"})  # Отметки в процессе мероприятия
<file_sep>from base64 import b64encode
from collections import OrderedDict
from hashlib import sha256
from hmac import HMAC
from urllib.parse import urlparse, parse_qsl, urlencode

client_secret = "<KEY>"  # "<KEY>"  # Защищённый ключ


def valid_user_id(url) -> dict:
	"""
	Проверить валидность УРЛь и выдать пользовательский ID
	:param url: Урль
	:return: словарь с ключами valid и user_id
	"""
	global client_secret
	query = dict(parse_qsl(urlparse(url).query, keep_blank_values=True))
	secret = client_secret
	valid, user_id = False, -1
	try:
		vk_subset = OrderedDict(sorted(x for x in query.items() if x[0][:3] == "vk_"))
		hash_code = b64encode(HMAC(secret.encode(), urlencode(vk_subset, doseq=True).encode(), sha256).digest())
		decoded_hash_code = hash_code.decode('utf-8')[:-1].replace('+', '-').replace('/', '_')
		valid = query["sign"] == decoded_hash_code
		user_id = query["vk_user_id"]
	except:
		pass

	return {"valid": valid, "user_id": user_id}
