Compare commits
15 Commits
fddfa5df0b
...
059fc0d966
Author | SHA1 | Date | |
---|---|---|---|
059fc0d966 | |||
f8f06f8b68 | |||
e74a7461fa | |||
22ebf35382 | |||
2398ef5cf9 | |||
c0a5f32060 | |||
849ff0a1e9 | |||
f3b3ce3d57 | |||
032e737ab9 | |||
1428c83050 | |||
6ad9105c82 | |||
c060a6fc41 | |||
48db95ff6e | |||
ddb723ab88 | |||
933e601572 |
@ -1,4 +1,4 @@
|
||||
pipeline:
|
||||
steps:
|
||||
buildweb:
|
||||
group: build
|
||||
image: node
|
||||
@ -7,6 +7,7 @@ pipeline:
|
||||
environment:
|
||||
- VITE_API_URL=https://api.treadl.com
|
||||
- VITE_IMAGINARY_URL=https://images.treadl.com
|
||||
- VITE_SENTRY_DSN=https://7c88f77dd19c57bfb92bb9eb53e33c4b@o4508066290532352.ingest.de.sentry.io/4508075022090320
|
||||
- VITE_SOURCE_REPO_URL=https://git.wilw.dev/wilw/treadl
|
||||
- VITE_PATREON_URL=https://www.patreon.com/treadl
|
||||
- VITE_KOFI_URL=https://ko-fi.com/wilw88
|
||||
@ -43,5 +44,6 @@ pipeline:
|
||||
- s3cmd --configure --access_key=$LINODE_ACCESS_KEY --secret_key=$LINODE_SECRET_ACCESS_KEY --host=https://eu-central-1.linodeobjects.com --host-bucket="%(bucket)s.eu-central-1.linodeobjects.com" --dump-config > /root/.s3cfg
|
||||
- s3cmd -c /root/.s3cfg sync --no-mime-magic --guess-mime-type dist/* s3://treadl.com
|
||||
- 'curl -X POST -H "AccessKey: $BUNNY_KEY" https://api.bunny.net/pullzone/782753/purgeCache'
|
||||
|
||||
branches: main
|
||||
|
||||
when:
|
||||
branch: main
|
||||
|
@ -1,37 +1,72 @@
|
||||
import datetime, jwt, bcrypt, re, os
|
||||
import datetime
|
||||
import jwt
|
||||
import bcrypt
|
||||
import re
|
||||
import os
|
||||
from bson.objectid import ObjectId
|
||||
from util import database, mail, util
|
||||
from api import uploads
|
||||
|
||||
jwt_secret = os.environ['JWT_SECRET']
|
||||
jwt_secret = os.environ["JWT_SECRET"]
|
||||
MIN_PASSWORD_LENGTH = 8
|
||||
|
||||
def register(username, email, password, how_find_us):
|
||||
if not username or len(username) < 4 or not email or len(email) < 6:
|
||||
raise util.errors.BadRequest('Your username or email is too short or invalid.')
|
||||
username = username.lower()
|
||||
email = email.lower()
|
||||
if not re.match("^[a-z0-9_]+$", username):
|
||||
raise util.errors.BadRequest('Usernames can only contain letters, numbers, and underscores')
|
||||
if not password or len(password) < MIN_PASSWORD_LENGTH:
|
||||
raise util.errors.BadRequest('Your password should be at least {0} characters.'.format(MIN_PASSWORD_LENGTH))
|
||||
db = database.get_db()
|
||||
existingUser = db.users.find_one({'$or': [{'username': username}, {'email': email}]})
|
||||
if existingUser:
|
||||
raise util.errors.BadRequest('An account with this username or email already exists.')
|
||||
|
||||
try:
|
||||
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||
result = db.users.insert_one({ 'username': username, 'email': email, 'password': hashed_password, 'createdAt': datetime.datetime.now(), 'subscriptions': {'email': ['groups.invited', 'groups.joinRequested', 'groups.joined', 'messages.replied', 'projects.commented']}})
|
||||
mail.send({
|
||||
'to': os.environ.get('ADMIN_EMAIL'),
|
||||
'subject': '{} signup'.format(os.environ.get('APP_NAME')),
|
||||
'text': 'A new user signed up with username {0} and email {1}, discovered from {2}'.format(username, email, how_find_us)
|
||||
})
|
||||
mail.send({
|
||||
'to': email,
|
||||
'subject': 'Welcome to {}!'.format(os.environ.get('APP_NAME')),
|
||||
'text': '''Dear {0},
|
||||
def register(username, email, password, how_find_us):
|
||||
if not username or len(username) < 4 or not email or len(email) < 6:
|
||||
raise util.errors.BadRequest("Your username or email is too short or invalid.")
|
||||
username = username.lower()
|
||||
email = email.lower()
|
||||
if not re.match("^[a-z0-9_]+$", username):
|
||||
raise util.errors.BadRequest(
|
||||
"Usernames can only contain letters, numbers, and underscores"
|
||||
)
|
||||
if not password or len(password) < MIN_PASSWORD_LENGTH:
|
||||
raise util.errors.BadRequest(
|
||||
"Your password should be at least {0} characters.".format(
|
||||
MIN_PASSWORD_LENGTH
|
||||
)
|
||||
)
|
||||
db = database.get_db()
|
||||
existingUser = db.users.find_one(
|
||||
{"$or": [{"username": username}, {"email": email}]}
|
||||
)
|
||||
if existingUser:
|
||||
raise util.errors.BadRequest(
|
||||
"An account with this username or email already exists."
|
||||
)
|
||||
|
||||
try:
|
||||
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||
result = db.users.insert_one(
|
||||
{
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": hashed_password,
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"subscriptions": {
|
||||
"email": [
|
||||
"groups.invited",
|
||||
"groups.joinRequested",
|
||||
"groups.joined",
|
||||
"messages.replied",
|
||||
"projects.commented",
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
mail.send(
|
||||
{
|
||||
"to": os.environ.get("ADMIN_EMAIL"),
|
||||
"subject": "{} signup".format(os.environ.get("APP_NAME")),
|
||||
"text": "A new user signed up with username {0} and email {1}, discovered from {2}".format(
|
||||
username, email, how_find_us
|
||||
),
|
||||
}
|
||||
)
|
||||
mail.send(
|
||||
{
|
||||
"to": email,
|
||||
"subject": "Welcome to {}!".format(os.environ.get("APP_NAME")),
|
||||
"text": """Dear {0},
|
||||
|
||||
Welcome to {3}! We won't send you many emails but we just want to introduce ourselves and to give you some tips to help you get started.
|
||||
|
||||
@ -61,157 +96,226 @@ We hope you enjoy using {3} and if you have any comments or feedback please tell
|
||||
Best wishes,
|
||||
|
||||
The {3} Team
|
||||
'''.format(
|
||||
username,
|
||||
os.environ.get('APP_URL'),
|
||||
os.environ.get('CONTACT_EMAIL'),
|
||||
os.environ.get('APP_NAME'),
|
||||
)})
|
||||
return {'token': generate_access_token(result.inserted_id)}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise util.errors.BadRequest('Unable to register your account. Please try again later')
|
||||
""".format(
|
||||
username,
|
||||
os.environ.get("APP_URL"),
|
||||
os.environ.get("CONTACT_EMAIL"),
|
||||
os.environ.get("APP_NAME"),
|
||||
),
|
||||
}
|
||||
)
|
||||
return {"token": generate_access_token(result.inserted_id)}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise util.errors.BadRequest(
|
||||
"Unable to register your account. Please try again later"
|
||||
)
|
||||
|
||||
|
||||
def login(email, password):
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({'$or': [{'username': email.lower()}, {'email': email.lower()}]})
|
||||
try:
|
||||
if user and bcrypt.checkpw(password.encode("utf-8"), user['password']):
|
||||
return {'token': generate_access_token(user['_id'])}
|
||||
else:
|
||||
raise util.errors.BadRequest('Your username or password is incorrect.')
|
||||
except Exception as e:
|
||||
raise util.errors.BadRequest('Your username or password is incorrect.')
|
||||
db = database.get_db()
|
||||
user = db.users.find_one(
|
||||
{"$or": [{"username": email.lower()}, {"email": email.lower()}]}
|
||||
)
|
||||
try:
|
||||
if user and bcrypt.checkpw(password.encode("utf-8"), user["password"]):
|
||||
return {"token": generate_access_token(user["_id"])}
|
||||
else:
|
||||
raise util.errors.BadRequest("Your username or password is incorrect.")
|
||||
except Exception:
|
||||
raise util.errors.BadRequest("Your username or password is incorrect.")
|
||||
|
||||
|
||||
def logout(user):
|
||||
db = database.get_db()
|
||||
db.users.update_one({'_id': user['_id']}, {'$pull': {'tokens.login': user['currentToken']}})
|
||||
return {'loggedOut': True}
|
||||
db = database.get_db()
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]}, {"$pull": {"tokens.login": user["currentToken"]}}
|
||||
)
|
||||
return {"loggedOut": True}
|
||||
|
||||
|
||||
def update_email(user, data):
|
||||
if not data: raise util.errors.BadRequest('Invalid request')
|
||||
if 'email' not in data: raise util.errors.BadRequest('Invalid request')
|
||||
if len(data['email']) < 4: raise util.errors.BadRequest('New email is too short')
|
||||
db = database.get_db()
|
||||
db.users.update_one({'_id': user['_id']}, {'$set': {'email': data['email']}})
|
||||
mail.send({
|
||||
'to': user['email'],
|
||||
'subject': 'Your email address has changed on {}'.format(os.environ.get('APP_NAME')),
|
||||
'text': 'Dear {0},\n\nThis email is to let you know that we recently received a request to change your account email address on {2}. We have now made this change.\n\nThe new email address for your account is {1}.\n\nIf you think this is a mistake then please get in touch with us as soon as possible.'.format(
|
||||
user['username'],
|
||||
data['email'],
|
||||
os.environ.get('APP_NAME'),
|
||||
if not data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
if "email" not in data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
if len(data["email"]) < 4:
|
||||
raise util.errors.BadRequest("New email is too short")
|
||||
db = database.get_db()
|
||||
db.users.update_one({"_id": user["_id"]}, {"$set": {"email": data["email"]}})
|
||||
mail.send(
|
||||
{
|
||||
"to": user["email"],
|
||||
"subject": "Your email address has changed on {}".format(
|
||||
os.environ.get("APP_NAME")
|
||||
),
|
||||
"text": "Dear {0},\n\nThis email is to let you know that we recently received a request to change your account email address on {2}. We have now made this change.\n\nThe new email address for your account is {1}.\n\nIf you think this is a mistake then please get in touch with us as soon as possible.".format(
|
||||
user["username"],
|
||||
data["email"],
|
||||
os.environ.get("APP_NAME"),
|
||||
),
|
||||
}
|
||||
)
|
||||
})
|
||||
mail.send({
|
||||
'to': data['email'],
|
||||
'subject': 'Your email address has changed on {}'.format(os.environ.get('APP_NAME')),
|
||||
'text': 'Dear {0},\n\nThis email is to let you know that we recently received a request to change your account email address on {2}. We have now made this change.\n\nThe new email address for your account is {1}.\n\nIf you think this is a mistake then please get in touch with us as soon as possible.'.format(
|
||||
user['username'],
|
||||
data['email'],
|
||||
os.environ.get('APP_NAME'),
|
||||
mail.send(
|
||||
{
|
||||
"to": data["email"],
|
||||
"subject": "Your email address has changed on {}".format(
|
||||
os.environ.get("APP_NAME")
|
||||
),
|
||||
"text": "Dear {0},\n\nThis email is to let you know that we recently received a request to change your account email address on {2}. We have now made this change.\n\nThe new email address for your account is {1}.\n\nIf you think this is a mistake then please get in touch with us as soon as possible.".format(
|
||||
user["username"],
|
||||
data["email"],
|
||||
os.environ.get("APP_NAME"),
|
||||
),
|
||||
}
|
||||
)
|
||||
})
|
||||
return {'email': data['email']}
|
||||
return {"email": data["email"]}
|
||||
|
||||
|
||||
def update_password(user, data):
|
||||
if not data: raise util.errors.BadRequest('Invalid request')
|
||||
if 'newPassword' not in data: raise util.errors.BadRequest('Invalid request')
|
||||
if len(data['newPassword']) < MIN_PASSWORD_LENGTH: raise util.errors.BadRequest('New password should be at least {0} characters long'.format(MIN_PASSWORD_LENGTH))
|
||||
if not data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
if "newPassword" not in data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
if len(data["newPassword"]) < MIN_PASSWORD_LENGTH:
|
||||
raise util.errors.BadRequest(
|
||||
"New password should be at least {0} characters long".format(
|
||||
MIN_PASSWORD_LENGTH
|
||||
)
|
||||
)
|
||||
|
||||
db = database.get_db()
|
||||
if 'currentPassword' in data:
|
||||
if not user: raise util.errors.BadRequest('User context is required')
|
||||
if not bcrypt.checkpw(data['currentPassword'].encode('utf-8'), user['password']):
|
||||
raise util.errors.BadRequest('Incorrect password')
|
||||
elif 'token' in data:
|
||||
try:
|
||||
id = jwt.decode(data['token'], jwt_secret, algorithms='HS256')['sub']
|
||||
user = db.users.find_one({'_id': ObjectId(id), 'tokens.passwordReset': data['token']})
|
||||
if not user: raise Exception
|
||||
except Exception as e:
|
||||
raise util.errors.BadRequest('There was a problem updating your password. Your token may be invalid or out of date')
|
||||
else:
|
||||
raise util.errors.BadRequest('Current password or reset token is required')
|
||||
if not user: raise util.errors.BadRequest('Unable to change your password')
|
||||
db = database.get_db()
|
||||
if "currentPassword" in data:
|
||||
if not user:
|
||||
raise util.errors.BadRequest("User context is required")
|
||||
if not bcrypt.checkpw(
|
||||
data["currentPassword"].encode("utf-8"), user["password"]
|
||||
):
|
||||
raise util.errors.BadRequest("Incorrect password")
|
||||
elif "token" in data:
|
||||
try:
|
||||
id = jwt.decode(data["token"], jwt_secret, algorithms="HS256")["sub"]
|
||||
user = db.users.find_one(
|
||||
{"_id": ObjectId(id), "tokens.passwordReset": data["token"]}
|
||||
)
|
||||
if not user:
|
||||
raise Exception
|
||||
except Exception:
|
||||
raise util.errors.BadRequest(
|
||||
"There was a problem updating your password. Your token may be invalid or out of date"
|
||||
)
|
||||
else:
|
||||
raise util.errors.BadRequest("Current password or reset token is required")
|
||||
if not user:
|
||||
raise util.errors.BadRequest("Unable to change your password")
|
||||
|
||||
hashed_password = bcrypt.hashpw(data['newPassword'].encode("utf-8"), bcrypt.gensalt())
|
||||
db.users.update_one({'_id': user['_id']}, {'$set': {'password': hashed_password}, '$unset': {'tokens.passwordReset': ''}})
|
||||
|
||||
mail.send({
|
||||
'to_user': user,
|
||||
'subject': 'Your {} password has changed'.format(os.environ.get('APP_NAME')),
|
||||
'text': 'Dear {0},\n\nThis email is to let you know that we recently received a request to change your account password on {1}. We have now made this change.\n\nIf you think this is a mistake then please login to change your password as soon as possible.'.format(
|
||||
user['username'],
|
||||
os.environ.get('APP_NAME'),
|
||||
hashed_password = bcrypt.hashpw(
|
||||
data["newPassword"].encode("utf-8"), bcrypt.gensalt()
|
||||
)
|
||||
})
|
||||
return {'passwordUpdated': True}
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]},
|
||||
{"$set": {"password": hashed_password}, "$unset": {"tokens.passwordReset": ""}},
|
||||
)
|
||||
|
||||
mail.send(
|
||||
{
|
||||
"to_user": user,
|
||||
"subject": "Your {} password has changed".format(
|
||||
os.environ.get("APP_NAME")
|
||||
),
|
||||
"text": "Dear {0},\n\nThis email is to let you know that we recently received a request to change your account password on {1}. We have now made this change.\n\nIf you think this is a mistake then please login to change your password as soon as possible.".format(
|
||||
user["username"],
|
||||
os.environ.get("APP_NAME"),
|
||||
),
|
||||
}
|
||||
)
|
||||
return {"passwordUpdated": True}
|
||||
|
||||
|
||||
def delete(user, password):
|
||||
if not password or not bcrypt.checkpw(password.encode('utf-8'), user['password']):
|
||||
raise util.errors.BadRequest('Incorrect password')
|
||||
db = database.get_db()
|
||||
for project in db.projects.find({'user': user['_id']}):
|
||||
db.objects.delete_many({'project': project['_id']})
|
||||
db.projects.delete_one({'_id': project['_id']})
|
||||
db.comments.delete_many({'user': user['_id']})
|
||||
db.users.update_many({'following.user': user['_id']}, {'$pull': {'following': {'user': user['_id']}}})
|
||||
db.users.delete_one({'_id': user['_id']})
|
||||
return {'deletedUser': user['_id']}
|
||||
if not password or not bcrypt.checkpw(password.encode("utf-8"), user["password"]):
|
||||
raise util.errors.BadRequest("Incorrect password")
|
||||
db = database.get_db()
|
||||
for project in db.projects.find({"user": user["_id"]}):
|
||||
db.objects.delete_many({"project": project["_id"]})
|
||||
db.projects.delete_one({"_id": project["_id"]})
|
||||
db.comments.delete_many({"user": user["_id"]})
|
||||
db.users.update_many(
|
||||
{"following.user": user["_id"]}, {"$pull": {"following": {"user": user["_id"]}}}
|
||||
)
|
||||
db.users.delete_one({"_id": user["_id"]})
|
||||
return {"deletedUser": user["_id"]}
|
||||
|
||||
|
||||
def generate_access_token(user_id):
|
||||
payload = {
|
||||
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=30),
|
||||
'iat': datetime.datetime.utcnow(),
|
||||
'sub': str(user_id)
|
||||
}
|
||||
token = jwt.encode(payload, jwt_secret, algorithm='HS256')
|
||||
db = database.get_db()
|
||||
db.users.update_one({'_id': user_id}, {'$addToSet': {'tokens.login': token}})
|
||||
return token
|
||||
payload = {
|
||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(days=30),
|
||||
"iat": datetime.datetime.utcnow(),
|
||||
"sub": str(user_id),
|
||||
}
|
||||
token = jwt.encode(payload, jwt_secret, algorithm="HS256")
|
||||
db = database.get_db()
|
||||
db.users.update_one({"_id": user_id}, {"$addToSet": {"tokens.login": token}})
|
||||
return token
|
||||
|
||||
|
||||
def get_user_context(token):
|
||||
if not token: return None
|
||||
try:
|
||||
payload = jwt.decode(token, jwt_secret, algorithms='HS256')
|
||||
id = payload['sub']
|
||||
if id:
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({'_id': ObjectId(id), 'tokens.login': token})
|
||||
db.users.update_one({'_id': user['_id']}, {'$set': {'lastSeenAt': datetime.datetime.now()}})
|
||||
user['currentToken'] = token
|
||||
return user
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
payload = jwt.decode(token, jwt_secret, algorithms="HS256")
|
||||
id = payload["sub"]
|
||||
if id:
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({"_id": ObjectId(id), "tokens.login": token})
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]}, {"$set": {"lastSeenAt": datetime.datetime.now()}}
|
||||
)
|
||||
user["currentToken"] = token
|
||||
return user
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
|
||||
def reset_password(data):
|
||||
if not data or not 'email' in data: raise util.errors.BadRequest('Invalid request')
|
||||
if len(data['email']) < 5: raise util.errors.BadRequest('Your email is too short')
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({'email': data['email'].lower()})
|
||||
if user:
|
||||
payload = {
|
||||
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1),
|
||||
'iat': datetime.datetime.utcnow(),
|
||||
'sub': str(user['_id'])
|
||||
}
|
||||
token = jwt.encode(payload, jwt_secret, algorithm='HS256')
|
||||
mail.send({
|
||||
'to_user': user,
|
||||
'subject': 'Reset your password',
|
||||
'text': 'Dear {0},\n\nA password reset email was recently requested for your {2} account. If this was you and you want to continue, please follow the link below:\n\n{1}\n\nThis link will expire after 24 hours.\n\nIf this was not you, then someone may be trying to gain access to your account. We recommend using a strong and unique password for your account.'.format(
|
||||
user['username'],
|
||||
'{}/password/reset?token={}'.format(os.environ.get('APP_URL'), token),
|
||||
os.environ.get('APP_NAME'),
|
||||
)
|
||||
})
|
||||
db.users.update_one({'_id': user['_id']}, {'$set': {'tokens.passwordReset': token}})
|
||||
return {'passwordResetEmailSent': True}
|
||||
if not data or "email" not in data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
if len(data["email"]) < 5:
|
||||
raise util.errors.BadRequest("Your email is too short")
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({"email": data["email"].lower()})
|
||||
if user:
|
||||
payload = {
|
||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(days=1),
|
||||
"iat": datetime.datetime.utcnow(),
|
||||
"sub": str(user["_id"]),
|
||||
}
|
||||
token = jwt.encode(payload, jwt_secret, algorithm="HS256")
|
||||
mail.send(
|
||||
{
|
||||
"to_user": user,
|
||||
"subject": "Reset your password",
|
||||
"text": "Dear {0},\n\nA password reset email was recently requested for your {2} account. If this was you and you want to continue, please follow the link below:\n\n{1}\n\nThis link will expire after 24 hours.\n\nIf this was not you, then someone may be trying to gain access to your account. We recommend using a strong and unique password for your account.".format(
|
||||
user["username"],
|
||||
"{}/password/reset?token={}".format(
|
||||
os.environ.get("APP_URL"), token
|
||||
),
|
||||
os.environ.get("APP_NAME"),
|
||||
),
|
||||
}
|
||||
)
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]}, {"$set": {"tokens.passwordReset": token}}
|
||||
)
|
||||
return {"passwordResetEmailSent": True}
|
||||
|
||||
|
||||
def update_push_token(user, data):
|
||||
if not data or 'pushToken' not in data: raise util.errors.BadRequest('Push token is required')
|
||||
db = database.get_db()
|
||||
db.users.update_one({'_id': user['_id']}, {'$set': {'pushToken': data['pushToken']}})
|
||||
return {'addedPushToken': data['pushToken']}
|
||||
if not data or "pushToken" not in data:
|
||||
raise util.errors.BadRequest("Push token is required")
|
||||
db = database.get_db()
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]}, {"$set": {"pushToken": data["pushToken"]}}
|
||||
)
|
||||
return {"addedPushToken": data["pushToken"]}
|
||||
|
@ -1,165 +1,190 @@
|
||||
import os, re
|
||||
import os
|
||||
import re
|
||||
from util import database, util
|
||||
from api import uploads
|
||||
|
||||
DOMAIN = os.environ.get('APP_DOMAIN')
|
||||
DOMAIN = os.environ.get("APP_DOMAIN")
|
||||
|
||||
|
||||
def webfinger(resource):
|
||||
if not resource: raise util.errors.BadRequest('Resource required')
|
||||
resource = resource.lower()
|
||||
exp = re.compile('acct:([a-z0-9_-]+)@([a-z0-9_\-\.]+)', re.IGNORECASE)
|
||||
matches = exp.findall(resource)
|
||||
if not matches or not matches[0]: raise util.errors.BadRequest('Resource invalid')
|
||||
username, host = matches[0]
|
||||
if not username or not host: raise util.errors.BadRequest('Resource invalid')
|
||||
if host != DOMAIN: raise util.errors.NotFound('Host unknown')
|
||||
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({'username': username})
|
||||
if not user: raise util.errors.NotFound('User unknown')
|
||||
if not resource:
|
||||
raise util.errors.BadRequest("Resource required")
|
||||
resource = resource.lower()
|
||||
exp = re.compile("acct:([a-z0-9_-]+)@([a-z0-9_\-\.]+)", re.IGNORECASE)
|
||||
matches = exp.findall(resource)
|
||||
if not matches or not matches[0]:
|
||||
raise util.errors.BadRequest("Resource invalid")
|
||||
username, host = matches[0]
|
||||
if not username or not host:
|
||||
raise util.errors.BadRequest("Resource invalid")
|
||||
if host != DOMAIN:
|
||||
raise util.errors.NotFound("Host unknown")
|
||||
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({"username": username})
|
||||
if not user:
|
||||
raise util.errors.NotFound("User unknown")
|
||||
|
||||
return {
|
||||
"subject": resource,
|
||||
"aliases": [
|
||||
"https://{}/{}".format(DOMAIN, username),
|
||||
"https://{}/u/{}".format(DOMAIN, username),
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"type": "text/html",
|
||||
"href": "https://{}/{}".format(DOMAIN, username),
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "https://{}/u/{}".format(DOMAIN, username),
|
||||
},
|
||||
{
|
||||
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||
"template": "https://{}/authorize_interaction".format(DOMAIN)
|
||||
+ "?uri={uri}",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return {
|
||||
"subject": resource,
|
||||
"aliases": [
|
||||
"https://{}/{}".format(DOMAIN, username),
|
||||
"https://{}/u/{}".format(DOMAIN, username)
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"type": "text/html",
|
||||
"href": "https://{}/{}".format(DOMAIN, username)
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "https://{}/u/{}".format(DOMAIN, username)
|
||||
},
|
||||
{
|
||||
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||
"template": "https://{}/authorize_interaction".format(DOMAIN) + "?uri={uri}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def user(username):
|
||||
if not username: raise util.errors.BadRequest('Username required')
|
||||
username = username.lower()
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({'username': username})
|
||||
if not user: raise util.errors.NotFound('User unknown')
|
||||
avatar_url = user.get('avatar') and uploads.get_presigned_url('users/{0}/{1}'.format(user['_id'], user['avatar']))
|
||||
if not username:
|
||||
raise util.errors.BadRequest("Username required")
|
||||
username = username.lower()
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({"username": username})
|
||||
if not user:
|
||||
raise util.errors.NotFound("User unknown")
|
||||
avatar_url = user.get("avatar") and uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(user["_id"], user["avatar"])
|
||||
)
|
||||
|
||||
pub_key = None
|
||||
if user.get('services', {}).get('activityPub', {}).get('publicKey'):
|
||||
pub_key = user['services']['activityPub']['publicKey']
|
||||
else:
|
||||
priv_key, pub_key = util.generate_rsa_keypair()
|
||||
db.users.update_one({'_id': user['_id']}, {'$set': {
|
||||
'services.activityPub.publicKey': pub_key,
|
||||
'services.activityPub.privateKey': priv_key,
|
||||
}})
|
||||
pub_key = None
|
||||
if user.get("services", {}).get("activityPub", {}).get("publicKey"):
|
||||
pub_key = user["services"]["activityPub"]["publicKey"]
|
||||
else:
|
||||
priv_key, pub_key = util.generate_rsa_keypair()
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]},
|
||||
{
|
||||
"$set": {
|
||||
"services.activityPub.publicKey": pub_key,
|
||||
"services.activityPub.privateKey": priv_key,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
resp = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
],
|
||||
"id": "https://{}/u/{}".format(DOMAIN, username),
|
||||
"type": "Person",
|
||||
#"following": "https://fosstodon.org/users/wilw/following",
|
||||
#"followers": "https://fosstodon.org/users/wilw/followers",
|
||||
"inbox": "https://{}/inbox".format(DOMAIN),
|
||||
"outbox": "https://{}/u/{}/outbox".format(DOMAIN, username),
|
||||
"preferredUsername": username,
|
||||
"name": username,
|
||||
"summary": user.get('bio', ''),
|
||||
"url": "https://{}/{}".format(DOMAIN, username),
|
||||
"discoverable": True,
|
||||
"published": "2021-01-27T00:00:00Z",
|
||||
"publicKey": {
|
||||
"id": "https://{}/u/{}#main-key".format(DOMAIN, username),
|
||||
"owner": "https://{}/u/{}".format(DOMAIN, username),
|
||||
"publicKeyPem": pub_key.decode('utf-8')
|
||||
},
|
||||
"attachment": [],
|
||||
"endpoints": {
|
||||
"sharedInbox": "https://{}/inbox".format(DOMAIN)
|
||||
},
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
"url": avatar_url
|
||||
},
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
"url": avatar_url
|
||||
resp = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
],
|
||||
"id": "https://{}/u/{}".format(DOMAIN, username),
|
||||
"type": "Person",
|
||||
# "following": "https://fosstodon.org/users/wilw/following",
|
||||
# "followers": "https://fosstodon.org/users/wilw/followers",
|
||||
"inbox": "https://{}/inbox".format(DOMAIN),
|
||||
"outbox": "https://{}/u/{}/outbox".format(DOMAIN, username),
|
||||
"preferredUsername": username,
|
||||
"name": username,
|
||||
"summary": user.get("bio", ""),
|
||||
"url": "https://{}/{}".format(DOMAIN, username),
|
||||
"discoverable": True,
|
||||
"published": "2021-01-27T00:00:00Z",
|
||||
"publicKey": {
|
||||
"id": "https://{}/u/{}#main-key".format(DOMAIN, username),
|
||||
"owner": "https://{}/u/{}".format(DOMAIN, username),
|
||||
"publicKeyPem": pub_key.decode("utf-8"),
|
||||
},
|
||||
"attachment": [],
|
||||
"endpoints": {"sharedInbox": "https://{}/inbox".format(DOMAIN)},
|
||||
"icon": {"type": "Image", "mediaType": "image/jpeg", "url": avatar_url},
|
||||
"image": {"type": "Image", "mediaType": "image/jpeg", "url": avatar_url},
|
||||
}
|
||||
}
|
||||
|
||||
if user.get('website'):
|
||||
resp['attachment'].append({
|
||||
"type": "PropertyValue",
|
||||
"name": "Website",
|
||||
"value": "<a href=\"https://{}\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span><span class=\"\">{}</span><span class=\"invisible\"></span></a>".format(user['website'], user['website'])
|
||||
})
|
||||
if user.get("website"):
|
||||
resp["attachment"].append(
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "Website",
|
||||
"value": '<a href="https://{}" target="_blank" rel="nofollow noopener noreferrer me"><span class="invisible">https://</span><span class="">{}</span><span class="invisible"></span></a>'.format(
|
||||
user["website"], user["website"]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
return resp
|
||||
|
||||
def outbox(username, page, min_id, max_id):
|
||||
if not username: raise util.errors.BadRequest('Username required')
|
||||
username = username.lower()
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({'username': username})
|
||||
if not user: raise util.errors.NotFound('User unknown')
|
||||
if not username:
|
||||
raise util.errors.BadRequest("Username required")
|
||||
username = username.lower()
|
||||
db = database.get_db()
|
||||
user = db.users.find_one({"username": username})
|
||||
if not user:
|
||||
raise util.errors.NotFound("User unknown")
|
||||
|
||||
if not page or page != 'true':
|
||||
return {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://{}/u/{}/outbox".format(DOMAIN, username),
|
||||
"type": "OrderedCollection",
|
||||
"first": "https://{}/u/{}/outbox?page=true".format(DOMAIN, username)
|
||||
}
|
||||
if page == 'true':
|
||||
min_string = '&min_id={}'.format(min_id) if min_id else ''
|
||||
max_string = '&max_id={}'.format(max_id) if max_id else ''
|
||||
ret = {
|
||||
"id": "https://{}/u/{}/outbox?page=true{}{}".format(DOMAIN, username, min_string, max_string),
|
||||
"type": "OrderedCollectionPage",
|
||||
#"next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
|
||||
#"prev": "https://example.org/users/whatever/outbox?min_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
|
||||
"partOf": "https://{}/u/{}/outbox".format(DOMAIN, username),
|
||||
"orderedItems": []
|
||||
}
|
||||
|
||||
project_list = list(db.projects.find({'user': user['_id'], 'visibility': 'public'}))
|
||||
for p in project_list:
|
||||
ret['orderedItems'].append({
|
||||
"id": "https://{}/{}/{}/activity".format(DOMAIN, username, p['path']),
|
||||
"type": "Create",
|
||||
"actor": "https://{}/u/{}".format(DOMAIN, username),
|
||||
"published": p['createdAt'].strftime("%Y-%m-%dT%H:%M:%SZ"),#"2021-10-18T20:06:18Z",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"object": {
|
||||
"id": "https://{}/{}/{}".format(DOMAIN, username, p['path']),
|
||||
"type": "Note",
|
||||
"summary": None,
|
||||
#"inReplyTo": "https://mastodon.lhin.space/users/0xvms/statuses/108759565436297722",
|
||||
"published": p['createdAt'].strftime("%Y-%m-%dT%H:%M:%SZ"),#"2022-08-03T15:43:30Z",
|
||||
"url": "https://{}/{}/{}".format(DOMAIN, username, p['path']),
|
||||
"attributedTo": "https://{}/u/{}".format(DOMAIN, username),
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"cc": [
|
||||
"https://{}/u/{}/followers".format(DOMAIN, username),
|
||||
],
|
||||
"sensitive": False,
|
||||
"content": "{} created a project: {}".format(username, p['name']),
|
||||
if not page or page != "true":
|
||||
return {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://{}/u/{}/outbox".format(DOMAIN, username),
|
||||
"type": "OrderedCollection",
|
||||
"first": "https://{}/u/{}/outbox?page=true".format(DOMAIN, username),
|
||||
}
|
||||
})
|
||||
|
||||
return ret
|
||||
if page == "true":
|
||||
min_string = "&min_id={}".format(min_id) if min_id else ""
|
||||
max_string = "&max_id={}".format(max_id) if max_id else ""
|
||||
ret = {
|
||||
"id": "https://{}/u/{}/outbox?page=true{}{}".format(
|
||||
DOMAIN, username, min_string, max_string
|
||||
),
|
||||
"type": "OrderedCollectionPage",
|
||||
# "next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
|
||||
# "prev": "https://example.org/users/whatever/outbox?min_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
|
||||
"partOf": "https://{}/u/{}/outbox".format(DOMAIN, username),
|
||||
"orderedItems": [],
|
||||
}
|
||||
|
||||
project_list = list(
|
||||
db.projects.find({"user": user["_id"], "visibility": "public"})
|
||||
)
|
||||
for p in project_list:
|
||||
ret["orderedItems"].append(
|
||||
{
|
||||
"id": "https://{}/{}/{}/activity".format(
|
||||
DOMAIN, username, p["path"]
|
||||
),
|
||||
"type": "Create",
|
||||
"actor": "https://{}/u/{}".format(DOMAIN, username),
|
||||
"published": p["createdAt"].strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
), # "2021-10-18T20:06:18Z",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"object": {
|
||||
"id": "https://{}/{}/{}".format(DOMAIN, username, p["path"]),
|
||||
"type": "Note",
|
||||
"summary": None,
|
||||
# "inReplyTo": "https://mastodon.lhin.space/users/0xvms/statuses/108759565436297722",
|
||||
"published": p["createdAt"].strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
), # "2022-08-03T15:43:30Z",
|
||||
"url": "https://{}/{}/{}".format(DOMAIN, username, p["path"]),
|
||||
"attributedTo": "https://{}/u/{}".format(DOMAIN, username),
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc": [
|
||||
"https://{}/u/{}/followers".format(DOMAIN, username),
|
||||
],
|
||||
"sensitive": False,
|
||||
"content": "{} created a project: {}".format(
|
||||
username, p["name"]
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return ret
|
||||
|
@ -1,268 +1,419 @@
|
||||
import datetime, re, os
|
||||
import datetime
|
||||
import re
|
||||
import os
|
||||
import pymongo
|
||||
from bson.objectid import ObjectId
|
||||
from util import database, util, mail, push
|
||||
from api import uploads
|
||||
|
||||
APP_NAME = os.environ.get('APP_NAME')
|
||||
APP_URL = os.environ.get('APP_URL')
|
||||
APP_NAME = os.environ.get("APP_NAME")
|
||||
APP_URL = os.environ.get("APP_URL")
|
||||
|
||||
|
||||
def create(user, data):
|
||||
if not data: raise util.errors.BadRequest('Invalid request')
|
||||
if len(data.get('name')) < 3: raise util.errors.BadRequest('A longer name is required')
|
||||
db = database.get_db()
|
||||
if not data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
if len(data.get("name")) < 3:
|
||||
raise util.errors.BadRequest("A longer name is required")
|
||||
db = database.get_db()
|
||||
|
||||
group = {
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"user": user["_id"],
|
||||
"admins": [user["_id"]],
|
||||
"name": data["name"],
|
||||
"description": data.get("description", ""),
|
||||
"closed": data.get("closed", False),
|
||||
}
|
||||
result = db.groups.insert_one(group)
|
||||
group["_id"] = result.inserted_id
|
||||
create_member(user, group["_id"], user["_id"])
|
||||
return group
|
||||
|
||||
group = {
|
||||
'createdAt': datetime.datetime.now(),
|
||||
'user': user['_id'],
|
||||
'admins': [user['_id']],
|
||||
'name': data['name'],
|
||||
'description': data.get('description', ''),
|
||||
'closed': data.get('closed', False),
|
||||
}
|
||||
result = db.groups.insert_one(group)
|
||||
group['_id'] = result.inserted_id
|
||||
create_member(user, group['_id'], user['_id'])
|
||||
return group
|
||||
|
||||
def get(user):
|
||||
db = database.get_db()
|
||||
groups = list(db.groups.find({'_id': {'$in': user.get('groups', [])}}))
|
||||
return {'groups': groups}
|
||||
db = database.get_db()
|
||||
groups = list(db.groups.find({"_id": {"$in": user.get("groups", [])}}))
|
||||
return {"groups": groups}
|
||||
|
||||
|
||||
def get_one(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({'_id': id})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
group['adminUsers'] = list(db.users.find({'_id': {'$in': group.get('admins', [])}}, {'username': 1, 'avatar': 1}))
|
||||
for u in group['adminUsers']:
|
||||
if 'avatar' in u:
|
||||
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar']))
|
||||
return group
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
group["adminUsers"] = list(
|
||||
db.users.find(
|
||||
{"_id": {"$in": group.get("admins", [])}}, {"username": 1, "avatar": 1}
|
||||
)
|
||||
)
|
||||
for u in group["adminUsers"]:
|
||||
if "avatar" in u:
|
||||
u["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(u["_id"], u["avatar"])
|
||||
)
|
||||
return group
|
||||
|
||||
|
||||
def update(user, id, update):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You\'re not a group admin')
|
||||
allowed_keys = ['name', 'description', 'closed']
|
||||
updater = util.build_updater(update, allowed_keys)
|
||||
if updater: db.groups.update_one({'_id': id}, updater)
|
||||
return get_one(user, id)
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("You're not a group admin")
|
||||
allowed_keys = ["name", "description", "closed"]
|
||||
updater = util.build_updater(update, allowed_keys)
|
||||
if updater:
|
||||
db.groups.update_one({"_id": id}, updater)
|
||||
return get_one(user, id)
|
||||
|
||||
|
||||
def delete(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You\'re not a group admin')
|
||||
db.groups.delete_one({'_id': id})
|
||||
db.groupEntries.delete_many({'group': id})
|
||||
db.users.update_many({'groups': id}, {'$pull': {'groups': id}})
|
||||
return {'deletedGroup': id}
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("You're not a group admin")
|
||||
db.groups.delete_one({"_id": id})
|
||||
db.groupEntries.delete_many({"group": id})
|
||||
db.users.update_many({"groups": id}, {"$pull": {"groups": id}})
|
||||
return {"deletedGroup": id}
|
||||
|
||||
|
||||
def create_entry(user, id, data):
|
||||
if not data or 'content' not in data: raise util.errors.BadRequest('Invalid request')
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1, 'name': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if group['_id'] not in user.get('groups', []): raise util.errors.Forbidden('You must be a member to write in the feed')
|
||||
entry = {
|
||||
'createdAt': datetime.datetime.now(),
|
||||
'group': id,
|
||||
'user': user['_id'],
|
||||
'content': data['content'],
|
||||
}
|
||||
if 'attachments' in data:
|
||||
entry['attachments'] = data['attachments']
|
||||
for attachment in entry['attachments']:
|
||||
if re.search(r'(.jpg)|(.png)|(.jpeg)|(.gif)$', attachment['storedName'].lower()):
|
||||
attachment['isImage'] = True
|
||||
if attachment['type'] == 'file':
|
||||
attachment['url'] = uploads.get_presigned_url('groups/{0}/{1}'.format(id, attachment['storedName']))
|
||||
if not data or "content" not in data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1, "name": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if group["_id"] not in user.get("groups", []):
|
||||
raise util.errors.Forbidden("You must be a member to write in the feed")
|
||||
entry = {
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"group": id,
|
||||
"user": user["_id"],
|
||||
"content": data["content"],
|
||||
}
|
||||
if "attachments" in data:
|
||||
entry["attachments"] = data["attachments"]
|
||||
for attachment in entry["attachments"]:
|
||||
if re.search(
|
||||
r"(.jpg)|(.png)|(.jpeg)|(.gif)$", attachment["storedName"].lower()
|
||||
):
|
||||
attachment["isImage"] = True
|
||||
if attachment["type"] == "file":
|
||||
attachment["url"] = uploads.get_presigned_url(
|
||||
"groups/{0}/{1}".format(id, attachment["storedName"])
|
||||
)
|
||||
|
||||
result = db.groupEntries.insert_one(entry)
|
||||
entry['_id'] = result.inserted_id
|
||||
entry['authorUser'] = {'_id': user['_id'], 'username': user['username'], 'avatar': user.get('avatar')}
|
||||
if 'avatar' in user:
|
||||
entry['authorUser']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(user['_id'], user['avatar']))
|
||||
result = db.groupEntries.insert_one(entry)
|
||||
entry["_id"] = result.inserted_id
|
||||
entry["authorUser"] = {
|
||||
"_id": user["_id"],
|
||||
"username": user["username"],
|
||||
"avatar": user.get("avatar"),
|
||||
}
|
||||
if "avatar" in user:
|
||||
entry["authorUser"]["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(user["_id"], user["avatar"])
|
||||
)
|
||||
|
||||
for u in db.users.find(
|
||||
{
|
||||
"_id": {"$ne": user["_id"]},
|
||||
"groups": id,
|
||||
"subscriptions.email": "groupFeed-" + str(id),
|
||||
},
|
||||
{"email": 1, "username": 1},
|
||||
):
|
||||
mail.send(
|
||||
{
|
||||
"to_user": u,
|
||||
"subject": "New message in " + group["name"],
|
||||
"text": "Dear {0},\n\n{1} posted a message in the Notice Board of {2} on {5}:\n\n{3}\n\nFollow the link below to visit the group:\n\n{4}".format(
|
||||
u["username"],
|
||||
user["username"],
|
||||
group["name"],
|
||||
data["content"],
|
||||
"{}/groups/{}".format(APP_URL, str(id)),
|
||||
APP_NAME,
|
||||
),
|
||||
}
|
||||
)
|
||||
push.send_multiple(
|
||||
list(db.users.find({"_id": {"$ne": user["_id"]}, "groups": id})),
|
||||
"{} posted in {}".format(user["username"], group["name"]),
|
||||
data["content"][:30] + "...",
|
||||
)
|
||||
return entry
|
||||
|
||||
for u in db.users.find({'_id': {'$ne': user['_id']}, 'groups': id, 'subscriptions.email': 'groupFeed-' + str(id)}, {'email': 1, 'username': 1}):
|
||||
mail.send({
|
||||
'to_user': u,
|
||||
'subject': 'New message in ' + group['name'],
|
||||
'text': 'Dear {0},\n\n{1} posted a message in the Notice Board of {2} on {5}:\n\n{3}\n\nFollow the link below to visit the group:\n\n{4}'.format(
|
||||
u['username'],
|
||||
user['username'],
|
||||
group['name'],
|
||||
data['content'],
|
||||
'{}/groups/{}'.format(APP_URL, str(id)),
|
||||
APP_NAME,
|
||||
)
|
||||
})
|
||||
push.send_multiple(list(db.users.find({'_id': {'$ne': user['_id']}, 'groups': id})), '{} posted in {}'.format(user['username'], group['name']), data['content'][:30] + '...')
|
||||
return entry
|
||||
|
||||
def get_entries(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if id not in user.get('groups', []): raise util.errors.BadRequest('You\'re not a member of this group')
|
||||
entries = list(db.groupEntries.find({'group': id}).sort('createdAt', pymongo.DESCENDING))
|
||||
authors = list(db.users.find({'_id': {'$in': [e['user'] for e in entries]}}, {'username': 1, 'avatar': 1}))
|
||||
for entry in entries:
|
||||
if 'attachments' in entry:
|
||||
for attachment in entry['attachments']:
|
||||
attachment['url'] = uploads.get_presigned_url('groups/{0}/{1}'.format(id, attachment['storedName']))
|
||||
for author in authors:
|
||||
if entry['user'] == author['_id']:
|
||||
entry['authorUser'] = author
|
||||
if 'avatar' in author:
|
||||
entry['authorUser']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(author['_id'], author['avatar']))
|
||||
return {'entries': entries}
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if id not in user.get("groups", []):
|
||||
raise util.errors.BadRequest("You're not a member of this group")
|
||||
entries = list(
|
||||
db.groupEntries.find({"group": id}).sort("createdAt", pymongo.DESCENDING)
|
||||
)
|
||||
authors = list(
|
||||
db.users.find(
|
||||
{"_id": {"$in": [e["user"] for e in entries]}}, {"username": 1, "avatar": 1}
|
||||
)
|
||||
)
|
||||
for entry in entries:
|
||||
if "attachments" in entry:
|
||||
for attachment in entry["attachments"]:
|
||||
attachment["url"] = uploads.get_presigned_url(
|
||||
"groups/{0}/{1}".format(id, attachment["storedName"])
|
||||
)
|
||||
for author in authors:
|
||||
if entry["user"] == author["_id"]:
|
||||
entry["authorUser"] = author
|
||||
if "avatar" in author:
|
||||
entry["authorUser"]["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(author["_id"], author["avatar"])
|
||||
)
|
||||
return {"entries": entries}
|
||||
|
||||
|
||||
def delete_entry(user, id, entry_id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
entry_id = ObjectId(entry_id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
entry = db.groupEntries.find_one(entry_id, {'user': 1, 'group': 1})
|
||||
if not entry or entry['group'] != id: raise util.errors.NotFound('Entry not found')
|
||||
if entry['user'] != user['_id'] and user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You must own the entry or be an admin of the group')
|
||||
db.groupEntries.delete_one({'$or': [{'_id': entry_id}, {'inReplyTo': entry_id}]})
|
||||
return {'deletedEntry': entry_id}
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
entry_id = ObjectId(entry_id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
entry = db.groupEntries.find_one(entry_id, {"user": 1, "group": 1})
|
||||
if not entry or entry["group"] != id:
|
||||
raise util.errors.NotFound("Entry not found")
|
||||
if entry["user"] != user["_id"] and user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden(
|
||||
"You must own the entry or be an admin of the group"
|
||||
)
|
||||
db.groupEntries.delete_one({"$or": [{"_id": entry_id}, {"inReplyTo": entry_id}]})
|
||||
return {"deletedEntry": entry_id}
|
||||
|
||||
|
||||
def create_entry_reply(user, id, entry_id, data):
|
||||
if not data or 'content' not in data: raise util.errors.BadRequest('Invalid request')
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
entry_id = ObjectId(entry_id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1, 'name': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
entry = db.groupEntries.find_one({'_id': entry_id})
|
||||
if not entry or entry.get('group') != group['_id']: raise util.errors.NotFound('Entry to reply to not found')
|
||||
if group['_id'] not in user.get('groups', []): raise util.errors.Forbidden('You must be a member to write in the feed')
|
||||
reply = {
|
||||
'createdAt': datetime.datetime.now(),
|
||||
'group': id,
|
||||
'inReplyTo': entry_id,
|
||||
'user': user['_id'],
|
||||
'content': data['content'],
|
||||
}
|
||||
if 'attachments' in data:
|
||||
reply['attachments'] = data['attachments']
|
||||
for attachment in reply['attachments']:
|
||||
if re.search(r'(.jpg)|(.png)|(.jpeg)|(.gif)$', attachment['storedName'].lower()):
|
||||
attachment['isImage'] = True
|
||||
if attachment['type'] == 'file':
|
||||
attachment['url'] = uploads.get_presigned_url('groups/{0}/{1}'.format(id, attachment['storedName']))
|
||||
if not data or "content" not in data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
entry_id = ObjectId(entry_id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1, "name": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
entry = db.groupEntries.find_one({"_id": entry_id})
|
||||
if not entry or entry.get("group") != group["_id"]:
|
||||
raise util.errors.NotFound("Entry to reply to not found")
|
||||
if group["_id"] not in user.get("groups", []):
|
||||
raise util.errors.Forbidden("You must be a member to write in the feed")
|
||||
reply = {
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"group": id,
|
||||
"inReplyTo": entry_id,
|
||||
"user": user["_id"],
|
||||
"content": data["content"],
|
||||
}
|
||||
if "attachments" in data:
|
||||
reply["attachments"] = data["attachments"]
|
||||
for attachment in reply["attachments"]:
|
||||
if re.search(
|
||||
r"(.jpg)|(.png)|(.jpeg)|(.gif)$", attachment["storedName"].lower()
|
||||
):
|
||||
attachment["isImage"] = True
|
||||
if attachment["type"] == "file":
|
||||
attachment["url"] = uploads.get_presigned_url(
|
||||
"groups/{0}/{1}".format(id, attachment["storedName"])
|
||||
)
|
||||
|
||||
result = db.groupEntries.insert_one(reply)
|
||||
reply["_id"] = result.inserted_id
|
||||
reply["authorUser"] = {
|
||||
"_id": user["_id"],
|
||||
"username": user["username"],
|
||||
"avatar": user.get("avatar"),
|
||||
}
|
||||
if "avatar" in user:
|
||||
reply["authorUser"]["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(user["_id"], user["avatar"])
|
||||
)
|
||||
op = db.users.find_one(
|
||||
{
|
||||
"$and": [{"_id": entry.get("user")}, {"_id": {"$ne": user["_id"]}}],
|
||||
"subscriptions.email": "messages.replied",
|
||||
}
|
||||
)
|
||||
if op:
|
||||
mail.send(
|
||||
{
|
||||
"to_user": op,
|
||||
"subject": user["username"] + " replied to your post",
|
||||
"text": "Dear {0},\n\n{1} replied to your message in the Notice Board of {2} on {5}:\n\n{3}\n\nFollow the link below to visit the group:\n\n{4}".format(
|
||||
op["username"],
|
||||
user["username"],
|
||||
group["name"],
|
||||
data["content"],
|
||||
"{}/groups/{}".format(APP_URL, str(id)),
|
||||
APP_NAME,
|
||||
),
|
||||
}
|
||||
)
|
||||
return reply
|
||||
|
||||
result = db.groupEntries.insert_one(reply)
|
||||
reply['_id'] = result.inserted_id
|
||||
reply['authorUser'] = {'_id': user['_id'], 'username': user['username'], 'avatar': user.get('avatar')}
|
||||
if 'avatar' in user:
|
||||
reply['authorUser']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(user['_id'], user['avatar']))
|
||||
op = db.users.find_one({'$and': [{'_id': entry.get('user')}, {'_id': {'$ne': user['_id']}}], 'subscriptions.email': 'messages.replied'})
|
||||
if op:
|
||||
mail.send({
|
||||
'to_user': op,
|
||||
'subject': user['username'] + ' replied to your post',
|
||||
'text': 'Dear {0},\n\n{1} replied to your message in the Notice Board of {2} on {5}:\n\n{3}\n\nFollow the link below to visit the group:\n\n{4}'.format(
|
||||
op['username'],
|
||||
user['username'],
|
||||
group['name'],
|
||||
data['content'],
|
||||
'{}/groups/{}'.format(APP_URL, str(id)),
|
||||
APP_NAME,
|
||||
)
|
||||
})
|
||||
return reply
|
||||
|
||||
def delete_entry_reply(user, id, entry_id, reply_id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
entry_id = ObjectId(entry_id)
|
||||
reply_id = ObjectId(reply_id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
entry = db.groupEntries.find_one(entry_id, {'user': 1, 'group': 1})
|
||||
if not entry or entry['group'] != id: raise util.errors.NotFound('Entry not found')
|
||||
reply = db.groupEntries.find_one(reply_id)
|
||||
if not reply or reply.get('inReplyTo') != entry_id: raise util.errors.NotFound('Reply not found')
|
||||
if entry['user'] != user['_id'] and reply['user'] != user['_id'] and user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You must own the reply or entry or be an admin of the group')
|
||||
db.groupEntries.delete_one({'_id': entry_id})
|
||||
return {'deletedEntry': entry_id}
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
entry_id = ObjectId(entry_id)
|
||||
reply_id = ObjectId(reply_id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
entry = db.groupEntries.find_one(entry_id, {"user": 1, "group": 1})
|
||||
if not entry or entry["group"] != id:
|
||||
raise util.errors.NotFound("Entry not found")
|
||||
reply = db.groupEntries.find_one(reply_id)
|
||||
if not reply or reply.get("inReplyTo") != entry_id:
|
||||
raise util.errors.NotFound("Reply not found")
|
||||
if (
|
||||
entry["user"] != user["_id"]
|
||||
and reply["user"] != user["_id"]
|
||||
and user["_id"] not in group.get("admins", [])
|
||||
):
|
||||
raise util.errors.Forbidden(
|
||||
"You must own the reply or entry or be an admin of the group"
|
||||
)
|
||||
db.groupEntries.delete_one({"_id": entry_id})
|
||||
return {"deletedEntry": entry_id}
|
||||
|
||||
def create_member(user, id, user_id, invited = False):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
user_id = ObjectId(user_id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1, 'name': 1, 'closed': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if user_id != user['_id']: raise util.errors.Forbidden('Not allowed to add someone else to the group')
|
||||
if group.get('closed') and not invited and user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('Not allowed to join a closed group')
|
||||
db.users.update_one({'_id': user_id}, {'$addToSet': {'groups': id, 'subscriptions.email': 'groupFeed-' + str(id)}})
|
||||
db.invitations.delete_many({'type': 'group', 'typeId': id, 'recipient': user_id})
|
||||
for admin in db.users.find({'_id': {'$in': group.get('admins', []), '$ne': user_id}, 'subscriptions.email': 'groups.joined'}, {'email': 1, 'username': 1}):
|
||||
mail.send({
|
||||
'to_user': admin,
|
||||
'subject': 'Someone joined your group',
|
||||
'text': 'Dear {0},\n\n{1} recently joined your group {2} on {4}!\n\nFollow the link below to manage your group:\n\n{3}'.format(
|
||||
admin['username'],
|
||||
user['username'],
|
||||
group['name'],
|
||||
'{}/groups/{}'.format(APP_URL, str(id)),
|
||||
APP_NAME,
|
||||
)
|
||||
})
|
||||
|
||||
return {'newMember': user_id}
|
||||
def create_member(user, id, user_id, invited=False):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
user_id = ObjectId(user_id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1, "name": 1, "closed": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user_id != user["_id"]:
|
||||
raise util.errors.Forbidden("Not allowed to add someone else to the group")
|
||||
if (
|
||||
group.get("closed")
|
||||
and not invited
|
||||
and user["_id"] not in group.get("admins", [])
|
||||
):
|
||||
raise util.errors.Forbidden("Not allowed to join a closed group")
|
||||
db.users.update_one(
|
||||
{"_id": user_id},
|
||||
{"$addToSet": {"groups": id, "subscriptions.email": "groupFeed-" + str(id)}},
|
||||
)
|
||||
db.invitations.delete_many({"type": "group", "typeId": id, "recipient": user_id})
|
||||
for admin in db.users.find(
|
||||
{
|
||||
"_id": {"$in": group.get("admins", []), "$ne": user_id},
|
||||
"subscriptions.email": "groups.joined",
|
||||
},
|
||||
{"email": 1, "username": 1},
|
||||
):
|
||||
mail.send(
|
||||
{
|
||||
"to_user": admin,
|
||||
"subject": "Someone joined your group",
|
||||
"text": "Dear {0},\n\n{1} recently joined your group {2} on {4}!\n\nFollow the link below to manage your group:\n\n{3}".format(
|
||||
admin["username"],
|
||||
user["username"],
|
||||
group["name"],
|
||||
"{}/groups/{}".format(APP_URL, str(id)),
|
||||
APP_NAME,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return {"newMember": user_id}
|
||||
|
||||
|
||||
def get_members(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if id not in user.get('groups', []) and not 'root' in user.get('roles', []): raise util.errors.Forbidden('You need to be a member to see the member list')
|
||||
members = list(db.users.find({'groups': id}, {'username': 1, 'avatar': 1, 'bio': 1, 'groups': 1}))
|
||||
for m in members:
|
||||
if 'avatar' in m:
|
||||
m['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(m['_id'], m['avatar']))
|
||||
return {'members': members}
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if id not in user.get("groups", []) and "root" not in user.get("roles", []):
|
||||
raise util.errors.Forbidden("You need to be a member to see the member list")
|
||||
members = list(
|
||||
db.users.find(
|
||||
{"groups": id}, {"username": 1, "avatar": 1, "bio": 1, "groups": 1}
|
||||
)
|
||||
)
|
||||
for m in members:
|
||||
if "avatar" in m:
|
||||
m["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(m["_id"], m["avatar"])
|
||||
)
|
||||
return {"members": members}
|
||||
|
||||
|
||||
def delete_member(user, id, user_id):
|
||||
id = ObjectId(id)
|
||||
user_id = ObjectId(user_id)
|
||||
db = database.get_db()
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if user_id != user['_id'] and user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You can\'t remove this user')
|
||||
if user_id in group.get('admins', []) and len(group['admins']) == 1:
|
||||
raise util.errors.Forbidden('There needs to be at least one admin in this group')
|
||||
db.users.update_one({'_id': user_id}, {'$pull': {'groups': id, 'subscriptions.email': 'groupFeed-' + str(id)}})
|
||||
db.groups.update_one({'_id': id}, {'$pull': {'admins': user_id}})
|
||||
return {'deletedMember': user_id}
|
||||
id = ObjectId(id)
|
||||
user_id = ObjectId(user_id)
|
||||
db = database.get_db()
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user_id != user["_id"] and user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("You can't remove this user")
|
||||
if user_id in group.get("admins", []) and len(group["admins"]) == 1:
|
||||
raise util.errors.Forbidden(
|
||||
"There needs to be at least one admin in this group"
|
||||
)
|
||||
db.users.update_one(
|
||||
{"_id": user_id},
|
||||
{"$pull": {"groups": id, "subscriptions.email": "groupFeed-" + str(id)}},
|
||||
)
|
||||
db.groups.update_one({"_id": id}, {"$pull": {"admins": user_id}})
|
||||
return {"deletedMember": user_id}
|
||||
|
||||
|
||||
def get_projects(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({'_id': id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if id not in user.get('groups', []): raise util.errors.Forbidden('You need to be a member to see the project list')
|
||||
projects = list(db.projects.find({'groupVisibility': id}, {'name': 1, 'path': 1, 'user': 1, 'description': 1, 'visibility': 1}))
|
||||
authors = list(db.users.find({'groups': id, '_id': {'$in': list(map(lambda p: p['user'], projects))}}, {'username': 1, 'avatar': 1, 'bio': 1}))
|
||||
for a in authors:
|
||||
if 'avatar' in a:
|
||||
a['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(a['_id'], a['avatar']))
|
||||
for project in projects:
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if id not in user.get("groups", []):
|
||||
raise util.errors.Forbidden("You need to be a member to see the project list")
|
||||
projects = list(
|
||||
db.projects.find(
|
||||
{"groupVisibility": id},
|
||||
{"name": 1, "path": 1, "user": 1, "description": 1, "visibility": 1},
|
||||
)
|
||||
)
|
||||
authors = list(
|
||||
db.users.find(
|
||||
{"groups": id, "_id": {"$in": list(map(lambda p: p["user"], projects))}},
|
||||
{"username": 1, "avatar": 1, "bio": 1},
|
||||
)
|
||||
)
|
||||
for a in authors:
|
||||
if project['user'] == a['_id']:
|
||||
project['owner'] = a
|
||||
project['fullName'] = a['username'] + '/' + project['path']
|
||||
break
|
||||
return {'projects': projects}
|
||||
if "avatar" in a:
|
||||
a["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(a["_id"], a["avatar"])
|
||||
)
|
||||
for project in projects:
|
||||
for a in authors:
|
||||
if project["user"] == a["_id"]:
|
||||
project["owner"] = a
|
||||
project["fullName"] = a["username"] + "/" + project["path"]
|
||||
break
|
||||
return {"projects": projects}
|
||||
|
@ -1,171 +1,252 @@
|
||||
import re, datetime, os
|
||||
import pymongo
|
||||
import datetime
|
||||
import os
|
||||
from bson.objectid import ObjectId
|
||||
from util import database, util, mail
|
||||
from api import uploads, groups
|
||||
|
||||
APP_NAME = os.environ.get('APP_NAME')
|
||||
APP_URL = os.environ.get('APP_URL')
|
||||
APP_NAME = os.environ.get("APP_NAME")
|
||||
APP_URL = os.environ.get("APP_URL")
|
||||
|
||||
|
||||
def get(user):
|
||||
db = database.get_db()
|
||||
admin_groups = list(db.groups.find({'admins': user['_id']}))
|
||||
invites = list(db.invitations.find({'$or': [{'recipient': user['_id']}, {'recipientGroup': {'$in': list(map(lambda g: g['_id'], admin_groups))}}]}))
|
||||
inviters = list(db.users.find({'_id': {'$in': [i['user'] for i in invites]}}, {'username': 1, 'avatar': 1}))
|
||||
for invite in invites:
|
||||
invite['recipient'] = user['_id']
|
||||
if invite['type'] in ['group', 'groupJoinRequest']: invite['group'] = db.groups.find_one({'_id': invite['typeId']}, {'name': 1})
|
||||
for u in inviters:
|
||||
if u['_id'] == invite['user']:
|
||||
if 'avatar' in u:
|
||||
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar']))
|
||||
invite['invitedBy'] = u
|
||||
break
|
||||
sent_invites = list(db.invitations.find({'user': user['_id']}))
|
||||
recipients = list(db.users.find({'_id': {'$in': list(map(lambda i: i.get('recipient'), sent_invites))}}, {'username': 1, 'avatar': 1}))
|
||||
for invite in sent_invites:
|
||||
if invite['type'] in ['group', 'groupJoinRequest']: invite['group'] = db.groups.find_one({'_id': invite['typeId']}, {'name': 1})
|
||||
for u in recipients:
|
||||
if u['_id'] == invite.get('recipient'):
|
||||
if 'avatar' in u:
|
||||
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar']))
|
||||
invite['invitedBy'] = u
|
||||
break
|
||||
return {'invitations': invites, 'sentInvitations': sent_invites}
|
||||
db = database.get_db()
|
||||
admin_groups = list(db.groups.find({"admins": user["_id"]}))
|
||||
invites = list(
|
||||
db.invitations.find(
|
||||
{
|
||||
"$or": [
|
||||
{"recipient": user["_id"]},
|
||||
{
|
||||
"recipientGroup": {
|
||||
"$in": list(map(lambda g: g["_id"], admin_groups))
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
)
|
||||
inviters = list(
|
||||
db.users.find(
|
||||
{"_id": {"$in": [i["user"] for i in invites]}}, {"username": 1, "avatar": 1}
|
||||
)
|
||||
)
|
||||
for invite in invites:
|
||||
invite["recipient"] = user["_id"]
|
||||
if invite["type"] in ["group", "groupJoinRequest"]:
|
||||
invite["group"] = db.groups.find_one({"_id": invite["typeId"]}, {"name": 1})
|
||||
for u in inviters:
|
||||
if u["_id"] == invite["user"]:
|
||||
if "avatar" in u:
|
||||
u["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(u["_id"], u["avatar"])
|
||||
)
|
||||
invite["invitedBy"] = u
|
||||
break
|
||||
sent_invites = list(db.invitations.find({"user": user["_id"]}))
|
||||
recipients = list(
|
||||
db.users.find(
|
||||
{"_id": {"$in": list(map(lambda i: i.get("recipient"), sent_invites))}},
|
||||
{"username": 1, "avatar": 1},
|
||||
)
|
||||
)
|
||||
for invite in sent_invites:
|
||||
if invite["type"] in ["group", "groupJoinRequest"]:
|
||||
invite["group"] = db.groups.find_one({"_id": invite["typeId"]}, {"name": 1})
|
||||
for u in recipients:
|
||||
if u["_id"] == invite.get("recipient"):
|
||||
if "avatar" in u:
|
||||
u["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(u["_id"], u["avatar"])
|
||||
)
|
||||
invite["invitedBy"] = u
|
||||
break
|
||||
return {"invitations": invites, "sentInvitations": sent_invites}
|
||||
|
||||
|
||||
def accept(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
invite = db.invitations.find_one({'_id': id})
|
||||
if not invite: raise util.errors.NotFound('Invitation not found')
|
||||
if invite['type'] == 'group':
|
||||
if invite['recipient'] != user['_id']: raise util.errors.Forbidden('This invitation is not yours to accept')
|
||||
group = db.groups.find_one({'_id': invite['typeId']}, {'name': 1})
|
||||
if not group:
|
||||
db.invitations.delete_one({'_id': id})
|
||||
return {'acceptedInvitation': id}
|
||||
groups.create_member(user, group['_id'], user['_id'], invited = True)
|
||||
db.invitations.delete_one({'_id': id})
|
||||
return {'acceptedInvitation': id, 'group': group}
|
||||
if invite['type'] == 'groupJoinRequest':
|
||||
group = db.groups.find_one({'_id': invite['typeId']})
|
||||
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You need to be an admin of this group to accept this request')
|
||||
requester = db.users.find_one({'_id': invite['user']})
|
||||
if not group or not requester:
|
||||
db.invitations.delete_one({'_id': id})
|
||||
return {'acceptedInvitation': id}
|
||||
groups.create_member(requester, group['_id'], requester['_id'], invited = True)
|
||||
db.invitations.delete_one({'_id': id})
|
||||
return {'acceptedInvitation': id, 'group': group}
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
invite = db.invitations.find_one({"_id": id})
|
||||
if not invite:
|
||||
raise util.errors.NotFound("Invitation not found")
|
||||
if invite["type"] == "group":
|
||||
if invite["recipient"] != user["_id"]:
|
||||
raise util.errors.Forbidden("This invitation is not yours to accept")
|
||||
group = db.groups.find_one({"_id": invite["typeId"]}, {"name": 1})
|
||||
if not group:
|
||||
db.invitations.delete_one({"_id": id})
|
||||
return {"acceptedInvitation": id}
|
||||
groups.create_member(user, group["_id"], user["_id"], invited=True)
|
||||
db.invitations.delete_one({"_id": id})
|
||||
return {"acceptedInvitation": id, "group": group}
|
||||
if invite["type"] == "groupJoinRequest":
|
||||
group = db.groups.find_one({"_id": invite["typeId"]})
|
||||
if user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden(
|
||||
"You need to be an admin of this group to accept this request"
|
||||
)
|
||||
requester = db.users.find_one({"_id": invite["user"]})
|
||||
if not group or not requester:
|
||||
db.invitations.delete_one({"_id": id})
|
||||
return {"acceptedInvitation": id}
|
||||
groups.create_member(requester, group["_id"], requester["_id"], invited=True)
|
||||
db.invitations.delete_one({"_id": id})
|
||||
return {"acceptedInvitation": id, "group": group}
|
||||
|
||||
|
||||
def delete(user, id):
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
invite = db.invitations.find_one({'_id': id})
|
||||
if not invite: raise util.errors.NotFound('Invitation not found')
|
||||
if invite['type'] == 'group':
|
||||
if invite['recipient'] != user['_id']: raise util.errors.Forbidden('This invitation is not yours to decline')
|
||||
if invite['type'] == 'groupJoinRequest':
|
||||
group = db.groups.find_one({'_id': invite['typeId']})
|
||||
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You need to be an admin of this group to manage this request')
|
||||
db.invitations.delete_one({'_id': id})
|
||||
return {'deletedInvitation': id}
|
||||
db = database.get_db()
|
||||
id = ObjectId(id)
|
||||
invite = db.invitations.find_one({"_id": id})
|
||||
if not invite:
|
||||
raise util.errors.NotFound("Invitation not found")
|
||||
if invite["type"] == "group":
|
||||
if invite["recipient"] != user["_id"]:
|
||||
raise util.errors.Forbidden("This invitation is not yours to decline")
|
||||
if invite["type"] == "groupJoinRequest":
|
||||
group = db.groups.find_one({"_id": invite["typeId"]})
|
||||
if user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden(
|
||||
"You need to be an admin of this group to manage this request"
|
||||
)
|
||||
db.invitations.delete_one({"_id": id})
|
||||
return {"deletedInvitation": id}
|
||||
|
||||
|
||||
def create_group_invitation(user, group_id, data):
|
||||
if not data or 'user' not in data: raise util.errors.BadRequest('Invalid request')
|
||||
db = database.get_db()
|
||||
recipient_id = ObjectId(data['user'])
|
||||
group_id = ObjectId(group_id)
|
||||
group = db.groups.find_one({'_id': group_id}, {'admins': 1, 'name': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You need to be a group admin to invite users')
|
||||
recipient = db.users.find_one({'_id': recipient_id}, {'groups': 1, 'username': 1, 'email': 1, 'subscriptions': 1})
|
||||
if not recipient: raise util.errors.NotFound('User not found')
|
||||
if group_id in recipient.get('groups', []): raise util.errors.BadRequest('This user is already in this group')
|
||||
if db.invitations.find_one({'recipient': recipient_id, 'typeId': group_id, 'type': 'group'}):
|
||||
raise util.errors.BadRequest('This user has already been invited to this group')
|
||||
invite = {
|
||||
'createdAt': datetime.datetime.now(),
|
||||
'user': user['_id'],
|
||||
'recipient': recipient_id,
|
||||
'type': 'group',
|
||||
'typeId': group_id
|
||||
}
|
||||
result = db.invitations.insert_one(invite)
|
||||
if 'groups.invited' in recipient.get('subscriptions', {}).get('email', []):
|
||||
mail.send({
|
||||
'to_user': recipient,
|
||||
'subject': 'You\'ve been invited to a group on {}!'.format(APP_NAME),
|
||||
'text': 'Dear {0},\n\nYou have been invited to join the group {1} on {3}!\n\nLogin by visting {2} to find your invitation.'.format(
|
||||
recipient['username'],
|
||||
group['name'],
|
||||
APP_URL,
|
||||
APP_NAME,
|
||||
)
|
||||
})
|
||||
invite['_id'] = result.inserted_id
|
||||
return invite
|
||||
if not data or "user" not in data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
db = database.get_db()
|
||||
recipient_id = ObjectId(data["user"])
|
||||
group_id = ObjectId(group_id)
|
||||
group = db.groups.find_one({"_id": group_id}, {"admins": 1, "name": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("You need to be a group admin to invite users")
|
||||
recipient = db.users.find_one(
|
||||
{"_id": recipient_id},
|
||||
{"groups": 1, "username": 1, "email": 1, "subscriptions": 1},
|
||||
)
|
||||
if not recipient:
|
||||
raise util.errors.NotFound("User not found")
|
||||
if group_id in recipient.get("groups", []):
|
||||
raise util.errors.BadRequest("This user is already in this group")
|
||||
if db.invitations.find_one(
|
||||
{"recipient": recipient_id, "typeId": group_id, "type": "group"}
|
||||
):
|
||||
raise util.errors.BadRequest("This user has already been invited to this group")
|
||||
invite = {
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"user": user["_id"],
|
||||
"recipient": recipient_id,
|
||||
"type": "group",
|
||||
"typeId": group_id,
|
||||
}
|
||||
result = db.invitations.insert_one(invite)
|
||||
if "groups.invited" in recipient.get("subscriptions", {}).get("email", []):
|
||||
mail.send(
|
||||
{
|
||||
"to_user": recipient,
|
||||
"subject": "You've been invited to a group on {}!".format(APP_NAME),
|
||||
"text": "Dear {0},\n\nYou have been invited to join the group {1} on {3}!\n\nLogin by visting {2} to find your invitation.".format(
|
||||
recipient["username"],
|
||||
group["name"],
|
||||
APP_URL,
|
||||
APP_NAME,
|
||||
),
|
||||
}
|
||||
)
|
||||
invite["_id"] = result.inserted_id
|
||||
return invite
|
||||
|
||||
|
||||
def create_group_request(user, group_id):
|
||||
db = database.get_db()
|
||||
group_id = ObjectId(group_id)
|
||||
group = db.groups.find_one({'_id': group_id}, {'admins': 1, 'name': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if group_id in user.get('groups', []): raise util.errors.BadRequest('You are already a member of this group')
|
||||
admin = db.users.find_one({'_id': {'$in': group.get('admins', [])}}, {'groups': 1, 'username': 1, 'email': 1, 'subscriptions': 1})
|
||||
if not admin: raise util.errors.NotFound('No users can approve you to join this group')
|
||||
if db.invitations.find_one({'recipient': user['_id'], 'typeId': group_id, 'type': 'group'}):
|
||||
raise util.errors.BadRequest('You have already been invited to this group')
|
||||
if db.invitations.find_one({'user': user['_id'], 'typeId': group_id, 'type': 'groupJoinRequest'}):
|
||||
raise util.errors.BadRequest('You have already requested access to this group')
|
||||
invite = {
|
||||
'createdAt': datetime.datetime.now(),
|
||||
'user': user['_id'],
|
||||
'recipientGroup': group['_id'],
|
||||
'type': 'groupJoinRequest',
|
||||
'typeId': group_id
|
||||
}
|
||||
result = db.invitations.insert_one(invite)
|
||||
if 'groups.joinRequested' in admin.get('subscriptions', {}).get('email', []):
|
||||
mail.send({
|
||||
'to_user': admin,
|
||||
'subject': 'Someone wants to join your group',
|
||||
'text': 'Dear {0},\n\{1} has requested to join your group {2} on {4}!\n\nLogin by visting {3} to find and approve your requests.'.format(
|
||||
admin['username'],
|
||||
user['username'],
|
||||
group['name'],
|
||||
APP_URL,
|
||||
APP_NAME,
|
||||
)
|
||||
})
|
||||
invite['_id'] = result.inserted_id
|
||||
return invite
|
||||
db = database.get_db()
|
||||
group_id = ObjectId(group_id)
|
||||
group = db.groups.find_one({"_id": group_id}, {"admins": 1, "name": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if group_id in user.get("groups", []):
|
||||
raise util.errors.BadRequest("You are already a member of this group")
|
||||
admin = db.users.find_one(
|
||||
{"_id": {"$in": group.get("admins", [])}},
|
||||
{"groups": 1, "username": 1, "email": 1, "subscriptions": 1},
|
||||
)
|
||||
if not admin:
|
||||
raise util.errors.NotFound("No users can approve you to join this group")
|
||||
if db.invitations.find_one(
|
||||
{"recipient": user["_id"], "typeId": group_id, "type": "group"}
|
||||
):
|
||||
raise util.errors.BadRequest("You have already been invited to this group")
|
||||
if db.invitations.find_one(
|
||||
{"user": user["_id"], "typeId": group_id, "type": "groupJoinRequest"}
|
||||
):
|
||||
raise util.errors.BadRequest("You have already requested access to this group")
|
||||
invite = {
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"user": user["_id"],
|
||||
"recipientGroup": group["_id"],
|
||||
"type": "groupJoinRequest",
|
||||
"typeId": group_id,
|
||||
}
|
||||
result = db.invitations.insert_one(invite)
|
||||
if "groups.joinRequested" in admin.get("subscriptions", {}).get("email", []):
|
||||
mail.send(
|
||||
{
|
||||
"to_user": admin,
|
||||
"subject": "Someone wants to join your group",
|
||||
"text": "Dear {0},\n\{1} has requested to join your group {2} on {4}!\n\nLogin by visting {3} to find and approve your requests.".format(
|
||||
admin["username"],
|
||||
user["username"],
|
||||
group["name"],
|
||||
APP_URL,
|
||||
APP_NAME,
|
||||
),
|
||||
}
|
||||
)
|
||||
invite["_id"] = result.inserted_id
|
||||
return invite
|
||||
|
||||
|
||||
def get_group_invitations(user, id):
|
||||
db = database.get_db()
|
||||
group_id = ObjectId(id)
|
||||
group = db.groups.find_one({'_id': group_id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You need to be a group admin to see invitations')
|
||||
invites = list(db.invitations.find({'type': 'group', 'typeId': group_id}))
|
||||
recipients = list(db.users.find({'_id': {'$in': [i['recipient'] for i in invites]}}, {'username': 1, 'avatar': 1}))
|
||||
for invite in invites:
|
||||
for recipient in recipients:
|
||||
if invite['recipient'] == recipient['_id']:
|
||||
if 'avatar' in recipient:
|
||||
recipient['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(recipient['_id'], recipient['avatar']))
|
||||
invite['recipientUser'] = recipient
|
||||
break
|
||||
return {'invitations': invites}
|
||||
db = database.get_db()
|
||||
group_id = ObjectId(id)
|
||||
group = db.groups.find_one({"_id": group_id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("You need to be a group admin to see invitations")
|
||||
invites = list(db.invitations.find({"type": "group", "typeId": group_id}))
|
||||
recipients = list(
|
||||
db.users.find(
|
||||
{"_id": {"$in": [i["recipient"] for i in invites]}},
|
||||
{"username": 1, "avatar": 1},
|
||||
)
|
||||
)
|
||||
for invite in invites:
|
||||
for recipient in recipients:
|
||||
if invite["recipient"] == recipient["_id"]:
|
||||
if "avatar" in recipient:
|
||||
recipient["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(recipient["_id"], recipient["avatar"])
|
||||
)
|
||||
invite["recipientUser"] = recipient
|
||||
break
|
||||
return {"invitations": invites}
|
||||
|
||||
|
||||
def delete_group_invitation(user, id, invite_id):
|
||||
db = database.get_db()
|
||||
group_id = ObjectId(id)
|
||||
invite_id = ObjectId(invite_id)
|
||||
group = db.groups.find_one({'_id': group_id}, {'admins': 1})
|
||||
if not group: raise util.errors.NotFound('Group not found')
|
||||
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You need to be a group admin to see invitations')
|
||||
invite = db.invitations.find_one({'_id': invite_id})
|
||||
if not invite or invite['typeId'] != group_id: raise util.errors.NotFound('This invite could not be found')
|
||||
db.invitations.delete_one({'_id': invite_id})
|
||||
return {'deletedInvite': invite_id}
|
||||
db = database.get_db()
|
||||
group_id = ObjectId(id)
|
||||
invite_id = ObjectId(invite_id)
|
||||
group = db.groups.find_one({"_id": group_id}, {"admins": 1})
|
||||
if not group:
|
||||
raise util.errors.NotFound("Group not found")
|
||||
if user["_id"] not in group.get("admins", []):
|
||||
raise util.errors.Forbidden("You need to be a group admin to see invitations")
|
||||
invite = db.invitations.find_one({"_id": invite_id})
|
||||
if not invite or invite["typeId"] != group_id:
|
||||
raise util.errors.NotFound("This invite could not be found")
|
||||
db.invitations.delete_one({"_id": invite_id})
|
||||
return {"deletedInvite": invite_id}
|
||||
|
@ -1,199 +1,256 @@
|
||||
import datetime, base64, os
|
||||
import datetime
|
||||
import base64
|
||||
import os
|
||||
from bson.objectid import ObjectId
|
||||
import requests
|
||||
from util import database, wif, util, mail
|
||||
from api import uploads
|
||||
|
||||
APP_NAME = os.environ.get('APP_NAME')
|
||||
APP_URL = os.environ.get('APP_URL')
|
||||
APP_NAME = os.environ.get("APP_NAME")
|
||||
APP_URL = os.environ.get("APP_URL")
|
||||
|
||||
|
||||
def delete(user, id):
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id), {'project': 1})
|
||||
if not obj:
|
||||
raise util.errors.NotFound('Object not found')
|
||||
project = db.projects.find_one(obj.get('project'), {'user': 1})
|
||||
if not project:
|
||||
raise util.errors.NotFound('Project not found')
|
||||
if not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden('Forbidden', 403)
|
||||
db.objects.delete_one({'_id': ObjectId(id)})
|
||||
return {'deletedObject': id}
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id), {"project": 1})
|
||||
if not obj:
|
||||
raise util.errors.NotFound("Object not found")
|
||||
project = db.projects.find_one(obj.get("project"), {"user": 1})
|
||||
if not project:
|
||||
raise util.errors.NotFound("Project not found")
|
||||
if not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden("Forbidden", 403)
|
||||
db.objects.delete_one({"_id": ObjectId(id)})
|
||||
return {"deletedObject": id}
|
||||
|
||||
|
||||
def get(user, id):
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one({'_id': ObjectId(id)})
|
||||
if not obj: raise util.errors.NotFound('Object not found')
|
||||
proj = db.projects.find_one({'_id': obj['project']})
|
||||
if not proj: raise util.errors.NotFound('Project not found')
|
||||
is_owner = user and (user.get('_id') == proj['user'])
|
||||
if not is_owner and proj['visibility'] != 'public':
|
||||
raise util.errors.BadRequest('Forbidden')
|
||||
owner = db.users.find_one({'_id': proj['user']}, {'username': 1, 'avatar': 1})
|
||||
if obj['type'] == 'file' and 'storedName' in obj:
|
||||
obj['url'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['storedName']))
|
||||
if obj['type'] == 'pattern' and 'preview' in obj and '.png' in obj['preview']:
|
||||
obj['previewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['preview']))
|
||||
del obj['preview']
|
||||
if obj.get('fullPreview'):
|
||||
obj['fullPreviewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['fullPreview']))
|
||||
obj['projectObject'] = proj
|
||||
if owner:
|
||||
if 'avatar' in owner:
|
||||
owner['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(owner['_id']), owner['avatar']))
|
||||
obj['projectObject']['owner'] = owner
|
||||
return obj
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one({"_id": ObjectId(id)})
|
||||
if not obj:
|
||||
raise util.errors.NotFound("Object not found")
|
||||
proj = db.projects.find_one({"_id": obj["project"]})
|
||||
if not proj:
|
||||
raise util.errors.NotFound("Project not found")
|
||||
is_owner = user and (user.get("_id") == proj["user"])
|
||||
if not is_owner and proj["visibility"] != "public":
|
||||
raise util.errors.BadRequest("Forbidden")
|
||||
owner = db.users.find_one({"_id": proj["user"]}, {"username": 1, "avatar": 1})
|
||||
if obj["type"] == "file" and "storedName" in obj:
|
||||
obj["url"] = uploads.get_presigned_url(
|
||||
"projects/{0}/{1}".format(proj["_id"], obj["storedName"])
|
||||
)
|
||||
if obj["type"] == "pattern" and "preview" in obj and ".png" in obj["preview"]:
|
||||
obj["previewUrl"] = uploads.get_presigned_url(
|
||||
"projects/{0}/{1}".format(proj["_id"], obj["preview"])
|
||||
)
|
||||
del obj["preview"]
|
||||
if obj.get("fullPreview"):
|
||||
obj["fullPreviewUrl"] = uploads.get_presigned_url(
|
||||
"projects/{0}/{1}".format(proj["_id"], obj["fullPreview"])
|
||||
)
|
||||
obj["projectObject"] = proj
|
||||
if owner:
|
||||
if "avatar" in owner:
|
||||
owner["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(str(owner["_id"]), owner["avatar"])
|
||||
)
|
||||
obj["projectObject"]["owner"] = owner
|
||||
return obj
|
||||
|
||||
|
||||
def copy_to_project(user, id, project_id):
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id))
|
||||
if not obj: raise util.errors.NotFound('This object could not be found')
|
||||
original_project = db.projects.find_one(obj['project'])
|
||||
if not original_project:
|
||||
raise util.errors.NotFound('Project not found')
|
||||
if not original_project.get('openSource') and not util.can_edit_project(user, original_project):
|
||||
raise util.errors.Forbidden('This project is not open-source')
|
||||
if original_project.get('visibility') != 'public' and not util.can_edit_project(user, original_project):
|
||||
raise util.errors.Forbidden('This project is not public')
|
||||
target_project = db.projects.find_one(ObjectId(project_id))
|
||||
if not target_project or not util.can_edit_project(user, target_project):
|
||||
raise util.errors.Forbidden('You don\'t own the target project')
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id))
|
||||
if not obj:
|
||||
raise util.errors.NotFound("This object could not be found")
|
||||
original_project = db.projects.find_one(obj["project"])
|
||||
if not original_project:
|
||||
raise util.errors.NotFound("Project not found")
|
||||
if not original_project.get("openSource") and not util.can_edit_project(
|
||||
user, original_project
|
||||
):
|
||||
raise util.errors.Forbidden("This project is not open-source")
|
||||
if original_project.get("visibility") != "public" and not util.can_edit_project(
|
||||
user, original_project
|
||||
):
|
||||
raise util.errors.Forbidden("This project is not public")
|
||||
target_project = db.projects.find_one(ObjectId(project_id))
|
||||
if not target_project or not util.can_edit_project(user, target_project):
|
||||
raise util.errors.Forbidden("You don't own the target project")
|
||||
|
||||
obj["_id"] = ObjectId()
|
||||
obj["project"] = target_project["_id"]
|
||||
obj["createdAt"] = datetime.datetime.now()
|
||||
obj["commentCount"] = 0
|
||||
if "preview" in obj:
|
||||
del obj["preview"]
|
||||
if obj.get("pattern"):
|
||||
images = wif.generate_images(obj)
|
||||
if images:
|
||||
obj.update(images)
|
||||
db.objects.insert_one(obj)
|
||||
return obj
|
||||
|
||||
obj['_id'] = ObjectId()
|
||||
obj['project'] = target_project['_id']
|
||||
obj['createdAt'] = datetime.datetime.now()
|
||||
obj['commentCount'] = 0
|
||||
if 'preview' in obj: del obj['preview']
|
||||
if obj.get('pattern'):
|
||||
images = wif.generate_images(obj)
|
||||
if images: obj.update(images)
|
||||
db.objects.insert_one(obj)
|
||||
return obj
|
||||
|
||||
def get_wif(user, id):
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id))
|
||||
if not obj: raise util.errors.NotFound('Object not found')
|
||||
project = db.projects.find_one(obj['project'])
|
||||
if not project.get('openSource') and not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden('This project is not open-source')
|
||||
if project.get('visibility') != 'public' and not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden('This project is not public')
|
||||
try:
|
||||
output = wif.dumps(obj).replace('\n', '\\n')
|
||||
return {'wif': output}
|
||||
except Exception as e:
|
||||
raise util.errors.BadRequest('Unable to create WIF file')
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id))
|
||||
if not obj:
|
||||
raise util.errors.NotFound("Object not found")
|
||||
project = db.projects.find_one(obj["project"])
|
||||
if not project.get("openSource") and not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden("This project is not open-source")
|
||||
if project.get("visibility") != "public" and not util.can_edit_project(
|
||||
user, project
|
||||
):
|
||||
raise util.errors.Forbidden("This project is not public")
|
||||
try:
|
||||
output = wif.dumps(obj).replace("\n", "\\n")
|
||||
return {"wif": output}
|
||||
except Exception:
|
||||
raise util.errors.BadRequest("Unable to create WIF file")
|
||||
|
||||
|
||||
def get_pdf(user, id):
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id))
|
||||
if not obj: raise util.errors.NotFound('Object not found')
|
||||
project = db.projects.find_one(obj['project'])
|
||||
if not project.get('openSource') and not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden('This project is not open-source')
|
||||
if project.get('visibility') != 'public' and not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden('This project is not public')
|
||||
try:
|
||||
response = requests.get('https://h2io6k3ovg.execute-api.eu-west-1.amazonaws.com/prod/pdf?object=' + id + '&landscape=true&paperWidth=23.39&paperHeight=33.11')
|
||||
response.raise_for_status()
|
||||
pdf = uploads.get_file('objects/' + id + '/export.pdf')
|
||||
body64 = base64.b64encode(pdf['Body'].read())
|
||||
bytes_str = str(body64).replace("b'", '')[:-1]
|
||||
return {'pdf': body64.decode('ascii')}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise util.errors.BadRequest('Unable to export PDF')
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id))
|
||||
if not obj:
|
||||
raise util.errors.NotFound("Object not found")
|
||||
project = db.projects.find_one(obj["project"])
|
||||
if not project.get("openSource") and not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden("This project is not open-source")
|
||||
if project.get("visibility") != "public" and not util.can_edit_project(
|
||||
user, project
|
||||
):
|
||||
raise util.errors.Forbidden("This project is not public")
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://h2io6k3ovg.execute-api.eu-west-1.amazonaws.com/prod/pdf?object="
|
||||
+ id
|
||||
+ "&landscape=true&paperWidth=23.39&paperHeight=33.11"
|
||||
)
|
||||
response.raise_for_status()
|
||||
pdf = uploads.get_file("objects/" + id + "/export.pdf")
|
||||
body64 = base64.b64encode(pdf["Body"].read())
|
||||
return {"pdf": body64.decode("ascii")}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise util.errors.BadRequest("Unable to export PDF")
|
||||
|
||||
|
||||
def update(user, id, data):
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id), {'project': 1})
|
||||
if not obj: raise util.errors.NotFound('Object not found')
|
||||
project = db.projects.find_one(obj.get('project'), {'user': 1})
|
||||
if not project: raise util.errors.NotFound('Project not found')
|
||||
if not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden('Forbidden')
|
||||
allowed_keys = ['name', 'description', 'pattern']
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one(ObjectId(id), {"project": 1})
|
||||
if not obj:
|
||||
raise util.errors.NotFound("Object not found")
|
||||
project = db.projects.find_one(obj.get("project"), {"user": 1})
|
||||
if not project:
|
||||
raise util.errors.NotFound("Project not found")
|
||||
if not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden("Forbidden")
|
||||
allowed_keys = ["name", "description", "pattern"]
|
||||
|
||||
if data.get('pattern'):
|
||||
obj.update(data)
|
||||
images = wif.generate_images(obj)
|
||||
if images:
|
||||
data.update(images)
|
||||
allowed_keys += ['preview', 'fullPreview']
|
||||
if data.get("pattern"):
|
||||
obj.update(data)
|
||||
images = wif.generate_images(obj)
|
||||
if images:
|
||||
data.update(images)
|
||||
allowed_keys += ["preview", "fullPreview"]
|
||||
|
||||
updater = util.build_updater(data, allowed_keys)
|
||||
if updater:
|
||||
db.objects.update_one({"_id": ObjectId(id)}, updater)
|
||||
return get(user, id)
|
||||
|
||||
updater = util.build_updater(data, allowed_keys)
|
||||
if updater:
|
||||
db.objects.update_one({'_id': ObjectId(id)}, updater)
|
||||
return get(user, id)
|
||||
|
||||
def create_comment(user, id, data):
|
||||
if not data or not data.get('content'): raise util.errors.BadRequest('Comment data is required')
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one({'_id': ObjectId(id)})
|
||||
if not obj: raise util.errors.NotFound('We could not find the specified object')
|
||||
project = db.projects.find_one({'_id': obj['project']})
|
||||
comment = {
|
||||
'content': data.get('content', ''),
|
||||
'object': ObjectId(id),
|
||||
'user': user['_id'],
|
||||
'createdAt': datetime.datetime.now()
|
||||
}
|
||||
result = db.comments.insert_one(comment)
|
||||
db.objects.update_one({'_id': ObjectId(id)}, {'$inc': {'commentCount': 1}})
|
||||
comment['_id'] = result.inserted_id
|
||||
comment['authorUser'] = {
|
||||
'username': user['username'],
|
||||
'avatar': user.get('avatar'),
|
||||
'avatarUrl': uploads.get_presigned_url('users/{0}/{1}'.format(user['_id'], user.get('avatar')))
|
||||
}
|
||||
project_owner = db.users.find_one({'_id': project['user'], 'subscriptions.email': 'projects.commented'})
|
||||
if project_owner and project_owner['_id'] != user['_id']:
|
||||
mail.send({
|
||||
'to_user': project_owner,
|
||||
'subject': '{} commented on {}'.format(user['username'], project['name']),
|
||||
'text': 'Dear {0},\n\n{1} commented on {2} in your project {3} on {6}:\n\n{4}\n\nFollow the link below to see the comment:\n\n{5}'.format(
|
||||
project_owner['username'],
|
||||
user['username'],
|
||||
obj['name'],
|
||||
project['name'],
|
||||
comment['content'],
|
||||
'{}/{}/{}/{}'.format(
|
||||
APP_URL, project_owner['username'], project['path'], str(id)
|
||||
if not data or not data.get("content"):
|
||||
raise util.errors.BadRequest("Comment data is required")
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one({"_id": ObjectId(id)})
|
||||
if not obj:
|
||||
raise util.errors.NotFound("We could not find the specified object")
|
||||
project = db.projects.find_one({"_id": obj["project"]})
|
||||
comment = {
|
||||
"content": data.get("content", ""),
|
||||
"object": ObjectId(id),
|
||||
"user": user["_id"],
|
||||
"createdAt": datetime.datetime.now(),
|
||||
}
|
||||
result = db.comments.insert_one(comment)
|
||||
db.objects.update_one({"_id": ObjectId(id)}, {"$inc": {"commentCount": 1}})
|
||||
comment["_id"] = result.inserted_id
|
||||
comment["authorUser"] = {
|
||||
"username": user["username"],
|
||||
"avatar": user.get("avatar"),
|
||||
"avatarUrl": uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(user["_id"], user.get("avatar"))
|
||||
),
|
||||
APP_NAME,
|
||||
)
|
||||
})
|
||||
return comment
|
||||
}
|
||||
project_owner = db.users.find_one(
|
||||
{"_id": project["user"], "subscriptions.email": "projects.commented"}
|
||||
)
|
||||
if project_owner and project_owner["_id"] != user["_id"]:
|
||||
mail.send(
|
||||
{
|
||||
"to_user": project_owner,
|
||||
"subject": "{} commented on {}".format(
|
||||
user["username"], project["name"]
|
||||
),
|
||||
"text": "Dear {0},\n\n{1} commented on {2} in your project {3} on {6}:\n\n{4}\n\nFollow the link below to see the comment:\n\n{5}".format(
|
||||
project_owner["username"],
|
||||
user["username"],
|
||||
obj["name"],
|
||||
project["name"],
|
||||
comment["content"],
|
||||
"{}/{}/{}/{}".format(
|
||||
APP_URL, project_owner["username"], project["path"], str(id)
|
||||
),
|
||||
APP_NAME,
|
||||
),
|
||||
}
|
||||
)
|
||||
return comment
|
||||
|
||||
|
||||
def get_comments(user, id):
|
||||
id = ObjectId(id)
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one({'_id': id}, {'project': 1})
|
||||
if not obj: raise util.errors.NotFound('Object not found')
|
||||
proj = db.projects.find_one({'_id': obj['project']}, {'user': 1, 'visibility': 1})
|
||||
if not proj: raise util.errors.NotFound('Project not found')
|
||||
is_owner = user and (user.get('_id') == proj['user'])
|
||||
if not is_owner and proj['visibility'] != 'public':
|
||||
raise util.errors.Forbidden('This project is private')
|
||||
comments = list(db.comments.find({'object': id}))
|
||||
user_ids = list(map(lambda c:c['user'], comments))
|
||||
users = list(db.users.find({'_id': {'$in': user_ids}}, {'username': 1, 'avatar': 1}))
|
||||
for comment in comments:
|
||||
for u in users:
|
||||
if comment['user'] == u['_id']:
|
||||
comment['authorUser'] = u
|
||||
if 'avatar' in u:
|
||||
comment['authorUser']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar']))
|
||||
return {'comments': comments}
|
||||
id = ObjectId(id)
|
||||
db = database.get_db()
|
||||
obj = db.objects.find_one({"_id": id}, {"project": 1})
|
||||
if not obj:
|
||||
raise util.errors.NotFound("Object not found")
|
||||
proj = db.projects.find_one({"_id": obj["project"]}, {"user": 1, "visibility": 1})
|
||||
if not proj:
|
||||
raise util.errors.NotFound("Project not found")
|
||||
is_owner = user and (user.get("_id") == proj["user"])
|
||||
if not is_owner and proj["visibility"] != "public":
|
||||
raise util.errors.Forbidden("This project is private")
|
||||
comments = list(db.comments.find({"object": id}))
|
||||
user_ids = list(map(lambda c: c["user"], comments))
|
||||
users = list(
|
||||
db.users.find({"_id": {"$in": user_ids}}, {"username": 1, "avatar": 1})
|
||||
)
|
||||
for comment in comments:
|
||||
for u in users:
|
||||
if comment["user"] == u["_id"]:
|
||||
comment["authorUser"] = u
|
||||
if "avatar" in u:
|
||||
comment["authorUser"]["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(u["_id"], u["avatar"])
|
||||
)
|
||||
return {"comments": comments}
|
||||
|
||||
|
||||
def delete_comment(user, id, comment_id):
|
||||
db = database.get_db()
|
||||
comment = db.comments.find_one({'_id': ObjectId(comment_id)})
|
||||
obj = db.objects.find_one({'_id': ObjectId(id)})
|
||||
if not comment or not obj or obj['_id'] != comment['object']: raise util.errors.NotFound('Comment not found')
|
||||
project = db.projects.find_one({'_id': obj['project']})
|
||||
if comment['user'] != user['_id'] and not util.can_edit_project(user, project): raise util.errors.Forbidden('You can\'t delete this comment')
|
||||
db.comments.delete_one({'_id': comment['_id']})
|
||||
db.objects.update_one({'_id': ObjectId(id)}, {'$inc': {'commentCount': -1}})
|
||||
return {'deletedComment': comment['_id']}
|
||||
db = database.get_db()
|
||||
comment = db.comments.find_one({"_id": ObjectId(comment_id)})
|
||||
obj = db.objects.find_one({"_id": ObjectId(id)})
|
||||
if not comment or not obj or obj["_id"] != comment["object"]:
|
||||
raise util.errors.NotFound("Comment not found")
|
||||
project = db.projects.find_one({"_id": obj["project"]})
|
||||
if comment["user"] != user["_id"] and not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden("You can't delete this comment")
|
||||
db.comments.delete_one({"_id": comment["_id"]})
|
||||
db.objects.update_one({"_id": ObjectId(id)}, {"$inc": {"commentCount": -1}})
|
||||
return {"deletedComment": comment["_id"]}
|
||||
|
@ -1,189 +1,345 @@
|
||||
import datetime, re
|
||||
import datetime
|
||||
import re
|
||||
from bson.objectid import ObjectId
|
||||
from util import database, wif, util
|
||||
from api import uploads, objects
|
||||
|
||||
default_pattern = {
|
||||
'warp': {
|
||||
'shafts': 8,
|
||||
'threading': [{'shaft': 0}] * 100,
|
||||
'defaultColour': '178,53,111',
|
||||
'defaultSpacing': 1,
|
||||
'defaultThickness': 1,
|
||||
},
|
||||
'weft': {
|
||||
'treadles': 8,
|
||||
'treadling': [{'treadle': 0}] * 50,
|
||||
'defaultColour': '53,69,178',
|
||||
'defaultSpacing': 1,
|
||||
'defaultThickness': 1
|
||||
},
|
||||
'tieups': [[]] * 8,
|
||||
'colours': ['256,256,256', '0,0,0', '50,0,256', '0,68,256', '0,256,256', '0,256,0', '119,256,0', '256,256,0', '256,136,0', '256,0,0', '256,0,153', '204,0,256', '132,102,256', '102,155,256', '102,256,256', '102,256,102', '201,256,102', '256,256,102', '256,173,102', '256,102,102', '256,102,194', '224,102,256', '31,0,153', '0,41,153', '0,153,153', '0,153,0', '71,153,0', '153,153,0', '153,82,0', '153,0,0', '153,0,92', '122,0,153', '94,68,204', '68,102,204', '68,204,204', '68,204,68', '153,204,68', '204,204,68', '204,136,68', '204,68,68', '204,68,153', '170,68,204', '37,0,204', '0,50,204', '0,204,204', '0,204,0', '89,204,0', '204,204,0', '204,102,0', '204,0,0', '204,0,115', '153,0,204', '168,136,256', '136,170,256', '136,256,256', '136,256,136', '230,256,136', '256,256,136', '256,178,136', '256,136,136', '256,136,204', '240,136,256', '49,34,238', '34,68,238', '34,238,238', '34,238,34', '71,238,34', '238,238,34', '238,82,34', '238,34,34', '238,34,92', '122,34,238', '128,102,238', '102,136,238', '102,238,238', '102,238,102', '187,238,102', '238,238,102', '238,170,102', '238,102,102', '238,102,187', '204,102,238', '178,53,111', '53,69,178'],
|
||||
"warp": {
|
||||
"shafts": 8,
|
||||
"threading": [{"shaft": 0}] * 100,
|
||||
"defaultColour": "178,53,111",
|
||||
"defaultSpacing": 1,
|
||||
"defaultThickness": 1,
|
||||
},
|
||||
"weft": {
|
||||
"treadles": 8,
|
||||
"treadling": [{"treadle": 0}] * 50,
|
||||
"defaultColour": "53,69,178",
|
||||
"defaultSpacing": 1,
|
||||
"defaultThickness": 1,
|
||||
},
|
||||
"tieups": [[]] * 8,
|
||||
"colours": [
|
||||
"256,256,256",
|
||||
"0,0,0",
|
||||
"50,0,256",
|
||||
"0,68,256",
|
||||
"0,256,256",
|
||||
"0,256,0",
|
||||
"119,256,0",
|
||||
"256,256,0",
|
||||
"256,136,0",
|
||||
"256,0,0",
|
||||
"256,0,153",
|
||||
"204,0,256",
|
||||
"132,102,256",
|
||||
"102,155,256",
|
||||
"102,256,256",
|
||||
"102,256,102",
|
||||
"201,256,102",
|
||||
"256,256,102",
|
||||
"256,173,102",
|
||||
"256,102,102",
|
||||
"256,102,194",
|
||||
"224,102,256",
|
||||
"31,0,153",
|
||||
"0,41,153",
|
||||
"0,153,153",
|
||||
"0,153,0",
|
||||
"71,153,0",
|
||||
"153,153,0",
|
||||
"153,82,0",
|
||||
"153,0,0",
|
||||
"153,0,92",
|
||||
"122,0,153",
|
||||
"94,68,204",
|
||||
"68,102,204",
|
||||
"68,204,204",
|
||||
"68,204,68",
|
||||
"153,204,68",
|
||||
"204,204,68",
|
||||
"204,136,68",
|
||||
"204,68,68",
|
||||
"204,68,153",
|
||||
"170,68,204",
|
||||
"37,0,204",
|
||||
"0,50,204",
|
||||
"0,204,204",
|
||||
"0,204,0",
|
||||
"89,204,0",
|
||||
"204,204,0",
|
||||
"204,102,0",
|
||||
"204,0,0",
|
||||
"204,0,115",
|
||||
"153,0,204",
|
||||
"168,136,256",
|
||||
"136,170,256",
|
||||
"136,256,256",
|
||||
"136,256,136",
|
||||
"230,256,136",
|
||||
"256,256,136",
|
||||
"256,178,136",
|
||||
"256,136,136",
|
||||
"256,136,204",
|
||||
"240,136,256",
|
||||
"49,34,238",
|
||||
"34,68,238",
|
||||
"34,238,238",
|
||||
"34,238,34",
|
||||
"71,238,34",
|
||||
"238,238,34",
|
||||
"238,82,34",
|
||||
"238,34,34",
|
||||
"238,34,92",
|
||||
"122,34,238",
|
||||
"128,102,238",
|
||||
"102,136,238",
|
||||
"102,238,238",
|
||||
"102,238,102",
|
||||
"187,238,102",
|
||||
"238,238,102",
|
||||
"238,170,102",
|
||||
"238,102,102",
|
||||
"238,102,187",
|
||||
"204,102,238",
|
||||
"178,53,111",
|
||||
"53,69,178",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def derive_path(name):
|
||||
path = name.replace(' ', '-').lower()
|
||||
return re.sub('[^0-9a-z\-]+', '', path)
|
||||
path = name.replace(" ", "-").lower()
|
||||
return re.sub("[^0-9a-z\-]+", "", path)
|
||||
|
||||
|
||||
def get_by_username(username, project_path):
|
||||
db = database.get_db()
|
||||
owner = db.users.find_one({'username': username}, {'_id': 1, 'username': 1})
|
||||
if not owner:
|
||||
raise util.errors.BadRequest('User not found')
|
||||
project = db.projects.find_one({'user': owner['_id'], 'path': project_path})
|
||||
if not project:
|
||||
raise util.errors.NotFound('Project not found')
|
||||
project['owner'] = owner
|
||||
project['fullName'] = owner['username'] + '/' + project['path']
|
||||
return project
|
||||
db = database.get_db()
|
||||
owner = db.users.find_one({"username": username}, {"_id": 1, "username": 1})
|
||||
if not owner:
|
||||
raise util.errors.BadRequest("User not found")
|
||||
project = db.projects.find_one({"user": owner["_id"], "path": project_path})
|
||||
if not project:
|
||||
raise util.errors.NotFound("Project not found")
|
||||
project["owner"] = owner
|
||||
project["fullName"] = owner["username"] + "/" + project["path"]
|
||||
return project
|
||||
|
||||
|
||||
def create(user, data):
|
||||
if not data: raise util.errors.BadRequest('Invalid request')
|
||||
name = data.get('name', '')
|
||||
if len(name) < 3: raise util.errors.BadRequest('A longer name is required')
|
||||
db = database.get_db()
|
||||
if not data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
name = data.get("name", "")
|
||||
if len(name) < 3:
|
||||
raise util.errors.BadRequest("A longer name is required")
|
||||
db = database.get_db()
|
||||
|
||||
path = derive_path(name)
|
||||
if db.projects.find_one({"user": user["_id"], "path": path}, {"_id": 1}):
|
||||
raise util.errors.BadRequest("Bad Name")
|
||||
groups = data.get("groupVisibility", [])
|
||||
group_visibility = []
|
||||
for group in groups:
|
||||
group_visibility.append(ObjectId(group))
|
||||
proj = {
|
||||
"name": name,
|
||||
"description": data.get("description", ""),
|
||||
"visibility": data.get("visibility", "public"),
|
||||
"openSource": data.get("openSource", True),
|
||||
"groupVisibility": group_visibility,
|
||||
"path": path,
|
||||
"user": user["_id"],
|
||||
"createdAt": datetime.datetime.now(),
|
||||
}
|
||||
result = db.projects.insert_one(proj)
|
||||
proj["_id"] = result.inserted_id
|
||||
proj["owner"] = {"_id": user["_id"], "username": user["username"]}
|
||||
proj["fullName"] = user["username"] + "/" + proj["path"]
|
||||
return proj
|
||||
|
||||
path = derive_path(name)
|
||||
if db.projects.find_one({'user': user['_id'], 'path': path}, {'_id': 1}):
|
||||
raise util.errors.BadRequest('Bad Name')
|
||||
groups = data.get('groupVisibility', [])
|
||||
group_visibility = []
|
||||
for group in groups:
|
||||
group_visibility.append(ObjectId(group))
|
||||
proj = {
|
||||
'name': name,
|
||||
'description': data.get('description', ''),
|
||||
'visibility': data.get('visibility', 'public'),
|
||||
'openSource': data.get('openSource', True),
|
||||
'groupVisibility': group_visibility,
|
||||
'path': path,
|
||||
'user': user['_id'],
|
||||
'createdAt': datetime.datetime.now()
|
||||
}
|
||||
result = db.projects.insert_one(proj)
|
||||
proj['_id'] = result.inserted_id
|
||||
proj['owner'] = {'_id': user['_id'], 'username': user['username']}
|
||||
proj['fullName'] = user['username'] + '/' + proj['path']
|
||||
return proj
|
||||
|
||||
def get(user, username, path):
|
||||
db = database.get_db()
|
||||
owner = db.users.find_one({'username': username}, {'_id': 1, 'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1})
|
||||
if not owner: raise util.errors.NotFound('User not found')
|
||||
project = db.projects.find_one({'user': owner['_id'], 'path': path})
|
||||
if not project: raise util.errors.NotFound('Project not found')
|
||||
if not util.can_view_project(user, project):
|
||||
raise util.errors.Forbidden('This project is private')
|
||||
db = database.get_db()
|
||||
owner = db.users.find_one(
|
||||
{"username": username},
|
||||
{
|
||||
"_id": 1,
|
||||
"username": 1,
|
||||
"avatar": 1,
|
||||
"isSilverSupporter": 1,
|
||||
"isGoldSupporter": 1,
|
||||
},
|
||||
)
|
||||
if not owner:
|
||||
raise util.errors.NotFound("User not found")
|
||||
project = db.projects.find_one({"user": owner["_id"], "path": path})
|
||||
if not project:
|
||||
raise util.errors.NotFound("Project not found")
|
||||
if not util.can_view_project(user, project):
|
||||
raise util.errors.Forbidden("This project is private")
|
||||
|
||||
if "avatar" in owner:
|
||||
owner["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(owner["_id"], owner["avatar"])
|
||||
)
|
||||
project["owner"] = owner
|
||||
project["fullName"] = owner["username"] + "/" + project["path"]
|
||||
return project
|
||||
|
||||
if 'avatar' in owner:
|
||||
owner['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(owner['_id'], owner['avatar']))
|
||||
project['owner'] = owner
|
||||
project['fullName'] = owner['username'] + '/' + project['path']
|
||||
return project
|
||||
|
||||
def update(user, username, project_path, update):
|
||||
db = database.get_db()
|
||||
project = get_by_username(username, project_path)
|
||||
if not util.can_edit_project(user, project): raise util.errors.Forbidden('Forbidden')
|
||||
db = database.get_db()
|
||||
project = get_by_username(username, project_path)
|
||||
if not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden("Forbidden")
|
||||
|
||||
current_path = project_path
|
||||
if "name" in update:
|
||||
if len(update["name"]) < 3:
|
||||
raise util.errors.BadRequest("The name is too short.")
|
||||
path = derive_path(update["name"])
|
||||
if db.projects.find_one({"user": user["_id"], "path": path}, {"_id": 1}):
|
||||
raise util.errors.BadRequest(
|
||||
"You already have a project with a similar name"
|
||||
)
|
||||
update["path"] = path
|
||||
current_path = path
|
||||
update["groupVisibility"] = list(
|
||||
map(lambda g: ObjectId(g), update.get("groupVisibility", []))
|
||||
)
|
||||
allowed_keys = [
|
||||
"name",
|
||||
"description",
|
||||
"path",
|
||||
"visibility",
|
||||
"openSource",
|
||||
"groupVisibility",
|
||||
]
|
||||
updater = util.build_updater(update, allowed_keys)
|
||||
if updater:
|
||||
db.projects.update_one({"_id": project["_id"]}, updater)
|
||||
return get(user, username, current_path)
|
||||
|
||||
current_path = project_path
|
||||
if 'name' in update:
|
||||
if len(update['name']) < 3: raise util.errors.BadRequest('The name is too short.')
|
||||
path = derive_path(update['name'])
|
||||
if db.projects.find_one({'user': user['_id'], 'path': path}, {'_id': 1}):
|
||||
raise util.errors.BadRequest('You already have a project with a similar name')
|
||||
update['path'] = path
|
||||
current_path = path
|
||||
update['groupVisibility'] = list(map(lambda g: ObjectId(g), update.get('groupVisibility', [])))
|
||||
allowed_keys = ['name', 'description', 'path', 'visibility', 'openSource', 'groupVisibility']
|
||||
updater = util.build_updater(update, allowed_keys)
|
||||
if updater:
|
||||
db.projects.update_one({'_id': project['_id']}, updater)
|
||||
return get(user, username, current_path)
|
||||
|
||||
def delete(user, username, project_path):
|
||||
db = database.get_db()
|
||||
project = get_by_username(username, project_path)
|
||||
if not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden('Forbidden')
|
||||
db.projects.delete_one({'_id': project['_id']})
|
||||
db.objects.delete_many({'project': project['_id']})
|
||||
return {'deletedProject': project['_id'] }
|
||||
db = database.get_db()
|
||||
project = get_by_username(username, project_path)
|
||||
if not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden("Forbidden")
|
||||
db.projects.delete_one({"_id": project["_id"]})
|
||||
db.objects.delete_many({"project": project["_id"]})
|
||||
return {"deletedProject": project["_id"]}
|
||||
|
||||
|
||||
def get_objects(user, username, path):
|
||||
db = database.get_db()
|
||||
project = get_by_username(username, path)
|
||||
if not project: raise util.errors.NotFound('Project not found')
|
||||
if not util.can_view_project(user, project):
|
||||
raise util.errors.Forbidden('This project is private')
|
||||
db = database.get_db()
|
||||
project = get_by_username(username, path)
|
||||
if not project:
|
||||
raise util.errors.NotFound("Project not found")
|
||||
if not util.can_view_project(user, project):
|
||||
raise util.errors.Forbidden("This project is private")
|
||||
|
||||
objs = list(
|
||||
db.objects.find(
|
||||
{"project": project["_id"]},
|
||||
{
|
||||
"createdAt": 1,
|
||||
"name": 1,
|
||||
"description": 1,
|
||||
"project": 1,
|
||||
"preview": 1,
|
||||
"fullPreview": 1,
|
||||
"type": 1,
|
||||
"storedName": 1,
|
||||
"isImage": 1,
|
||||
"imageBlurHash": 1,
|
||||
"commentCount": 1,
|
||||
},
|
||||
)
|
||||
)
|
||||
for obj in objs:
|
||||
if obj["type"] == "file" and "storedName" in obj:
|
||||
obj["url"] = uploads.get_presigned_url(
|
||||
"projects/{0}/{1}".format(project["_id"], obj["storedName"])
|
||||
)
|
||||
if obj["type"] == "pattern" and "preview" in obj and ".png" in obj["preview"]:
|
||||
obj["previewUrl"] = uploads.get_presigned_url(
|
||||
"projects/{0}/{1}".format(project["_id"], obj["preview"])
|
||||
)
|
||||
del obj["preview"]
|
||||
if obj.get("fullPreview"):
|
||||
obj["fullPreviewUrl"] = uploads.get_presigned_url(
|
||||
"projects/{0}/{1}".format(project["_id"], obj["fullPreview"])
|
||||
)
|
||||
return objs
|
||||
|
||||
objs = list(db.objects.find({'project': project['_id']}, {'createdAt': 1, 'name': 1, 'description': 1, 'project': 1, 'preview': 1, 'fullPreview': 1, 'type': 1, 'storedName': 1, 'isImage': 1, 'imageBlurHash': 1, 'commentCount': 1}))
|
||||
for obj in objs:
|
||||
if obj['type'] == 'file' and 'storedName' in obj:
|
||||
obj['url'] = uploads.get_presigned_url('projects/{0}/{1}'.format(project['_id'], obj['storedName']))
|
||||
if obj['type'] == 'pattern' and 'preview' in obj and '.png' in obj['preview']:
|
||||
obj['previewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(project['_id'], obj['preview']))
|
||||
del obj['preview']
|
||||
if obj.get('fullPreview'):
|
||||
obj['fullPreviewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(project['_id'], obj['fullPreview']))
|
||||
return objs
|
||||
|
||||
def create_object(user, username, path, data):
|
||||
if not data and not data.get('type'): raise util.errors.BadRequest('Invalid request')
|
||||
if not data.get('type'): raise util.errors.BadRequest('Object type is required.')
|
||||
db = database.get_db()
|
||||
project = get_by_username(username, path)
|
||||
if not util.can_edit_project(user, project): raise util.errors.Forbidden('Forbidden')
|
||||
file_count = db.objects.count_documents({'project': project['_id']})
|
||||
if not data and not data.get("type"):
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
if not data.get("type"):
|
||||
raise util.errors.BadRequest("Object type is required.")
|
||||
db = database.get_db()
|
||||
project = get_by_username(username, path)
|
||||
if not util.can_edit_project(user, project):
|
||||
raise util.errors.Forbidden("Forbidden")
|
||||
|
||||
if data['type'] == 'file':
|
||||
if not 'storedName' in data:
|
||||
raise util.errors.BadRequest('File stored name must be included')
|
||||
obj = {
|
||||
'project': project['_id'],
|
||||
'name': data.get('name', 'Untitled file'),
|
||||
'storedName': data['storedName'],
|
||||
'createdAt': datetime.datetime.now(),
|
||||
'type': 'file',
|
||||
}
|
||||
if re.search(r'(.jpg)|(.png)|(.jpeg)|(.gif)$', data['storedName'].lower()):
|
||||
obj['isImage'] = True
|
||||
result = db.objects.insert_one(obj)
|
||||
obj['_id'] = result.inserted_id
|
||||
obj['url'] = uploads.get_presigned_url('projects/{0}/{1}'.format(project['_id'], obj['storedName']))
|
||||
if obj.get('isImage'):
|
||||
def handle_cb(h):
|
||||
db.objects.update_one({'_id': obj['_id']}, {'$set': {'imageBlurHash': h}})
|
||||
uploads.blur_image('projects/' + str(project['_id']) + '/' + data['storedName'], handle_cb)
|
||||
return obj
|
||||
if data['type'] == 'pattern':
|
||||
obj = {
|
||||
'project': project['_id'],
|
||||
'createdAt': datetime.datetime.now(),
|
||||
'type': 'pattern',
|
||||
}
|
||||
if data.get('wif'):
|
||||
try:
|
||||
pattern = wif.loads(data['wif'])
|
||||
if pattern:
|
||||
obj['name'] = pattern['name']
|
||||
obj['pattern'] = pattern
|
||||
except Exception as e:
|
||||
raise util.errors.BadRequest('Unable to load WIF file. It is either invalid or in a format we cannot understand.')
|
||||
else:
|
||||
pattern = default_pattern.copy()
|
||||
pattern['warp'].update({'shafts': data.get('shafts', 8)})
|
||||
pattern['weft'].update({'treadles': data.get('treadles', 8)})
|
||||
obj['name'] = data.get('name') or 'Untitled Pattern'
|
||||
obj['pattern'] = pattern
|
||||
result = db.objects.insert_one(obj)
|
||||
obj['_id'] = result.inserted_id
|
||||
images = wif.generate_images(obj)
|
||||
if images:
|
||||
db.objects.update_one({'_id': obj['_id']}, {'$set': images})
|
||||
if data["type"] == "file":
|
||||
if "storedName" not in data:
|
||||
raise util.errors.BadRequest("File stored name must be included")
|
||||
obj = {
|
||||
"project": project["_id"],
|
||||
"name": data.get("name", "Untitled file"),
|
||||
"storedName": data["storedName"],
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"type": "file",
|
||||
}
|
||||
if re.search(r"(.jpg)|(.png)|(.jpeg)|(.gif)$", data["storedName"].lower()):
|
||||
obj["isImage"] = True
|
||||
result = db.objects.insert_one(obj)
|
||||
obj["_id"] = result.inserted_id
|
||||
obj["url"] = uploads.get_presigned_url(
|
||||
"projects/{0}/{1}".format(project["_id"], obj["storedName"])
|
||||
)
|
||||
if obj.get("isImage"):
|
||||
|
||||
return objects.get(user, obj['_id'])
|
||||
raise util.errors.BadRequest('Unable to create object')
|
||||
def handle_cb(h):
|
||||
db.objects.update_one(
|
||||
{"_id": obj["_id"]}, {"$set": {"imageBlurHash": h}}
|
||||
)
|
||||
|
||||
uploads.blur_image(
|
||||
"projects/" + str(project["_id"]) + "/" + data["storedName"], handle_cb
|
||||
)
|
||||
return obj
|
||||
if data["type"] == "pattern":
|
||||
obj = {
|
||||
"project": project["_id"],
|
||||
"createdAt": datetime.datetime.now(),
|
||||
"type": "pattern",
|
||||
}
|
||||
if data.get("wif"):
|
||||
try:
|
||||
pattern = wif.loads(data["wif"])
|
||||
if pattern:
|
||||
obj["name"] = pattern["name"]
|
||||
obj["pattern"] = pattern
|
||||
except Exception:
|
||||
raise util.errors.BadRequest(
|
||||
"Unable to load WIF file. It is either invalid or in a format we cannot understand."
|
||||
)
|
||||
else:
|
||||
pattern = default_pattern.copy()
|
||||
pattern["warp"].update({"shafts": data.get("shafts", 8)})
|
||||
pattern["weft"].update({"treadles": data.get("treadles", 8)})
|
||||
obj["name"] = data.get("name") or "Untitled Pattern"
|
||||
obj["pattern"] = pattern
|
||||
result = db.objects.insert_one(obj)
|
||||
obj["_id"] = result.inserted_id
|
||||
images = wif.generate_images(obj)
|
||||
if images:
|
||||
db.objects.update_one({"_id": obj["_id"]}, {"$set": images})
|
||||
|
||||
return objects.get(user, obj["_id"])
|
||||
raise util.errors.BadRequest("Unable to create object")
|
||||
|
@ -1,35 +1,54 @@
|
||||
import re, datetime
|
||||
import pymongo
|
||||
from bson.objectid import ObjectId
|
||||
from util import database, util, mail
|
||||
from api import uploads, groups
|
||||
from util import database, util
|
||||
from api import uploads
|
||||
|
||||
|
||||
def get_users(user):
|
||||
db = database.get_db()
|
||||
if not util.is_root(user): raise util.errors.Forbidden('Not allowed')
|
||||
users = list(db.users.find({}, {'username': 1, 'avatar': 1, 'email': 1, 'createdAt': 1, 'lastSeenAt': 1, 'roles': 1, 'groups': 1}).sort('lastSeenAt', -1).limit(200))
|
||||
group_ids = []
|
||||
for u in users: group_ids += u.get('groups', [])
|
||||
groups = list(db.groups.find({'_id': {'$in': group_ids}}, {'name': 1}))
|
||||
projects = list(db.projects.find({}, {'name': 1, 'path': 1, 'user': 1}))
|
||||
for u in users:
|
||||
if 'avatar' in u:
|
||||
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(u['_id']), u['avatar']))
|
||||
u['projects'] = []
|
||||
for p in projects:
|
||||
if p['user'] == u['_id']:
|
||||
u['projects'].append(p)
|
||||
u['groupMemberships'] = []
|
||||
if u.get('groups'):
|
||||
for g in groups:
|
||||
if g['_id'] in u.get('groups', []):
|
||||
u['groupMemberships'].append(g)
|
||||
return {'users': users}
|
||||
db = database.get_db()
|
||||
if not util.is_root(user):
|
||||
raise util.errors.Forbidden("Not allowed")
|
||||
users = list(
|
||||
db.users.find(
|
||||
{},
|
||||
{
|
||||
"username": 1,
|
||||
"avatar": 1,
|
||||
"email": 1,
|
||||
"createdAt": 1,
|
||||
"lastSeenAt": 1,
|
||||
"roles": 1,
|
||||
"groups": 1,
|
||||
},
|
||||
)
|
||||
.sort("lastSeenAt", -1)
|
||||
.limit(200)
|
||||
)
|
||||
group_ids = []
|
||||
for u in users:
|
||||
group_ids += u.get("groups", [])
|
||||
groups = list(db.groups.find({"_id": {"$in": group_ids}}, {"name": 1}))
|
||||
projects = list(db.projects.find({}, {"name": 1, "path": 1, "user": 1}))
|
||||
for u in users:
|
||||
if "avatar" in u:
|
||||
u["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(str(u["_id"]), u["avatar"])
|
||||
)
|
||||
u["projects"] = []
|
||||
for p in projects:
|
||||
if p["user"] == u["_id"]:
|
||||
u["projects"].append(p)
|
||||
u["groupMemberships"] = []
|
||||
if u.get("groups"):
|
||||
for g in groups:
|
||||
if g["_id"] in u.get("groups", []):
|
||||
u["groupMemberships"].append(g)
|
||||
return {"users": users}
|
||||
|
||||
|
||||
def get_groups(user):
|
||||
db = database.get_db()
|
||||
if not util.is_root(user): raise util.errors.Forbidden('Not allowed')
|
||||
groups = list(db.groups.find({}))
|
||||
for group in groups:
|
||||
group['memberCount'] = db.users.count_documents({'groups': group['_id']})
|
||||
return {'groups': groups}
|
||||
db = database.get_db()
|
||||
if not util.is_root(user):
|
||||
raise util.errors.Forbidden("Not allowed")
|
||||
groups = list(db.groups.find({}))
|
||||
for group in groups:
|
||||
group["memberCount"] = db.users.count_documents({"groups": group["_id"]})
|
||||
return {"groups": groups}
|
||||
|
@ -1,117 +1,236 @@
|
||||
import re, random
|
||||
import re
|
||||
import random
|
||||
import pymongo
|
||||
from util import database, util
|
||||
from api import uploads
|
||||
|
||||
|
||||
def all(user, params):
|
||||
if not params or 'query' not in params: raise util.errors.BadRequest('Username parameter needed')
|
||||
expression = re.compile(params['query'], re.IGNORECASE)
|
||||
db = database.get_db()
|
||||
if not params or "query" not in params:
|
||||
raise util.errors.BadRequest("Query parameter needed")
|
||||
expression = re.compile(params["query"], re.IGNORECASE)
|
||||
db = database.get_db()
|
||||
|
||||
users = list(db.users.find({'username': expression}, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}).limit(10).sort('username', pymongo.ASCENDING))
|
||||
for u in users:
|
||||
if 'avatar' in u:
|
||||
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar']))
|
||||
|
||||
my_projects = list(db.projects.find({'user': user['_id']}, {'name': 1, 'path': 1}))
|
||||
objects = list(db.objects.find({'project': {'$in': list(map(lambda p: p['_id'], my_projects))}, 'name': expression}, {'name': 1, 'type': 1, 'isImage': 1, 'project': 1}))
|
||||
for o in objects:
|
||||
proj = next(p for p in my_projects if p['_id'] == o['project'])
|
||||
if proj:
|
||||
o['path'] = user['username'] + '/' + proj['path'] + '/' + str(o['_id'])
|
||||
|
||||
projects = list(db.projects.find({'name': expression, '$or': [
|
||||
{'user': user['_id']},
|
||||
{'groupVisibility': {'$in': user.get('groups', [])}},
|
||||
{'visibility': 'public'}
|
||||
]}, {'name': 1, 'path': 1, 'user': 1}).limit(10))
|
||||
proj_users = list(db.users.find({'_id': {'$in': list(map(lambda p:p['user'], projects))}}, {'username': 1, 'avatar': 1}))
|
||||
for proj in projects:
|
||||
for proj_user in proj_users:
|
||||
if proj['user'] == proj_user['_id']:
|
||||
proj['owner'] = proj_user
|
||||
proj['fullName'] = proj_user['username'] + '/' + proj['path']
|
||||
if 'avatar' in proj_user:
|
||||
proj['owner']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(proj_user['_id'], proj_user['avatar']))
|
||||
users = list(
|
||||
db.users.find(
|
||||
{"username": expression},
|
||||
{"username": 1, "avatar": 1, "isSilverSupporter": 1, "isGoldSupporter": 1},
|
||||
)
|
||||
.limit(10)
|
||||
.sort("username", pymongo.ASCENDING)
|
||||
)
|
||||
for u in users:
|
||||
if "avatar" in u:
|
||||
u["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(u["_id"], u["avatar"])
|
||||
)
|
||||
|
||||
groups = list(db.groups.find({'name': expression, 'unlisted': {'$ne': True}}, {'name': 1, 'closed': 1}).limit(5))
|
||||
my_projects = list(db.projects.find({"user": user["_id"]}, {"name": 1, "path": 1}))
|
||||
objects = list(
|
||||
db.objects.find(
|
||||
{
|
||||
"project": {"$in": list(map(lambda p: p["_id"], my_projects))},
|
||||
"name": expression,
|
||||
},
|
||||
{"name": 1, "type": 1, "isImage": 1, "project": 1},
|
||||
)
|
||||
)
|
||||
for o in objects:
|
||||
proj = next(p for p in my_projects if p["_id"] == o["project"])
|
||||
if proj:
|
||||
o["path"] = user["username"] + "/" + proj["path"] + "/" + str(o["_id"])
|
||||
|
||||
projects = list(
|
||||
db.projects.find(
|
||||
{
|
||||
"name": expression,
|
||||
"$or": [
|
||||
{"user": user["_id"]},
|
||||
{"groupVisibility": {"$in": user.get("groups", [])}},
|
||||
{"visibility": "public"},
|
||||
],
|
||||
},
|
||||
{"name": 1, "path": 1, "user": 1},
|
||||
).limit(10)
|
||||
)
|
||||
proj_users = list(
|
||||
db.users.find(
|
||||
{"_id": {"$in": list(map(lambda p: p["user"], projects))}},
|
||||
{"username": 1, "avatar": 1},
|
||||
)
|
||||
)
|
||||
for proj in projects:
|
||||
for proj_user in proj_users:
|
||||
if proj["user"] == proj_user["_id"]:
|
||||
proj["owner"] = proj_user
|
||||
proj["fullName"] = proj_user["username"] + "/" + proj["path"]
|
||||
if "avatar" in proj_user:
|
||||
proj["owner"]["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(proj_user["_id"], proj_user["avatar"])
|
||||
)
|
||||
|
||||
groups = list(
|
||||
db.groups.find(
|
||||
{"name": expression, "unlisted": {"$ne": True}}, {"name": 1, "closed": 1}
|
||||
).limit(5)
|
||||
)
|
||||
|
||||
return {"users": users, "projects": projects, "groups": groups, "objects": objects}
|
||||
|
||||
return {'users': users, 'projects': projects, 'groups': groups, 'objects': objects}
|
||||
|
||||
def users(user, params):
|
||||
if not user: raise util.errors.Forbidden('You need to be logged in')
|
||||
if not params or 'username' not in params: raise util.errors.BadRequest('Username parameter needed')
|
||||
expression = re.compile(params['username'], re.IGNORECASE)
|
||||
db = database.get_db()
|
||||
users = list(db.users.find({'username': expression}, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}).limit(5).sort('username', pymongo.ASCENDING))
|
||||
for u in users:
|
||||
if 'avatar' in u:
|
||||
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar']))
|
||||
return {'users': users}
|
||||
if not user:
|
||||
raise util.errors.Forbidden("You need to be logged in")
|
||||
if not params or "username" not in params:
|
||||
raise util.errors.BadRequest("Username parameter needed")
|
||||
expression = re.compile(params["username"], re.IGNORECASE)
|
||||
db = database.get_db()
|
||||
users = list(
|
||||
db.users.find(
|
||||
{"username": expression},
|
||||
{"username": 1, "avatar": 1, "isSilverSupporter": 1, "isGoldSupporter": 1},
|
||||
)
|
||||
.limit(5)
|
||||
.sort("username", pymongo.ASCENDING)
|
||||
)
|
||||
for u in users:
|
||||
if "avatar" in u:
|
||||
u["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(u["_id"], u["avatar"])
|
||||
)
|
||||
return {"users": users}
|
||||
|
||||
def discover(user, count = 3):
|
||||
db = database.get_db()
|
||||
projects = []
|
||||
users = []
|
||||
|
||||
all_projects_query = {'name': {'$not': re.compile('my new project', re.IGNORECASE)}, 'visibility': 'public'}
|
||||
if user and user.get('_id'):
|
||||
all_projects_query['user'] = {'$ne': user['_id']}
|
||||
all_projects = list(db.projects.find(all_projects_query, {'name': 1, 'path': 1, 'user': 1}))
|
||||
random.shuffle(all_projects)
|
||||
for p in all_projects:
|
||||
if db.objects.find_one({'project': p['_id'], 'name': {'$ne': 'Untitled pattern'}}):
|
||||
owner = db.users.find_one({'_id': p['user']}, {'username': 1, 'avatar': 1})
|
||||
p['fullName'] = owner['username'] + '/' + p['path']
|
||||
p['owner'] = owner
|
||||
if 'avatar' in p['owner']:
|
||||
p['owner']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(p['owner']['_id'], p['owner']['avatar']))
|
||||
projects.append(p)
|
||||
if len(projects) >= count: break
|
||||
def discover(user, count=3):
|
||||
db = database.get_db()
|
||||
projects = []
|
||||
users = []
|
||||
|
||||
interest_fields = ['bio', 'avatar', 'website', 'facebook', 'twitter', 'instagram', 'location']
|
||||
all_users_query = {'$or': list(map(lambda f: {f: {'$exists': True}}, interest_fields))}
|
||||
if user and user.get('_id'):
|
||||
all_users_query['_id'] = {'$ne': user['_id']}
|
||||
all_users = list(db.users.find(all_users_query, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}))
|
||||
random.shuffle(all_users)
|
||||
for u in all_users:
|
||||
if 'avatar' in u:
|
||||
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar']))
|
||||
if user:
|
||||
u['following'] = u['_id'] in list(map(lambda f: f['user'], user.get('following', [])))
|
||||
users.append(u)
|
||||
if len(users) >= count: break
|
||||
all_projects_query = {
|
||||
"name": {"$not": re.compile("my new project", re.IGNORECASE)},
|
||||
"visibility": "public",
|
||||
}
|
||||
if user and user.get("_id"):
|
||||
all_projects_query["user"] = {"$ne": user["_id"]}
|
||||
all_projects = list(
|
||||
db.projects.find(all_projects_query, {"name": 1, "path": 1, "user": 1})
|
||||
)
|
||||
random.shuffle(all_projects)
|
||||
for p in all_projects:
|
||||
if db.objects.find_one(
|
||||
{"project": p["_id"], "name": {"$ne": "Untitled pattern"}}
|
||||
):
|
||||
owner = db.users.find_one({"_id": p["user"]}, {"username": 1, "avatar": 1})
|
||||
p["fullName"] = owner["username"] + "/" + p["path"]
|
||||
p["owner"] = owner
|
||||
if "avatar" in p["owner"]:
|
||||
p["owner"]["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(p["owner"]["_id"], p["owner"]["avatar"])
|
||||
)
|
||||
projects.append(p)
|
||||
if len(projects) >= count:
|
||||
break
|
||||
|
||||
return {
|
||||
'highlightProjects': projects,
|
||||
'highlightUsers': users,
|
||||
}
|
||||
interest_fields = [
|
||||
"bio",
|
||||
"avatar",
|
||||
"website",
|
||||
"facebook",
|
||||
"twitter",
|
||||
"instagram",
|
||||
"location",
|
||||
]
|
||||
all_users_query = {
|
||||
"$or": list(map(lambda f: {f: {"$exists": True}}, interest_fields))
|
||||
}
|
||||
if user and user.get("_id"):
|
||||
all_users_query["_id"] = {"$ne": user["_id"]}
|
||||
all_users = list(
|
||||
db.users.find(
|
||||
all_users_query,
|
||||
{"username": 1, "avatar": 1, "isSilverSupporter": 1, "isGoldSupporter": 1},
|
||||
)
|
||||
)
|
||||
random.shuffle(all_users)
|
||||
for u in all_users:
|
||||
if "avatar" in u:
|
||||
u["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(u["_id"], u["avatar"])
|
||||
)
|
||||
if user:
|
||||
u["following"] = u["_id"] in list(
|
||||
map(lambda f: f["user"], user.get("following", []))
|
||||
)
|
||||
users.append(u)
|
||||
if len(users) >= count:
|
||||
break
|
||||
|
||||
def explore(page = 1):
|
||||
db = database.get_db()
|
||||
per_page = 10
|
||||
|
||||
project_map = {}
|
||||
user_map = {}
|
||||
all_public_projects = list(db.projects.find({'name': {'$not': re.compile('my new project', re.IGNORECASE)}, 'visibility': 'public'}, {'name': 1, 'path': 1, 'user': 1}))
|
||||
all_public_project_ids = list(map(lambda p: p['_id'], all_public_projects))
|
||||
for project in all_public_projects:
|
||||
project_map[project['_id']] = project
|
||||
objects = list(db.objects.find({'project': {'$in': all_public_project_ids}, 'name': {'$not': re.compile('untitled pattern', re.IGNORECASE)}, 'preview': {'$exists': True}}, {'project': 1, 'name': 1, 'createdAt': 1, 'type': 1, 'preview': 1}).sort('createdAt', pymongo.DESCENDING).skip((page - 1) * per_page).limit(per_page))
|
||||
for object in objects:
|
||||
object['projectObject'] = project_map.get(object['project'])
|
||||
if 'preview' in object and '.png' in object['preview']:
|
||||
object['previewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(object['project'], object['preview']))
|
||||
del object['preview']
|
||||
authors = list(db.users.find({'_id': {'$in': list(map(lambda o: o.get('projectObject', {}).get('user'), objects))}}, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}))
|
||||
for a in authors:
|
||||
if 'avatar' in a:
|
||||
a['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(a['_id'], a['avatar']))
|
||||
user_map[a['_id']] = a
|
||||
for object in objects:
|
||||
object['userObject'] = user_map.get(object.get('projectObject', {}).get('user'))
|
||||
object['projectObject']['owner'] = user_map.get(object.get('projectObject', {}).get('user'))
|
||||
|
||||
return {'objects': objects}
|
||||
|
||||
return {
|
||||
"highlightProjects": projects,
|
||||
"highlightUsers": users,
|
||||
}
|
||||
|
||||
|
||||
def explore(page=1):
|
||||
db = database.get_db()
|
||||
per_page = 10
|
||||
|
||||
project_map = {}
|
||||
user_map = {}
|
||||
all_public_projects = list(
|
||||
db.projects.find(
|
||||
{
|
||||
"name": {"$not": re.compile("my new project", re.IGNORECASE)},
|
||||
"visibility": "public",
|
||||
},
|
||||
{"name": 1, "path": 1, "user": 1},
|
||||
)
|
||||
)
|
||||
all_public_project_ids = list(map(lambda p: p["_id"], all_public_projects))
|
||||
for project in all_public_projects:
|
||||
project_map[project["_id"]] = project
|
||||
objects = list(
|
||||
db.objects.find(
|
||||
{
|
||||
"project": {"$in": all_public_project_ids},
|
||||
"name": {"$not": re.compile("untitled pattern", re.IGNORECASE)},
|
||||
"preview": {"$exists": True},
|
||||
},
|
||||
{"project": 1, "name": 1, "createdAt": 1, "type": 1, "preview": 1},
|
||||
)
|
||||
.sort("createdAt", pymongo.DESCENDING)
|
||||
.skip((page - 1) * per_page)
|
||||
.limit(per_page)
|
||||
)
|
||||
for object in objects:
|
||||
object["projectObject"] = project_map.get(object["project"])
|
||||
if "preview" in object and ".png" in object["preview"]:
|
||||
object["previewUrl"] = uploads.get_presigned_url(
|
||||
"projects/{0}/{1}".format(object["project"], object["preview"])
|
||||
)
|
||||
del object["preview"]
|
||||
authors = list(
|
||||
db.users.find(
|
||||
{
|
||||
"_id": {
|
||||
"$in": list(
|
||||
map(lambda o: o.get("projectObject", {}).get("user"), objects)
|
||||
)
|
||||
}
|
||||
},
|
||||
{"username": 1, "avatar": 1, "isSilverSupporter": 1, "isGoldSupporter": 1},
|
||||
)
|
||||
)
|
||||
for a in authors:
|
||||
if "avatar" in a:
|
||||
a["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(a["_id"], a["avatar"])
|
||||
)
|
||||
user_map[a["_id"]] = a
|
||||
for object in objects:
|
||||
object["userObject"] = user_map.get(object.get("projectObject", {}).get("user"))
|
||||
object["projectObject"]["owner"] = user_map.get(
|
||||
object.get("projectObject", {}).get("user")
|
||||
)
|
||||
|
||||
return {"objects": objects}
|
||||
|
@ -2,35 +2,40 @@ import datetime
|
||||
from bson.objectid import ObjectId
|
||||
from util import database, util
|
||||
|
||||
|
||||
def list_for_user(user):
|
||||
db = database.get_db()
|
||||
snippets = db.snippets.find({'user': user['_id']}).sort('createdAt', -1)
|
||||
return {'snippets': list(snippets)}
|
||||
db = database.get_db()
|
||||
snippets = db.snippets.find({"user": user["_id"]}).sort("createdAt", -1)
|
||||
return {"snippets": list(snippets)}
|
||||
|
||||
|
||||
def create(user, data):
|
||||
if not data: raise util.errors.BadRequest('Invalid request')
|
||||
name = data.get('name', '')
|
||||
snippet_type = data.get('type', '')
|
||||
if len(name) < 3: raise util.errors.BadRequest('A longer name is required')
|
||||
if snippet_type not in ['warp', 'weft']:
|
||||
raise util.errors.BadRequest('Invalid snippet type')
|
||||
db = database.get_db()
|
||||
snippet = {
|
||||
'name': name,
|
||||
'user': user['_id'],
|
||||
'createdAt': datetime.datetime.utcnow(),
|
||||
'type': snippet_type,
|
||||
'threading': data.get('threading', []),
|
||||
'treadling': data.get('treadling', []),
|
||||
}
|
||||
result = db.snippets.insert_one(snippet)
|
||||
snippet['_id'] = result.inserted_id
|
||||
return snippet
|
||||
if not data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
name = data.get("name", "")
|
||||
snippet_type = data.get("type", "")
|
||||
if len(name) < 3:
|
||||
raise util.errors.BadRequest("A longer name is required")
|
||||
if snippet_type not in ["warp", "weft"]:
|
||||
raise util.errors.BadRequest("Invalid snippet type")
|
||||
db = database.get_db()
|
||||
snippet = {
|
||||
"name": name,
|
||||
"user": user["_id"],
|
||||
"createdAt": datetime.datetime.utcnow(),
|
||||
"type": snippet_type,
|
||||
"threading": data.get("threading", []),
|
||||
"treadling": data.get("treadling", []),
|
||||
}
|
||||
result = db.snippets.insert_one(snippet)
|
||||
snippet["_id"] = result.inserted_id
|
||||
return snippet
|
||||
|
||||
|
||||
def delete(user, id):
|
||||
db = database.get_db()
|
||||
snippet = db.snippets.find_one({'_id': ObjectId(id), 'user': user['_id']})
|
||||
if not snippet:
|
||||
raise util.errors.NotFound('Snippet not found')
|
||||
db.snippets.delete_one({'_id': snippet['_id']})
|
||||
return {'deletedSnippet': snippet['_id'] }
|
||||
db = database.get_db()
|
||||
snippet = db.snippets.find_one({"_id": ObjectId(id), "user": user["_id"]})
|
||||
if not snippet:
|
||||
raise util.errors.NotFound("Snippet not found")
|
||||
db.snippets.delete_one({"_id": snippet["_id"]})
|
||||
return {"deletedSnippet": snippet["_id"]}
|
||||
|
@ -1,90 +1,98 @@
|
||||
import os, time, re
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
from threading import Thread
|
||||
from bson.objectid import ObjectId
|
||||
import boto3
|
||||
import blurhash
|
||||
from util import database, util
|
||||
|
||||
|
||||
def sanitise_filename(s):
|
||||
bad_chars = re.compile('[^a-zA-Z0-9_.]')
|
||||
s = bad_chars.sub('_', s)
|
||||
return s
|
||||
bad_chars = re.compile("[^a-zA-Z0-9_.]")
|
||||
s = bad_chars.sub("_", s)
|
||||
return s
|
||||
|
||||
|
||||
def get_s3():
|
||||
session = boto3.session.Session()
|
||||
session = boto3.session.Session()
|
||||
|
||||
s3_client = session.client(
|
||||
service_name="s3",
|
||||
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
|
||||
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
|
||||
endpoint_url=os.environ["AWS_S3_ENDPOINT"],
|
||||
)
|
||||
return s3_client
|
||||
|
||||
s3_client = session.client(
|
||||
service_name='s3',
|
||||
aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'],
|
||||
aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY'],
|
||||
endpoint_url=os.environ['AWS_S3_ENDPOINT'],
|
||||
)
|
||||
return s3_client
|
||||
|
||||
def get_presigned_url(path):
|
||||
return os.environ['AWS_S3_ENDPOINT'] + os.environ['AWS_S3_BUCKET'] + '/' + path
|
||||
s3 = get_s3()
|
||||
return s3.generate_presigned_url('get_object',
|
||||
Params = {
|
||||
'Bucket': os.environ['AWS_S3_BUCKET'],
|
||||
'Key': path
|
||||
}
|
||||
)
|
||||
return os.environ["AWS_S3_ENDPOINT"] + os.environ["AWS_S3_BUCKET"] + "/" + path
|
||||
s3 = get_s3()
|
||||
return s3.generate_presigned_url(
|
||||
"get_object", Params={"Bucket": os.environ["AWS_S3_BUCKET"], "Key": path}
|
||||
)
|
||||
|
||||
|
||||
def upload_file(path, data):
|
||||
s3 = get_s3()
|
||||
s3.upload_fileobj(
|
||||
data,
|
||||
os.environ['AWS_S3_BUCKET'],
|
||||
path,
|
||||
)
|
||||
s3 = get_s3()
|
||||
s3.upload_fileobj(
|
||||
data,
|
||||
os.environ["AWS_S3_BUCKET"],
|
||||
path,
|
||||
)
|
||||
|
||||
|
||||
def get_file(key):
|
||||
s3 = get_s3()
|
||||
return s3.get_object(
|
||||
Bucket = os.environ['AWS_S3_BUCKET'],
|
||||
Key = key
|
||||
)
|
||||
s3 = get_s3()
|
||||
return s3.get_object(Bucket=os.environ["AWS_S3_BUCKET"], Key=key)
|
||||
|
||||
def generate_file_upload_request(user, file_name, file_size, file_type, for_type, for_id):
|
||||
if int(file_size) > (1024 * 1024 * 30): # 30MB
|
||||
raise util.errors.BadRequest('File size is too big')
|
||||
db = database.get_db()
|
||||
allowed = False
|
||||
path = ''
|
||||
if for_type == 'project':
|
||||
project = db.projects.find_one(ObjectId(for_id))
|
||||
allowed = project and util.can_edit_project(user, project)
|
||||
path = 'projects/' + for_id + '/'
|
||||
if for_type == 'user':
|
||||
allowed = for_id == str(user['_id'])
|
||||
path = 'users/' + for_id + '/'
|
||||
if for_type == 'group':
|
||||
allowed = ObjectId(for_id) in user.get('groups', [])
|
||||
path = 'groups/' + for_id + '/'
|
||||
if not allowed:
|
||||
raise util.errors.Forbidden('You\'re not allowed to upload this file')
|
||||
|
||||
file_body, file_extension = os.path.splitext(file_name)
|
||||
new_name = sanitise_filename('{0}_{1}{2}'.format(file_body or file_name, int(time.time()), file_extension or ''))
|
||||
s3 = get_s3()
|
||||
signed_url = s3.generate_presigned_url('put_object',
|
||||
Params = {
|
||||
'Bucket': os.environ['AWS_S3_BUCKET'],
|
||||
'Key': path + new_name,
|
||||
'ContentType': file_type
|
||||
}
|
||||
)
|
||||
return {
|
||||
'signedRequest': signed_url,
|
||||
'fileName': new_name
|
||||
}
|
||||
def generate_file_upload_request(
|
||||
user, file_name, file_size, file_type, for_type, for_id
|
||||
):
|
||||
if int(file_size) > (1024 * 1024 * 30): # 30MB
|
||||
raise util.errors.BadRequest("File size is too big")
|
||||
db = database.get_db()
|
||||
allowed = False
|
||||
path = ""
|
||||
if for_type == "project":
|
||||
project = db.projects.find_one(ObjectId(for_id))
|
||||
allowed = project and util.can_edit_project(user, project)
|
||||
path = "projects/" + for_id + "/"
|
||||
if for_type == "user":
|
||||
allowed = for_id == str(user["_id"])
|
||||
path = "users/" + for_id + "/"
|
||||
if for_type == "group":
|
||||
allowed = ObjectId(for_id) in user.get("groups", [])
|
||||
path = "groups/" + for_id + "/"
|
||||
if not allowed:
|
||||
raise util.errors.Forbidden("You're not allowed to upload this file")
|
||||
|
||||
file_body, file_extension = os.path.splitext(file_name)
|
||||
new_name = sanitise_filename(
|
||||
"{0}_{1}{2}".format(
|
||||
file_body or file_name, int(time.time()), file_extension or ""
|
||||
)
|
||||
)
|
||||
s3 = get_s3()
|
||||
signed_url = s3.generate_presigned_url(
|
||||
"put_object",
|
||||
Params={
|
||||
"Bucket": os.environ["AWS_S3_BUCKET"],
|
||||
"Key": path + new_name,
|
||||
"ContentType": file_type,
|
||||
},
|
||||
)
|
||||
return {"signedRequest": signed_url, "fileName": new_name}
|
||||
|
||||
|
||||
def handle_blur_image(key, func):
|
||||
f = get_file(key)['Body']
|
||||
bhash = blurhash.encode(f, x_components=4, y_components=3)
|
||||
func(bhash)
|
||||
f = get_file(key)["Body"]
|
||||
bhash = blurhash.encode(f, x_components=4, y_components=3)
|
||||
func(bhash)
|
||||
|
||||
|
||||
def blur_image(key, func):
|
||||
thr = Thread(target=handle_blur_image, args=[key, func])
|
||||
thr.start()
|
||||
thr = Thread(target=handle_blur_image, args=[key, func])
|
||||
thr.start()
|
||||
|
515
api/api/users.py
515
api/api/users.py
@ -3,216 +3,343 @@ from bson.objectid import ObjectId
|
||||
from util import database, util
|
||||
from api import uploads
|
||||
|
||||
|
||||
def me(user):
|
||||
db = database.get_db()
|
||||
return {
|
||||
'_id': user['_id'],
|
||||
'username': user['username'],
|
||||
'bio': user.get('bio'),
|
||||
'email': user.get('email'),
|
||||
'avatar': user.get('avatar'),
|
||||
'avatarUrl': user.get('avatar') and uploads.get_presigned_url('users/{0}/{1}'.format(user['_id'], user['avatar'])),
|
||||
'roles': user.get('roles', []),
|
||||
'groups': user.get('groups', []),
|
||||
'subscriptions': user.get('subscriptions'),
|
||||
'finishedTours': user.get('completedTours', []) + user.get('skippedTours', []),
|
||||
'isSilverSupporter': user.get('isSilverSupporter'),
|
||||
'isGoldSupporter': user.get('isGoldSupporter'),
|
||||
'followerCount': db.users.count_documents({'following.user': user['_id']}),
|
||||
}
|
||||
db = database.get_db()
|
||||
return {
|
||||
"_id": user["_id"],
|
||||
"username": user["username"],
|
||||
"bio": user.get("bio"),
|
||||
"email": user.get("email"),
|
||||
"avatar": user.get("avatar"),
|
||||
"avatarUrl": user.get("avatar")
|
||||
and uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(user["_id"], user["avatar"])
|
||||
),
|
||||
"roles": user.get("roles", []),
|
||||
"groups": user.get("groups", []),
|
||||
"subscriptions": user.get("subscriptions"),
|
||||
"finishedTours": user.get("completedTours", []) + user.get("skippedTours", []),
|
||||
"isSilverSupporter": user.get("isSilverSupporter"),
|
||||
"isGoldSupporter": user.get("isGoldSupporter"),
|
||||
"followerCount": db.users.count_documents({"following.user": user["_id"]}),
|
||||
}
|
||||
|
||||
|
||||
def get(user, username):
|
||||
db = database.get_db()
|
||||
fetch_user = db.users.find_one({'username': username}, {'username': 1, 'createdAt': 1, 'avatar': 1, 'avatarBlurHash': 1, 'bio': 1, 'location': 1, 'website': 1, 'twitter': 1, 'facebook': 1, 'linkedIn': 1, 'instagram': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1})
|
||||
if not fetch_user:
|
||||
raise util.errors.NotFound('User not found')
|
||||
project_query = {'user': fetch_user['_id']}
|
||||
if not user or not user['_id'] == fetch_user['_id']:
|
||||
project_query['visibility'] = 'public'
|
||||
db = database.get_db()
|
||||
fetch_user = db.users.find_one(
|
||||
{"username": username},
|
||||
{
|
||||
"username": 1,
|
||||
"createdAt": 1,
|
||||
"avatar": 1,
|
||||
"avatarBlurHash": 1,
|
||||
"bio": 1,
|
||||
"location": 1,
|
||||
"website": 1,
|
||||
"twitter": 1,
|
||||
"facebook": 1,
|
||||
"linkedIn": 1,
|
||||
"instagram": 1,
|
||||
"isSilverSupporter": 1,
|
||||
"isGoldSupporter": 1,
|
||||
},
|
||||
)
|
||||
if not fetch_user:
|
||||
raise util.errors.NotFound("User not found")
|
||||
project_query = {"user": fetch_user["_id"]}
|
||||
if not user or not user["_id"] == fetch_user["_id"]:
|
||||
project_query["visibility"] = "public"
|
||||
|
||||
if 'avatar' in fetch_user:
|
||||
fetch_user['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(fetch_user['_id']), fetch_user['avatar']))
|
||||
if user:
|
||||
fetch_user['following'] = fetch_user['_id'] in list(map(lambda f: f['user'], user.get('following', [])))
|
||||
if "avatar" in fetch_user:
|
||||
fetch_user["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(str(fetch_user["_id"]), fetch_user["avatar"])
|
||||
)
|
||||
if user:
|
||||
fetch_user["following"] = fetch_user["_id"] in list(
|
||||
map(lambda f: f["user"], user.get("following", []))
|
||||
)
|
||||
|
||||
user_projects = list(
|
||||
db.projects.find(
|
||||
project_query, {"name": 1, "path": 1, "description": 1, "visibility": 1}
|
||||
).limit(15)
|
||||
)
|
||||
for project in user_projects:
|
||||
project["fullName"] = fetch_user["username"] + "/" + project["path"]
|
||||
project["owner"] = {
|
||||
"_id": fetch_user["_id"],
|
||||
"username": fetch_user["username"],
|
||||
"avatar": fetch_user.get("avatar"),
|
||||
"avatarUrl": fetch_user.get("avatarUrl"),
|
||||
}
|
||||
fetch_user["projects"] = user_projects
|
||||
|
||||
return fetch_user
|
||||
|
||||
user_projects = list(db.projects.find(project_query, {'name': 1, 'path': 1, 'description': 1, 'visibility': 1}).limit(15))
|
||||
for project in user_projects:
|
||||
project['fullName'] = fetch_user['username'] + '/' + project['path']
|
||||
project['owner'] = {
|
||||
'_id': fetch_user['_id'],
|
||||
'username': fetch_user['username'],
|
||||
'avatar': fetch_user.get('avatar'),
|
||||
'avatarUrl': fetch_user.get('avatarUrl'),
|
||||
}
|
||||
fetch_user['projects'] = user_projects
|
||||
|
||||
return fetch_user
|
||||
|
||||
def update(user, username, data):
|
||||
if not data: raise util.errors.BadRequest('Invalid request')
|
||||
db = database.get_db()
|
||||
if user['username'] != username:
|
||||
raise util.errors.Forbidden('Not allowed')
|
||||
allowed_keys = ['username', 'avatar', 'bio', 'location', 'website', 'twitter', 'facebook', 'linkedIn', 'instagram']
|
||||
if 'username' in data:
|
||||
if not data.get('username') or len(data['username']) < 3:
|
||||
raise util.errors.BadRequest('New username is not valid')
|
||||
if db.users.count_documents({'username': data['username'].lower()}):
|
||||
raise util.errors.BadRequest('A user with this username already exists')
|
||||
data['username'] = data['username'].lower()
|
||||
if data.get('avatar') and len(data['avatar']) > 3: # Not a default avatar
|
||||
def handle_cb(h):
|
||||
db.users.update_one({'_id': user['_id']}, {'$set': {'avatarBlurHash': h}})
|
||||
uploads.blur_image('users/' + str(user['_id']) + '/' + data['avatar'], handle_cb)
|
||||
updater = util.build_updater(data, allowed_keys)
|
||||
if updater:
|
||||
if 'avatar' in updater.get('$unset', {}): # Also unset blurhash if removing avatar
|
||||
updater['$unset']['avatarBlurHash'] = ''
|
||||
db.users.update_one({'username': username}, updater)
|
||||
return get(user, data.get('username', username))
|
||||
if not data:
|
||||
raise util.errors.BadRequest("Invalid request")
|
||||
db = database.get_db()
|
||||
if user["username"] != username:
|
||||
raise util.errors.Forbidden("Not allowed")
|
||||
allowed_keys = [
|
||||
"username",
|
||||
"avatar",
|
||||
"bio",
|
||||
"location",
|
||||
"website",
|
||||
"twitter",
|
||||
"facebook",
|
||||
"linkedIn",
|
||||
"instagram",
|
||||
]
|
||||
if "username" in data:
|
||||
if not data.get("username") or len(data["username"]) < 3:
|
||||
raise util.errors.BadRequest("New username is not valid")
|
||||
if db.users.count_documents({"username": data["username"].lower()}):
|
||||
raise util.errors.BadRequest("A user with this username already exists")
|
||||
data["username"] = data["username"].lower()
|
||||
if data.get("avatar") and len(data["avatar"]) > 3: # Not a default avatar
|
||||
|
||||
def handle_cb(h):
|
||||
db.users.update_one({"_id": user["_id"]}, {"$set": {"avatarBlurHash": h}})
|
||||
|
||||
uploads.blur_image(
|
||||
"users/" + str(user["_id"]) + "/" + data["avatar"], handle_cb
|
||||
)
|
||||
updater = util.build_updater(data, allowed_keys)
|
||||
if updater:
|
||||
if "avatar" in updater.get(
|
||||
"$unset", {}
|
||||
): # Also unset blurhash if removing avatar
|
||||
updater["$unset"]["avatarBlurHash"] = ""
|
||||
db.users.update_one({"username": username}, updater)
|
||||
return get(user, data.get("username", username))
|
||||
|
||||
|
||||
def finish_tour(user, username, tour, status):
|
||||
db = database.get_db()
|
||||
if user['username'] != username:
|
||||
raise util.errors.Forbidden('Not allowed')
|
||||
key = 'completedTours' if status == 'completed' else 'skippedTours'
|
||||
db.users.update_one({'_id': user['_id']}, {'$addToSet': {key: tour}})
|
||||
return {'finishedTour': tour}
|
||||
db = database.get_db()
|
||||
if user["username"] != username:
|
||||
raise util.errors.Forbidden("Not allowed")
|
||||
key = "completedTours" if status == "completed" else "skippedTours"
|
||||
db.users.update_one({"_id": user["_id"]}, {"$addToSet": {key: tour}})
|
||||
return {"finishedTour": tour}
|
||||
|
||||
|
||||
def get_projects(user, id):
|
||||
db = database.get_db()
|
||||
u = db.users.find_one(id, {'username': 1, 'avatar': 1})
|
||||
if not u: raise util.errors.NotFound('User not found')
|
||||
if 'avatar' in u: u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(u['_id']), u['avatar']))
|
||||
projects = []
|
||||
project_query = {'user': ObjectId(id)}
|
||||
if not user or not user['_id'] == ObjectId(id):
|
||||
project_query['visibility'] = 'public'
|
||||
for project in db.projects.find(project_query):
|
||||
project['owner'] = u
|
||||
project['fullName'] = u['username'] + '/' + project['path']
|
||||
projects.append(project)
|
||||
return projects
|
||||
db = database.get_db()
|
||||
u = db.users.find_one(id, {"username": 1, "avatar": 1})
|
||||
if not u:
|
||||
raise util.errors.NotFound("User not found")
|
||||
if "avatar" in u:
|
||||
u["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(str(u["_id"]), u["avatar"])
|
||||
)
|
||||
projects = []
|
||||
project_query = {"user": ObjectId(id)}
|
||||
if not user or not user["_id"] == ObjectId(id):
|
||||
project_query["visibility"] = "public"
|
||||
for project in db.projects.find(project_query):
|
||||
project["owner"] = u
|
||||
project["fullName"] = u["username"] + "/" + project["path"]
|
||||
projects.append(project)
|
||||
return projects
|
||||
|
||||
|
||||
def create_email_subscription(user, username, subscription):
|
||||
db = database.get_db()
|
||||
if user['username'] != username: raise util.errors.Forbidden('Forbidden')
|
||||
u = db.users.find_one({'username': username})
|
||||
db.users.update_one({'_id': u['_id']}, {'$addToSet': {'subscriptions.email': subscription}})
|
||||
subs = db.users.find_one(u['_id'], {'subscriptions': 1})
|
||||
return {'subscriptions': subs.get('subscriptions', {})}
|
||||
db = database.get_db()
|
||||
if user["username"] != username:
|
||||
raise util.errors.Forbidden("Forbidden")
|
||||
u = db.users.find_one({"username": username})
|
||||
db.users.update_one(
|
||||
{"_id": u["_id"]}, {"$addToSet": {"subscriptions.email": subscription}}
|
||||
)
|
||||
subs = db.users.find_one(u["_id"], {"subscriptions": 1})
|
||||
return {"subscriptions": subs.get("subscriptions", {})}
|
||||
|
||||
|
||||
def delete_email_subscription(user, username, subscription):
|
||||
db = database.get_db()
|
||||
if user['username'] != username: raise util.errors.Forbidden('Forbidden')
|
||||
u = db.users.find_one({'username': username})
|
||||
db.users.update_one({'_id': u['_id']}, {'$pull': {'subscriptions.email': subscription}})
|
||||
subs = db.users.find_one(u['_id'], {'subscriptions': 1})
|
||||
return {'subscriptions': subs.get('subscriptions', {})}
|
||||
db = database.get_db()
|
||||
if user["username"] != username:
|
||||
raise util.errors.Forbidden("Forbidden")
|
||||
u = db.users.find_one({"username": username})
|
||||
db.users.update_one(
|
||||
{"_id": u["_id"]}, {"$pull": {"subscriptions.email": subscription}}
|
||||
)
|
||||
subs = db.users.find_one(u["_id"], {"subscriptions": 1})
|
||||
return {"subscriptions": subs.get("subscriptions", {})}
|
||||
|
||||
|
||||
def create_follower(user, username):
|
||||
db = database.get_db()
|
||||
target_user = db.users.find_one({'username': username.lower()})
|
||||
if not target_user: raise util.errors.NotFound('User not found')
|
||||
if target_user['_id'] == user['_id']: raise util.errors.BadRequest('Cannot follow yourself')
|
||||
follow_object = {
|
||||
'user': target_user['_id'],
|
||||
'followedAt': datetime.datetime.utcnow(),
|
||||
}
|
||||
db.users.update_one({'_id': user['_id']}, {'$addToSet': {'following': follow_object}})
|
||||
return follow_object
|
||||
|
||||
db = database.get_db()
|
||||
target_user = db.users.find_one({"username": username.lower()})
|
||||
if not target_user:
|
||||
raise util.errors.NotFound("User not found")
|
||||
if target_user["_id"] == user["_id"]:
|
||||
raise util.errors.BadRequest("Cannot follow yourself")
|
||||
follow_object = {
|
||||
"user": target_user["_id"],
|
||||
"followedAt": datetime.datetime.utcnow(),
|
||||
}
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]}, {"$addToSet": {"following": follow_object}}
|
||||
)
|
||||
return follow_object
|
||||
|
||||
|
||||
def delete_follower(user, username):
|
||||
db = database.get_db()
|
||||
target_user = db.users.find_one({'username': username.lower()})
|
||||
if not target_user: raise util.errors.NotFound('User not found')
|
||||
db.users.update_one({'_id': user['_id']}, {'$pull': {'following': {'user': target_user['_id']}}})
|
||||
return {'unfollowed': True}
|
||||
|
||||
db = database.get_db()
|
||||
target_user = db.users.find_one({"username": username.lower()})
|
||||
if not target_user:
|
||||
raise util.errors.NotFound("User not found")
|
||||
db.users.update_one(
|
||||
{"_id": user["_id"]}, {"$pull": {"following": {"user": target_user["_id"]}}}
|
||||
)
|
||||
return {"unfollowed": True}
|
||||
|
||||
|
||||
def get_feed(user, username):
|
||||
db = database.get_db()
|
||||
if user['username'] != username: raise util.errors.Forbidden('Forbidden')
|
||||
following_user_ids = list(map(lambda f: f['user'], user.get('following', [])))
|
||||
following_project_ids = list(map(lambda p: p['_id'], db.projects.find({'user': {'$in': following_user_ids}, 'visibility': 'public'}, {'_id': 1})))
|
||||
one_year_ago = datetime.datetime.utcnow() - datetime.timedelta(days = 365)
|
||||
|
||||
# Fetch the items for the feed
|
||||
recent_projects = list(db.projects.find({
|
||||
'_id': {'$in': following_project_ids},
|
||||
'createdAt': {'$gt': one_year_ago},
|
||||
'visibility': 'public',
|
||||
}, {'user': 1, 'createdAt': 1, 'name': 1, 'path': 1, 'visibility': 1}).sort('createdAt', -1).limit(20))
|
||||
recent_objects = list(db.objects.find({
|
||||
'project': {'$in': following_project_ids},
|
||||
'createdAt': {'$gt': one_year_ago}
|
||||
}, {'project': 1, 'createdAt': 1, 'name': 1}).sort('createdAt', -1).limit(30))
|
||||
recent_comments = list(db.comments.find({
|
||||
'user': {'$in': following_user_ids},
|
||||
'createdAt': {'$gt': one_year_ago}
|
||||
}, {'user': 1, 'createdAt': 1, 'object': 1, 'content': 1}).sort('createdAt', -1).limit(30))
|
||||
|
||||
# Process objects (as don't know the user)
|
||||
object_project_ids = list(map(lambda o: o['project'], recent_objects))
|
||||
object_projects = list(db.projects.find({'_id': {'$in': object_project_ids}, 'visibility': 'public'}, {'user': 1}))
|
||||
for obj in recent_objects:
|
||||
for proj in object_projects:
|
||||
if obj['project'] == proj['_id']: obj['user'] = proj.get('user')
|
||||
|
||||
# Process comments as don't know the project
|
||||
comment_object_ids = list(map(lambda c: c['object'], recent_comments))
|
||||
comment_objects = list(db.objects.find({'_id': {'$in': comment_object_ids}}, {'project': 1}))
|
||||
for com in recent_comments:
|
||||
for obj in comment_objects:
|
||||
if com['object'] == obj['_id']: com['project'] = obj.get('project')
|
||||
|
||||
# Prepare the feed items, and sort it
|
||||
feed_items = []
|
||||
for p in recent_projects:
|
||||
p['feedType'] = 'project'
|
||||
feed_items.append(p)
|
||||
for o in recent_objects:
|
||||
o['feedType'] = 'object'
|
||||
feed_items.append(o)
|
||||
for c in recent_comments:
|
||||
c['feedType'] = 'comment'
|
||||
feed_items.append(c)
|
||||
feed_items.sort(key=lambda d: d['createdAt'], reverse = True)
|
||||
feed_items = feed_items[:20]
|
||||
|
||||
# Post-process the feed, adding user/project objects
|
||||
feed_user_ids = set()
|
||||
feed_project_ids = set()
|
||||
for f in feed_items:
|
||||
feed_user_ids.add(f.get('user'))
|
||||
feed_project_ids.add(f.get('project'))
|
||||
feed_projects = list(db.projects.find({'_id': {'$in': list(feed_project_ids)}, 'visibility': 'public'}, {'name': 1, 'path': 1, 'user': 1, 'visibility': 1}))
|
||||
feed_users = list(db.users.find({'$or': [
|
||||
{'_id': {'$in': list(feed_user_ids)}},
|
||||
{'_id': {'$in': list(map(lambda p: p['user'], feed_projects))}},
|
||||
]}, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}))
|
||||
for u in feed_users:
|
||||
if 'avatar' in u:
|
||||
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(u['_id']), u['avatar']))
|
||||
feed_user_map = {}
|
||||
feed_project_map = {}
|
||||
for u in feed_users: feed_user_map[str(u['_id'])] = u
|
||||
for p in feed_projects: feed_project_map[str(p['_id'])] = p
|
||||
for f in feed_items:
|
||||
if f.get('user') and feed_user_map.get(str(f['user'])):
|
||||
f['userObject'] = feed_user_map.get(str(f['user']))
|
||||
if f.get('project') and feed_project_map.get(str(f['project'])):
|
||||
f['projectObject'] = feed_project_map.get(str(f['project']))
|
||||
if f.get('projectObject', {}).get('user') and feed_user_map.get(str(f['projectObject']['user'])):
|
||||
f['projectObject']['userObject'] = feed_user_map.get(str(f['projectObject']['user']))
|
||||
|
||||
# Filter out orphaned or non-public comments/objects
|
||||
def filter_func(f):
|
||||
if f['feedType'] == 'comment' and not f.get('projectObject'):
|
||||
return False
|
||||
if f['feedType'] == 'object' and not f.get('projectObject'):
|
||||
return False
|
||||
return True
|
||||
feed_items = list(filter(filter_func, feed_items))
|
||||
|
||||
return {'feed': feed_items}
|
||||
|
||||
db = database.get_db()
|
||||
if user["username"] != username:
|
||||
raise util.errors.Forbidden("Forbidden")
|
||||
following_user_ids = list(map(lambda f: f["user"], user.get("following", [])))
|
||||
following_project_ids = list(
|
||||
map(
|
||||
lambda p: p["_id"],
|
||||
db.projects.find(
|
||||
{"user": {"$in": following_user_ids}, "visibility": "public"},
|
||||
{"_id": 1},
|
||||
),
|
||||
)
|
||||
)
|
||||
one_year_ago = datetime.datetime.utcnow() - datetime.timedelta(days=365)
|
||||
|
||||
# Fetch the items for the feed
|
||||
recent_projects = list(
|
||||
db.projects.find(
|
||||
{
|
||||
"_id": {"$in": following_project_ids},
|
||||
"createdAt": {"$gt": one_year_ago},
|
||||
"visibility": "public",
|
||||
},
|
||||
{"user": 1, "createdAt": 1, "name": 1, "path": 1, "visibility": 1},
|
||||
)
|
||||
.sort("createdAt", -1)
|
||||
.limit(20)
|
||||
)
|
||||
recent_objects = list(
|
||||
db.objects.find(
|
||||
{
|
||||
"project": {"$in": following_project_ids},
|
||||
"createdAt": {"$gt": one_year_ago},
|
||||
},
|
||||
{"project": 1, "createdAt": 1, "name": 1},
|
||||
)
|
||||
.sort("createdAt", -1)
|
||||
.limit(30)
|
||||
)
|
||||
recent_comments = list(
|
||||
db.comments.find(
|
||||
{"user": {"$in": following_user_ids}, "createdAt": {"$gt": one_year_ago}},
|
||||
{"user": 1, "createdAt": 1, "object": 1, "content": 1},
|
||||
)
|
||||
.sort("createdAt", -1)
|
||||
.limit(30)
|
||||
)
|
||||
|
||||
# Process objects (as don't know the user)
|
||||
object_project_ids = list(map(lambda o: o["project"], recent_objects))
|
||||
object_projects = list(
|
||||
db.projects.find(
|
||||
{"_id": {"$in": object_project_ids}, "visibility": "public"}, {"user": 1}
|
||||
)
|
||||
)
|
||||
for obj in recent_objects:
|
||||
for proj in object_projects:
|
||||
if obj["project"] == proj["_id"]:
|
||||
obj["user"] = proj.get("user")
|
||||
|
||||
# Process comments as don't know the project
|
||||
comment_object_ids = list(map(lambda c: c["object"], recent_comments))
|
||||
comment_objects = list(
|
||||
db.objects.find({"_id": {"$in": comment_object_ids}}, {"project": 1})
|
||||
)
|
||||
for com in recent_comments:
|
||||
for obj in comment_objects:
|
||||
if com["object"] == obj["_id"]:
|
||||
com["project"] = obj.get("project")
|
||||
|
||||
# Prepare the feed items, and sort it
|
||||
feed_items = []
|
||||
for p in recent_projects:
|
||||
p["feedType"] = "project"
|
||||
feed_items.append(p)
|
||||
for o in recent_objects:
|
||||
o["feedType"] = "object"
|
||||
feed_items.append(o)
|
||||
for c in recent_comments:
|
||||
c["feedType"] = "comment"
|
||||
feed_items.append(c)
|
||||
feed_items.sort(key=lambda d: d["createdAt"], reverse=True)
|
||||
feed_items = feed_items[:20]
|
||||
|
||||
# Post-process the feed, adding user/project objects
|
||||
feed_user_ids = set()
|
||||
feed_project_ids = set()
|
||||
for f in feed_items:
|
||||
feed_user_ids.add(f.get("user"))
|
||||
feed_project_ids.add(f.get("project"))
|
||||
feed_projects = list(
|
||||
db.projects.find(
|
||||
{"_id": {"$in": list(feed_project_ids)}, "visibility": "public"},
|
||||
{"name": 1, "path": 1, "user": 1, "visibility": 1},
|
||||
)
|
||||
)
|
||||
feed_users = list(
|
||||
db.users.find(
|
||||
{
|
||||
"$or": [
|
||||
{"_id": {"$in": list(feed_user_ids)}},
|
||||
{"_id": {"$in": list(map(lambda p: p["user"], feed_projects))}},
|
||||
]
|
||||
},
|
||||
{"username": 1, "avatar": 1, "isSilverSupporter": 1, "isGoldSupporter": 1},
|
||||
)
|
||||
)
|
||||
for u in feed_users:
|
||||
if "avatar" in u:
|
||||
u["avatarUrl"] = uploads.get_presigned_url(
|
||||
"users/{0}/{1}".format(str(u["_id"]), u["avatar"])
|
||||
)
|
||||
feed_user_map = {}
|
||||
feed_project_map = {}
|
||||
for u in feed_users:
|
||||
feed_user_map[str(u["_id"])] = u
|
||||
for p in feed_projects:
|
||||
feed_project_map[str(p["_id"])] = p
|
||||
for f in feed_items:
|
||||
if f.get("user") and feed_user_map.get(str(f["user"])):
|
||||
f["userObject"] = feed_user_map.get(str(f["user"]))
|
||||
if f.get("project") and feed_project_map.get(str(f["project"])):
|
||||
f["projectObject"] = feed_project_map.get(str(f["project"]))
|
||||
if f.get("projectObject", {}).get("user") and feed_user_map.get(
|
||||
str(f["projectObject"]["user"])
|
||||
):
|
||||
f["projectObject"]["userObject"] = feed_user_map.get(
|
||||
str(f["projectObject"]["user"])
|
||||
)
|
||||
|
||||
# Filter out orphaned or non-public comments/objects
|
||||
def filter_func(f):
|
||||
if f["feedType"] == "comment" and not f.get("projectObject"):
|
||||
return False
|
||||
if f["feedType"] == "object" and not f.get("projectObject"):
|
||||
return False
|
||||
return True
|
||||
|
||||
feed_items = list(filter(filter_func, feed_items))
|
||||
|
||||
return {"feed": feed_items}
|
||||
|
806
api/app.py
806
api/app.py
@ -1,345 +1,709 @@
|
||||
import os, json
|
||||
import os
|
||||
import json
|
||||
from flask import Flask, request, Response, jsonify
|
||||
from flask_limiter import Limiter
|
||||
from flask_cors import CORS
|
||||
import werkzeug
|
||||
from webargs import fields, validate
|
||||
from webargs.flaskparser import use_args
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||
from util import util
|
||||
from api import accounts, users, projects, objects, snippets, uploads, groups, search, invitations, root, activitypub
|
||||
from api import (
|
||||
accounts,
|
||||
users,
|
||||
projects,
|
||||
objects,
|
||||
snippets,
|
||||
uploads,
|
||||
groups,
|
||||
search,
|
||||
invitations,
|
||||
root,
|
||||
activitypub,
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
limiter = Limiter(app, default_limits=['20 per minute'])
|
||||
limiter = Limiter(app, default_limits=["20 per minute"])
|
||||
|
||||
if os.environ.get("SENTRY_DSN"):
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ["SENTRY_DSN"],
|
||||
traces_sample_rate=1.0,
|
||||
profiles_sample_rate=0.1,
|
||||
)
|
||||
|
||||
if os.environ.get('SENTRY_DSN'):
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ['SENTRY_DSN'],
|
||||
integrations=[FlaskIntegration()],
|
||||
traces_sample_rate=1.0
|
||||
)
|
||||
|
||||
@app.errorhandler(werkzeug.exceptions.TooManyRequests)
|
||||
def handle_429(e):
|
||||
return jsonify({'message': 'You\'re making too many requests. Please wait for a few minutes before trying again.', 'Allowed limit': e.description}), 429
|
||||
return jsonify(
|
||||
{
|
||||
"message": "You're making too many requests. Please wait for a few minutes before trying again.",
|
||||
"Allowed limit": e.description,
|
||||
}
|
||||
), 429
|
||||
|
||||
|
||||
@app.errorhandler(werkzeug.exceptions.BadRequest)
|
||||
def handle_bad_request(e):
|
||||
return jsonify({'message': e.description}), 400
|
||||
return jsonify({"message": e.description}), 400
|
||||
|
||||
|
||||
@app.errorhandler(werkzeug.exceptions.Unauthorized)
|
||||
def handle_not_authorized(e):
|
||||
return jsonify({'message': e.description}), 401
|
||||
return jsonify({"message": e.description}), 401
|
||||
|
||||
|
||||
@app.errorhandler(werkzeug.exceptions.Forbidden)
|
||||
def handle_forbidden(e):
|
||||
return jsonify({'message': e.description}), 403
|
||||
return jsonify({"message": e.description}), 403
|
||||
|
||||
|
||||
@app.errorhandler(werkzeug.exceptions.NotFound)
|
||||
def handle_not_found(e):
|
||||
return jsonify({'message': e.description}), 404
|
||||
@app.route('/debug-sentry')
|
||||
return jsonify({"message": e.description}), 404
|
||||
|
||||
|
||||
@app.errorhandler(werkzeug.exceptions.UnprocessableEntity)
|
||||
def handle_unprocessable_entity(e):
|
||||
validation_errors = e.data.get("messages")
|
||||
message = ""
|
||||
|
||||
def build_message(message, d):
|
||||
if not d:
|
||||
return message
|
||||
for key in d:
|
||||
if isinstance(d[key], dict):
|
||||
message += f"""{str(key)}: """
|
||||
return build_message(message, d[key])
|
||||
elif isinstance(d[key], list):
|
||||
message += f"""{str(key)}: {',\n '.join(d[key])}\n"""
|
||||
return message
|
||||
|
||||
if validation_errors:
|
||||
message = build_message("", validation_errors.get("json"))
|
||||
return jsonify(
|
||||
{
|
||||
"message": message,
|
||||
"validations": validation_errors,
|
||||
}
|
||||
), 422
|
||||
|
||||
|
||||
@app.route("/debug-sentry")
|
||||
def trigger_error():
|
||||
division_by_zero = 1 / 0
|
||||
division_by_zero = 1 / 0 # noqa: F841
|
||||
|
||||
|
||||
# ACCOUNTS
|
||||
@limiter.limit('5 per minute', key_func=util.limit_by_client, methods=['POST'])
|
||||
@app.route('/accounts/register', methods=['POST'])
|
||||
def register():
|
||||
body = request.json
|
||||
return util.jsonify(accounts.register(body.get('username'), body.get('email'), body.get('password'), body.get('howFindUs')))
|
||||
|
||||
@limiter.limit('5 per minute', key_func=util.limit_by_client, methods=['POST'])
|
||||
@app.route('/accounts/login', methods=['POST'])
|
||||
def login():
|
||||
body = request.json
|
||||
return util.jsonify(accounts.login(body.get('email'), body.get('password')))
|
||||
|
||||
@app.route('/accounts/logout', methods=['POST'])
|
||||
@limiter.limit("5 per minute", key_func=util.limit_by_client, methods=["POST"])
|
||||
@app.route("/accounts/register", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"username": fields.Str(required=True, validate=validate.Length(min=3)),
|
||||
"email": fields.Email(required=True),
|
||||
"password": fields.Str(required=True, validate=validate.Length(min=8)),
|
||||
"howFindUs": fields.Str(required=False),
|
||||
}
|
||||
)
|
||||
def register(args):
|
||||
return util.jsonify(
|
||||
accounts.register(
|
||||
args["username"],
|
||||
args["email"],
|
||||
args["password"],
|
||||
args.get("howFindUs"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@limiter.limit("5 per minute", key_func=util.limit_by_client, methods=["POST"])
|
||||
@app.route("/accounts/login", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"email": fields.Str(required=True), # Can be username
|
||||
"password": fields.Str(required=True),
|
||||
}
|
||||
)
|
||||
def login(args):
|
||||
return util.jsonify(accounts.login(args["email"], args["password"]))
|
||||
|
||||
|
||||
@app.route("/accounts/logout", methods=["POST"])
|
||||
def logout():
|
||||
return util.jsonify(accounts.logout(util.get_user()))
|
||||
return util.jsonify(accounts.logout(util.get_user()))
|
||||
|
||||
@app.route('/accounts', methods=['DELETE'])
|
||||
def delete_account():
|
||||
body = request.json
|
||||
return util.jsonify(accounts.delete(util.get_user(), body.get('password')))
|
||||
|
||||
@app.route('/accounts/email', methods=['PUT'])
|
||||
def email_address():
|
||||
body = request.json
|
||||
return util.jsonify(accounts.update_email(util.get_user(), body))
|
||||
@app.route("/accounts", methods=["DELETE"])
|
||||
@use_args(
|
||||
{
|
||||
"password": fields.Str(required=True),
|
||||
}
|
||||
)
|
||||
def delete_account(args):
|
||||
return util.jsonify(accounts.delete(util.get_user(), args.get("password")))
|
||||
|
||||
@limiter.limit('5 per minute', key_func=util.limit_by_user, methods=['POST'])
|
||||
@app.route('/accounts/password', methods=['PUT'])
|
||||
def password():
|
||||
body = request.json
|
||||
return util.jsonify(accounts.update_password(util.get_user(required=False), body))
|
||||
|
||||
@limiter.limit('5 per minute', key_func=util.limit_by_client, methods=['POST'])
|
||||
@app.route('/accounts/password/reset', methods=['POST'])
|
||||
def reset_password():
|
||||
body = request.json
|
||||
return util.jsonify(accounts.reset_password(body))
|
||||
@app.route("/accounts/email", methods=["PUT"])
|
||||
@use_args(
|
||||
{
|
||||
"email": fields.Email(required=True),
|
||||
}
|
||||
)
|
||||
def email_address(args):
|
||||
return util.jsonify(accounts.update_email(util.get_user(), args))
|
||||
|
||||
|
||||
@limiter.limit("5 per minute", key_func=util.limit_by_user, methods=["POST"])
|
||||
@app.route("/accounts/password", methods=["PUT"])
|
||||
@use_args(
|
||||
{
|
||||
"newPassword": fields.Str(required=True, validate=validate.Length(min=8)),
|
||||
"currentPassword": fields.Str(),
|
||||
"token": fields.Str(),
|
||||
}
|
||||
)
|
||||
def password(args):
|
||||
return util.jsonify(accounts.update_password(util.get_user(required=False), args))
|
||||
|
||||
|
||||
@limiter.limit("5 per minute", key_func=util.limit_by_client, methods=["POST"])
|
||||
@app.route("/accounts/password/reset", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"email": fields.Email(required=True),
|
||||
}
|
||||
)
|
||||
def reset_password(args):
|
||||
return util.jsonify(accounts.reset_password(args))
|
||||
|
||||
|
||||
@app.route("/accounts/pushToken", methods=["PUT"])
|
||||
@use_args(
|
||||
{
|
||||
"pushToken": fields.Str(required=True),
|
||||
}
|
||||
)
|
||||
def set_push_token(args):
|
||||
return util.jsonify(accounts.update_push_token(util.get_user(), args))
|
||||
|
||||
@app.route('/accounts/pushToken', methods=['PUT'])
|
||||
def set_push_token():
|
||||
return util.jsonify(accounts.update_push_token(util.get_user(), request.json))
|
||||
|
||||
# UPLOADS
|
||||
|
||||
@app.route('/uploads/file/request', methods=['GET'])
|
||||
def file_request():
|
||||
params = request.args
|
||||
file_name = params.get('name')
|
||||
file_size = params.get('size')
|
||||
file_type = params.get('type')
|
||||
for_type = params.get('forType')
|
||||
for_id = params.get('forId')
|
||||
return util.jsonify(uploads.generate_file_upload_request(util.get_user(), file_name, file_size, file_type, for_type, for_id))
|
||||
|
||||
@app.route("/uploads/file/request", methods=["GET"])
|
||||
@use_args(
|
||||
{
|
||||
"name": fields.Str(required=True),
|
||||
"size": fields.Int(required=True),
|
||||
"type": fields.Str(required=True),
|
||||
"forType": fields.Str(required=True),
|
||||
"forId": fields.Str(required=True),
|
||||
},
|
||||
location="query",
|
||||
)
|
||||
def file_request(args):
|
||||
file_name = args.get("name")
|
||||
file_size = args.get("size")
|
||||
file_type = args.get("type")
|
||||
for_type = args.get("forType")
|
||||
for_id = args.get("forId")
|
||||
return util.jsonify(
|
||||
uploads.generate_file_upload_request(
|
||||
util.get_user(), file_name, file_size, file_type, for_type, for_id
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# USERS
|
||||
|
||||
@app.route('/users/me', methods=['GET'])
|
||||
|
||||
@app.route("/users/me", methods=["GET"])
|
||||
def users_me():
|
||||
return util.jsonify(users.me(util.get_user()))
|
||||
return util.jsonify(users.me(util.get_user()))
|
||||
|
||||
@app.route('/users/<username>', methods=['GET', 'PUT'])
|
||||
def users_username(username):
|
||||
if request.method == 'GET': return util.jsonify(users.get(util.get_user(required=False), username))
|
||||
if request.method == 'PUT': return util.jsonify(users.update(util.get_user(), username, request.json))
|
||||
|
||||
@app.route('/users/<username>/feed', methods=['GET'])
|
||||
@app.route("/users/<username>", methods=["GET"])
|
||||
def users_username_get(username):
|
||||
return util.jsonify(users.get(util.get_user(required=False), username))
|
||||
|
||||
|
||||
@app.route("/users/<username>", methods=["PUT"])
|
||||
@use_args(
|
||||
{
|
||||
"username": fields.Str(validate=validate.Length(min=3)),
|
||||
"avatar": fields.Str(),
|
||||
"bio": fields.Str(),
|
||||
"location": fields.Str(),
|
||||
"website": fields.Str(),
|
||||
"twitter": fields.Str(),
|
||||
"facebook": fields.Str(),
|
||||
"linkedIn": fields.Str(),
|
||||
"instagram": fields.Str(),
|
||||
}
|
||||
)
|
||||
def users_username_put(args, username):
|
||||
return util.jsonify(users.update(util.get_user(), username, args))
|
||||
|
||||
|
||||
@app.route("/users/<username>/feed", methods=["GET"])
|
||||
def users_feed(username):
|
||||
if request.method == 'GET': return util.jsonify(users.get_feed(util.get_user(), username))
|
||||
|
||||
@app.route('/users/<username>/followers', methods=['POST', 'DELETE'])
|
||||
return util.jsonify(users.get_feed(util.get_user(), username))
|
||||
|
||||
|
||||
@app.route("/users/<username>/followers", methods=["POST", "DELETE"])
|
||||
def users_followers(username):
|
||||
if request.method == 'POST': return util.jsonify(users.create_follower(util.get_user(), username))
|
||||
if request.method == 'DELETE': return util.jsonify(users.delete_follower(util.get_user(), username))
|
||||
if request.method == "POST":
|
||||
return util.jsonify(users.create_follower(util.get_user(), username))
|
||||
if request.method == "DELETE":
|
||||
return util.jsonify(users.delete_follower(util.get_user(), username))
|
||||
|
||||
@app.route('/users/<username>/tours/<tour>', methods=['PUT'])
|
||||
def users_tour(username, tour):
|
||||
status = request.args.get('status', 'completed')
|
||||
return util.jsonify(users.finish_tour(util.get_user(), username, tour, status))
|
||||
|
||||
@app.route('/users/me/projects', methods=['GET'])
|
||||
@app.route("/users/<username>/tours/<tour>", methods=["PUT"])
|
||||
@use_args(
|
||||
{
|
||||
"status": fields.Str(
|
||||
required=True, validate=validate.OneOf(["completed", "skipped"])
|
||||
),
|
||||
},
|
||||
location="query",
|
||||
)
|
||||
def users_tour(args, username, tour):
|
||||
status = args.get("status", "completed")
|
||||
return util.jsonify(users.finish_tour(util.get_user(), username, tour, status))
|
||||
|
||||
|
||||
@app.route("/users/me/projects", methods=["GET"])
|
||||
def me_projects_route():
|
||||
user = util.get_user()
|
||||
return util.jsonify({'projects': users.get_projects(user, user['_id'])})
|
||||
user = util.get_user()
|
||||
return util.jsonify({"projects": users.get_projects(user, user["_id"])})
|
||||
|
||||
@app.route('/users/<username>/subscriptions/email/<subscription>', methods=['PUT', 'DELETE'])
|
||||
|
||||
@app.route(
|
||||
"/users/<username>/subscriptions/email/<subscription>", methods=["PUT", "DELETE"]
|
||||
)
|
||||
def user_email_subscription(username, subscription):
|
||||
if request.method == 'PUT': return util.jsonify(users.create_email_subscription(util.get_user(), username, subscription))
|
||||
if request.method == 'DELETE': return util.jsonify(users.delete_email_subscription(util.get_user(), username, subscription))
|
||||
if request.method == "PUT":
|
||||
return util.jsonify(
|
||||
users.create_email_subscription(util.get_user(), username, subscription)
|
||||
)
|
||||
if request.method == "DELETE":
|
||||
return util.jsonify(
|
||||
users.delete_email_subscription(util.get_user(), username, subscription)
|
||||
)
|
||||
|
||||
|
||||
# PROJECTS
|
||||
|
||||
@app.route('/projects', methods=['POST'])
|
||||
def projects_route():
|
||||
return util.jsonify(projects.create(util.get_user(), request.json))
|
||||
|
||||
@app.route('/projects/<username>/<project_path>/objects', methods=['GET', 'POST'])
|
||||
@app.route("/projects", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"name": fields.Str(required=True),
|
||||
"description": fields.Str(),
|
||||
"visibility": fields.Str(validate=validate.OneOf(["public", "private"])),
|
||||
"openSource": fields.Bool(),
|
||||
"groupVisibility": fields.List(fields.Str()),
|
||||
}
|
||||
)
|
||||
def projects_route(args):
|
||||
return util.jsonify(projects.create(util.get_user(), args))
|
||||
|
||||
|
||||
@app.route("/projects/<username>/<project_path>/objects", methods=["GET"])
|
||||
def project_objects_get(username, project_path):
|
||||
if request.method == 'GET':
|
||||
return util.jsonify({'objects': projects.get_objects(util.get_user(required=False), username, project_path)})
|
||||
if request.method == 'POST':
|
||||
return util.jsonify(projects.create_object(util.get_user(), username, project_path, request.json))
|
||||
return util.jsonify(
|
||||
{
|
||||
"objects": projects.get_objects(
|
||||
util.get_user(required=False), username, project_path
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@app.route('/projects/<username>/<project_path>', methods=['GET', 'PUT', 'DELETE'])
|
||||
|
||||
@app.route("/projects/<username>/<project_path>/objects", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"name": fields.Str(),
|
||||
"storedName": fields.Str(),
|
||||
"wif": fields.Str(),
|
||||
"type": fields.Str(required=True, validate=validate.OneOf(["file", "pattern"])),
|
||||
}
|
||||
)
|
||||
def project_objects_post(args, username, project_path):
|
||||
return util.jsonify(
|
||||
projects.create_object(util.get_user(), username, project_path, args)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/projects/<username>/<project_path>", methods=["GET", "DELETE"])
|
||||
def project_by_path(username, project_path):
|
||||
if request.method == 'GET':
|
||||
return util.jsonify(projects.get(util.get_user(required=False), username, project_path))
|
||||
if request.method == 'PUT':
|
||||
return util.jsonify(projects.update(util.get_user(), username, project_path, request.json))
|
||||
if request.method == 'DELETE':
|
||||
return util.jsonify(projects.delete(util.get_user(), username, project_path))
|
||||
if request.method == "GET":
|
||||
return util.jsonify(
|
||||
projects.get(util.get_user(required=False), username, project_path)
|
||||
)
|
||||
if request.method == "DELETE":
|
||||
return util.jsonify(projects.delete(util.get_user(), username, project_path))
|
||||
|
||||
|
||||
@app.route("/projects/<username>/<project_path>", methods=["PUT"])
|
||||
@use_args(
|
||||
{
|
||||
"name": fields.Str(),
|
||||
"description": fields.Str(),
|
||||
"visibility": fields.Str(validate=validate.OneOf(["public", "private"])),
|
||||
"openSource": fields.Bool(),
|
||||
"groupVisibility": fields.List(fields.Str()),
|
||||
}
|
||||
)
|
||||
def project_by_path_put(args, username, project_path):
|
||||
return util.jsonify(projects.update(util.get_user(), username, project_path, args))
|
||||
|
||||
|
||||
# OBJECTS
|
||||
|
||||
@app.route('/objects/<id>', methods=['GET', 'DELETE', 'PUT'])
|
||||
def objects_route(id):
|
||||
if request.method == 'GET':
|
||||
return util.jsonify(objects.get(util.get_user(required=False), id))
|
||||
if request.method == 'DELETE':
|
||||
return util.jsonify({'_id': objects.delete(util.get_user(), id)})
|
||||
if request.method == 'PUT':
|
||||
body = request.json
|
||||
return util.jsonify(objects.update(util.get_user(), id, body))
|
||||
|
||||
@app.route('/objects/<id>/projects/<project_id>', methods=['PUT'])
|
||||
@app.route("/objects/<id>", methods=["GET", "DELETE"])
|
||||
def objects_route(id):
|
||||
if request.method == "GET":
|
||||
return util.jsonify(objects.get(util.get_user(required=False), id))
|
||||
if request.method == "DELETE":
|
||||
return util.jsonify({"_id": objects.delete(util.get_user(), id)})
|
||||
|
||||
|
||||
@app.route("/objects/<id>", methods=["PUT"])
|
||||
@use_args(
|
||||
{
|
||||
"name": fields.Str(),
|
||||
"description": fields.Str(),
|
||||
"pattern": fields.Dict(),
|
||||
}
|
||||
)
|
||||
def objects_route_put(args, id):
|
||||
return util.jsonify(objects.update(util.get_user(), id, args))
|
||||
|
||||
|
||||
@app.route("/objects/<id>/projects/<project_id>", methods=["PUT"])
|
||||
def copy_object_route(id, project_id):
|
||||
if request.method == 'PUT':
|
||||
return util.jsonify(objects.copy_to_project(util.get_user(), id, project_id))
|
||||
|
||||
@app.route('/objects/<id>/wif', methods=['GET'])
|
||||
|
||||
@app.route("/objects/<id>/wif", methods=["GET"])
|
||||
def objects_get_wif(id):
|
||||
if request.method == 'GET':
|
||||
return util.jsonify(objects.get_wif(util.get_user(required=False), id))
|
||||
|
||||
@app.route('/objects/<id>/pdf', methods=['GET'])
|
||||
|
||||
@app.route("/objects/<id>/pdf", methods=["GET"])
|
||||
def objects_get_pdf(id):
|
||||
return util.jsonify(objects.get_pdf(util.get_user(required=False), id))
|
||||
return util.jsonify(objects.get_pdf(util.get_user(required=False), id))
|
||||
|
||||
@app.route('/objects/<id>/comments', methods=['GET', 'POST'])
|
||||
def object_comments(id):
|
||||
if request.method == 'GET':
|
||||
|
||||
@app.route("/objects/<id>/comments", methods=["GET"])
|
||||
def object_comments_get(id):
|
||||
return util.jsonify(objects.get_comments(util.get_user(required=False), id))
|
||||
if request.method == 'POST':
|
||||
return util.jsonify(objects.create_comment(util.get_user(), id, request.json))
|
||||
|
||||
@app.route('/objects/<id>/comments/<comment_id>', methods=['DELETE'])
|
||||
|
||||
@app.route("/objects/<id>/comments", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"content": fields.Str(required=True, validate=validate.Length(min=1)),
|
||||
}
|
||||
)
|
||||
def object_comments_post(args, id):
|
||||
return util.jsonify(objects.create_comment(util.get_user(), id, args))
|
||||
|
||||
|
||||
@app.route("/objects/<id>/comments/<comment_id>", methods=["DELETE"])
|
||||
def object_comment(id, comment_id):
|
||||
return util.jsonify(objects.delete_comment(util.get_user(), id, comment_id))
|
||||
return util.jsonify(objects.delete_comment(util.get_user(), id, comment_id))
|
||||
|
||||
|
||||
# SNIPPETS
|
||||
@app.route('/snippets', methods=['POST', 'GET'])
|
||||
def snippets_route():
|
||||
if request.method == 'POST':
|
||||
return util.jsonify(snippets.create(util.get_user(), request.json))
|
||||
if request.method == 'GET':
|
||||
@app.route("/snippets", methods=["GET"])
|
||||
def snippets_route_get():
|
||||
return util.jsonify(snippets.list_for_user(util.get_user()))
|
||||
|
||||
@app.route('/snippets/<id>', methods=['DELETE'])
|
||||
|
||||
@app.route("/snippets", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"name": fields.Str(required=True, validate=validate.Length(min=3)),
|
||||
"type": fields.Str(required=True, validate=validate.OneOf(["warp", "weft"])),
|
||||
"threading": fields.List(fields.Dict(), allow_none=True),
|
||||
"treadling": fields.List(fields.Dict(), allow_none=True),
|
||||
}
|
||||
)
|
||||
def snippets_route_post(args):
|
||||
return util.jsonify(snippets.create(util.get_user(), args))
|
||||
|
||||
|
||||
@app.route("/snippets/<id>", methods=["DELETE"])
|
||||
def snippet_route(id):
|
||||
if request.method == 'DELETE':
|
||||
return util.jsonify(snippets.delete(util.get_user(), id))
|
||||
|
||||
|
||||
# GROUPS
|
||||
|
||||
@app.route('/groups', methods=['POST', 'GET'])
|
||||
def groups_route():
|
||||
if request.method == 'GET':
|
||||
|
||||
@app.route("/groups", methods=["GET"])
|
||||
def groups_route_get():
|
||||
return util.jsonify(groups.get(util.get_user(required=True)))
|
||||
if request.method == 'POST':
|
||||
return util.jsonify(groups.create(util.get_user(required=True), request.json))
|
||||
|
||||
@app.route('/groups/<id>', methods=['GET', 'PUT', 'DELETE'])
|
||||
|
||||
@app.route("/groups", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"name": fields.Str(required=True, validate=validate.Length(min=3)),
|
||||
"description": fields.Str(),
|
||||
"closed": fields.Bool(),
|
||||
}
|
||||
)
|
||||
def groups_route_post(args):
|
||||
return util.jsonify(groups.create(util.get_user(required=True), args))
|
||||
|
||||
|
||||
@app.route("/groups/<id>", methods=["GET", "DELETE"])
|
||||
def group_route(id):
|
||||
if request.method == 'GET':
|
||||
return util.jsonify(groups.get_one(util.get_user(required=False), id))
|
||||
if request.method == 'PUT':
|
||||
return util.jsonify(groups.update(util.get_user(required=True), id, request.json))
|
||||
if request.method == 'DELETE':
|
||||
return util.jsonify(groups.delete(util.get_user(required=True), id))
|
||||
if request.method == "GET":
|
||||
return util.jsonify(groups.get_one(util.get_user(required=False), id))
|
||||
if request.method == "DELETE":
|
||||
return util.jsonify(groups.delete(util.get_user(required=True), id))
|
||||
|
||||
@app.route('/groups/<id>/entries', methods=['GET', 'POST'])
|
||||
def group_entries_route(id):
|
||||
if request.method == 'GET':
|
||||
|
||||
@app.route("/groups/<id>", methods=["PUT"])
|
||||
@use_args(
|
||||
{
|
||||
"name": fields.Str(),
|
||||
"description": fields.Str(),
|
||||
"closed": fields.Bool(),
|
||||
}
|
||||
)
|
||||
def group_route_put(args, id):
|
||||
return util.jsonify(groups.update(util.get_user(required=True), id, args))
|
||||
|
||||
|
||||
@app.route("/groups/<id>/entries", methods=["GET"])
|
||||
def group_entries_route_get(id):
|
||||
return util.jsonify(groups.get_entries(util.get_user(required=True), id))
|
||||
if request.method == 'POST':
|
||||
return util.jsonify(groups.create_entry(util.get_user(required=True), id, request.json))
|
||||
|
||||
@app.route('/groups/<id>/entries/<entry_id>', methods=['DELETE'])
|
||||
|
||||
@app.route("/groups/<id>/entries", methods=["GET", "POST"])
|
||||
@use_args(
|
||||
{
|
||||
"content": fields.Str(required=True, validate=validate.Length(min=1)),
|
||||
"attachments": fields.List(fields.Dict()),
|
||||
}
|
||||
)
|
||||
def group_entries_route_post(args, id):
|
||||
return util.jsonify(groups.create_entry(util.get_user(required=True), id, args))
|
||||
|
||||
|
||||
@app.route("/groups/<id>/entries/<entry_id>", methods=["DELETE"])
|
||||
def group_entry_route(id, entry_id):
|
||||
if request.method == 'DELETE':
|
||||
return util.jsonify(groups.delete_entry(util.get_user(required=True), id, entry_id))
|
||||
|
||||
@app.route('/groups/<id>/entries/<entry_id>/replies', methods=['POST'])
|
||||
def group_entry_replies_route(id, entry_id):
|
||||
return util.jsonify(groups.create_entry_reply(util.get_user(required=True), id, entry_id, request.json))
|
||||
@app.route('/groups/<id>/entries/<entry_id>/replies/<reply_id>', methods=['DELETE'])
|
||||
def group_entry_reply_route(id, entry_id, reply_id):
|
||||
return util.jsonify(groups.delete_entry_reply(util.get_user(required=True), id, entry_id, reply_id))
|
||||
|
||||
@app.route('/groups/<id>/members', methods=['GET'])
|
||||
@app.route("/groups/<id>/entries/<entry_id>/replies", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"content": fields.Str(required=True, validate=validate.Length(min=1)),
|
||||
"attachments": fields.List(fields.Dict()),
|
||||
}
|
||||
)
|
||||
def group_entry_replies_route(args, id, entry_id):
|
||||
return util.jsonify(
|
||||
groups.create_entry_reply(util.get_user(required=True), id, entry_id, args)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/groups/<id>/entries/<entry_id>/replies/<reply_id>", methods=["DELETE"])
|
||||
def group_entry_reply_route(id, entry_id, reply_id):
|
||||
return util.jsonify(
|
||||
groups.delete_entry_reply(util.get_user(required=True), id, entry_id, reply_id)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/groups/<id>/members", methods=["GET"])
|
||||
def group_members_route(id):
|
||||
if request.method == 'GET':
|
||||
return util.jsonify(groups.get_members(util.get_user(required=True), id))
|
||||
|
||||
@app.route('/groups/<id>/members/<user_id>', methods=['PUT', 'DELETE'])
|
||||
def group_member_route(id, user_id):
|
||||
if request.method == 'DELETE':
|
||||
return util.jsonify(groups.delete_member(util.get_user(required=True), id, user_id))
|
||||
if request.method == 'PUT':
|
||||
return util.jsonify(groups.create_member(util.get_user(required=True), id, user_id))
|
||||
|
||||
@app.route('/groups/<id>/projects', methods=['GET'])
|
||||
@app.route("/groups/<id>/members/<user_id>", methods=["PUT", "DELETE"])
|
||||
def group_member_route(id, user_id):
|
||||
if request.method == "DELETE":
|
||||
return util.jsonify(
|
||||
groups.delete_member(util.get_user(required=True), id, user_id)
|
||||
)
|
||||
if request.method == "PUT":
|
||||
return util.jsonify(
|
||||
groups.create_member(util.get_user(required=True), id, user_id)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/groups/<id>/projects", methods=["GET"])
|
||||
def group_projects_route(id):
|
||||
if request.method == 'GET':
|
||||
return util.jsonify(groups.get_projects(util.get_user(required=True), id))
|
||||
|
||||
@app.route('/groups/<id>/invitations', methods=['POST', 'GET'])
|
||||
def group_invites_route(id):
|
||||
if request.method == 'POST':
|
||||
return util.jsonify(invitations.create_group_invitation(util.get_user(required=True), id, request.json))
|
||||
if request.method == 'GET':
|
||||
return util.jsonify(invitations.get_group_invitations(util.get_user(required=True), id))
|
||||
|
||||
@app.route('/groups/<id>/invitations/<invite_id>', methods=['DELETE'])
|
||||
@app.route("/groups/<id>/invitations", methods=["GET"])
|
||||
def group_invites_route_get(id):
|
||||
return util.jsonify(
|
||||
invitations.get_group_invitations(util.get_user(required=True), id)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/groups/<id>/invitations", methods=["POST"])
|
||||
@use_args(
|
||||
{
|
||||
"user": fields.Str(required=True),
|
||||
}
|
||||
)
|
||||
def group_invites_route(args, id):
|
||||
return util.jsonify(
|
||||
invitations.create_group_invitation(util.get_user(required=True), id, args)
|
||||
)
|
||||
|
||||
|
||||
@app.route("/groups/<id>/invitations/<invite_id>", methods=["DELETE"])
|
||||
def group_invite_route(id, invite_id):
|
||||
return util.jsonify(invitations.delete_group_invitation(util.get_user(required=True), id, invite_id))
|
||||
return util.jsonify(
|
||||
invitations.delete_group_invitation(util.get_user(required=True), id, invite_id)
|
||||
)
|
||||
|
||||
@app.route('/groups/<id>/requests', methods=['POST', 'GET'])
|
||||
|
||||
@app.route("/groups/<id>/requests", methods=["POST", "GET"])
|
||||
def group_requests_route(id):
|
||||
if request.method == 'POST':
|
||||
return util.jsonify(invitations.create_group_request(util.get_user(required=True), id))
|
||||
if request.method == 'GET':
|
||||
return util.jsonify(invitations.get_group_requests(util.get_user(required=True), id))
|
||||
if request.method == "POST":
|
||||
return util.jsonify(
|
||||
invitations.create_group_request(util.get_user(required=True), id)
|
||||
)
|
||||
if request.method == "GET":
|
||||
return util.jsonify(
|
||||
invitations.get_group_requests(util.get_user(required=True), id)
|
||||
)
|
||||
|
||||
|
||||
# SEARCH
|
||||
|
||||
@app.route('/search', methods=['GET'])
|
||||
def search_all():
|
||||
params = request.args
|
||||
return util.jsonify(search.all(util.get_user(required=True), params))
|
||||
|
||||
@app.route('/search/users', methods=['GET'])
|
||||
def search_users():
|
||||
params = request.args
|
||||
return util.jsonify(search.users(util.get_user(required=True), params))
|
||||
@app.route("/search", methods=["GET"])
|
||||
@use_args(
|
||||
{
|
||||
"query": fields.Str(required=True),
|
||||
},
|
||||
location="query",
|
||||
)
|
||||
def search_all(args):
|
||||
return util.jsonify(search.all(util.get_user(required=True), args))
|
||||
|
||||
@app.route('/search/discover', methods=['GET'])
|
||||
def search_discover():
|
||||
count = request.args.get('count', 3)
|
||||
if count: count = int(count)
|
||||
return util.jsonify(search.discover(util.get_user(required=False), count=count))
|
||||
|
||||
@app.route('/search/explore', methods=['GET'])
|
||||
def search_explore():
|
||||
page = request.args.get('page', 1)
|
||||
if page: page = int(page)
|
||||
return util.jsonify(search.explore(page=page))
|
||||
@app.route("/search/users", methods=["GET"])
|
||||
@use_args(
|
||||
{
|
||||
"username": fields.Str(required=True),
|
||||
},
|
||||
location="query",
|
||||
)
|
||||
def search_users(args):
|
||||
return util.jsonify(search.users(util.get_user(required=True), args))
|
||||
|
||||
|
||||
@app.route("/search/discover", methods=["GET"])
|
||||
@use_args(
|
||||
{
|
||||
"count": fields.Int(),
|
||||
},
|
||||
location="query",
|
||||
)
|
||||
def search_discover(args):
|
||||
count = args.get("count", 3)
|
||||
if count:
|
||||
count = int(count)
|
||||
return util.jsonify(search.discover(util.get_user(required=False), count=count))
|
||||
|
||||
|
||||
@app.route("/search/explore", methods=["GET"])
|
||||
@use_args(
|
||||
{
|
||||
"page": fields.Int(),
|
||||
},
|
||||
location="query",
|
||||
)
|
||||
def search_explore(args):
|
||||
page = args.get("page", 1)
|
||||
if page:
|
||||
page = int(page)
|
||||
return util.jsonify(search.explore(page=page))
|
||||
|
||||
|
||||
# INVITATIONS
|
||||
|
||||
@app.route('/invitations', methods=['GET'])
|
||||
def invites_route():
|
||||
return util.jsonify(invitations.get(util.get_user(required=True)))
|
||||
|
||||
@app.route('/invitations/<id>', methods=['PUT', 'DELETE'])
|
||||
@app.route("/invitations", methods=["GET"])
|
||||
def invites_route():
|
||||
return util.jsonify(invitations.get(util.get_user(required=True)))
|
||||
|
||||
|
||||
@app.route("/invitations/<id>", methods=["PUT", "DELETE"])
|
||||
def invite_route(id):
|
||||
if request.method == 'PUT':
|
||||
return util.jsonify(invitations.accept(util.get_user(required=True), id))
|
||||
if request.method =='DELETE':
|
||||
return util.jsonify(invitations.delete(util.get_user(required=True), id))
|
||||
if request.method == "PUT":
|
||||
return util.jsonify(invitations.accept(util.get_user(required=True), id))
|
||||
if request.method == "DELETE":
|
||||
return util.jsonify(invitations.delete(util.get_user(required=True), id))
|
||||
|
||||
|
||||
## ROOT
|
||||
|
||||
@app.route('/root/users', methods=['GET'])
|
||||
|
||||
@app.route("/root/users", methods=["GET"])
|
||||
def root_users():
|
||||
return util.jsonify(root.get_users(util.get_user(required=True)))
|
||||
@app.route('/root/groups', methods=['GET'])
|
||||
return util.jsonify(root.get_users(util.get_user(required=True)))
|
||||
|
||||
|
||||
@app.route("/root/groups", methods=["GET"])
|
||||
def root_groups():
|
||||
return util.jsonify(root.get_groups(util.get_user(required=True)))
|
||||
return util.jsonify(root.get_groups(util.get_user(required=True)))
|
||||
|
||||
|
||||
## ActivityPub Support
|
||||
|
||||
@app.route('/.well-known/webfinger', methods=['GET'])
|
||||
def webfinger():
|
||||
resource = request.args.get('resource')
|
||||
return util.jsonify(activitypub.webfinger(resource))
|
||||
|
||||
@app.route('/u/<username>', methods=['GET'])
|
||||
@app.route("/.well-known/webfinger", methods=["GET"])
|
||||
@use_args(
|
||||
{
|
||||
"resource": fields.Str(required=True),
|
||||
},
|
||||
location="query",
|
||||
)
|
||||
def webfinger(args):
|
||||
resource = args.get("resource")
|
||||
return util.jsonify(activitypub.webfinger(resource))
|
||||
|
||||
|
||||
@app.route("/u/<username>", methods=["GET"])
|
||||
def ap_user(username):
|
||||
resp_data = activitypub.user(username)
|
||||
resp = Response(json.dumps(resp_data))
|
||||
resp.headers['Content-Type'] = 'application/activity+json'
|
||||
return resp
|
||||
resp_data = activitypub.user(username)
|
||||
resp = Response(json.dumps(resp_data))
|
||||
resp.headers["Content-Type"] = "application/activity+json"
|
||||
return resp
|
||||
|
||||
@app.route('/u/<username>/outbox', methods=['GET'])
|
||||
def ap_user_outbox(username):
|
||||
page = request.args.get('page')
|
||||
min_id = request.args.get('min_id')
|
||||
max_id = request.args.get('max_id')
|
||||
resp_data = activitypub.outbox(username, page, min_id, max_id)
|
||||
resp = Response(json.dumps(resp_data))
|
||||
resp.headers['Content-Type'] = 'application/activity+json'
|
||||
return resp
|
||||
|
||||
@app.route("/u/<username>/outbox", methods=["GET"])
|
||||
@use_args(
|
||||
{
|
||||
"page": fields.Int(),
|
||||
"min_id": fields.Str(),
|
||||
"max_id": fields.Str(),
|
||||
},
|
||||
location="query",
|
||||
)
|
||||
def ap_user_outbox(args, username):
|
||||
page = args.get("page")
|
||||
min_id = args.get("min_id")
|
||||
max_id = args.get("max_id")
|
||||
resp_data = activitypub.outbox(username, page, min_id, max_id)
|
||||
resp = Response(json.dumps(resp_data))
|
||||
resp.headers["Content-Type"] = "application/activity+json"
|
||||
return resp
|
||||
|
4
api/lint.sh
Executable file
4
api/lint.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
ruff format .
|
||||
ruff check --fix .
|
@ -1,21 +1,34 @@
|
||||
# Script to migrate from the old data: string URLs for images to image files directly on S3.
|
||||
|
||||
from pymongo import MongoClient
|
||||
import base64, os
|
||||
import base64
|
||||
import os
|
||||
|
||||
db = MongoClient('mongodb://USER:PASS@db/admin')['treadl']
|
||||
db = MongoClient("mongodb://USER:PASS@db/admin")["treadl"]
|
||||
|
||||
os.makedirs('migration_projects/projects', exist_ok=True)
|
||||
os.makedirs("migration_projects/projects", exist_ok=True)
|
||||
|
||||
for obj in db.objects.find({'preview': {'$regex': '^data:'}}, {'preview': 1, 'project': 1}):
|
||||
preview = obj['preview']
|
||||
preview = preview.replace('data:image/png;base64,', '')
|
||||
|
||||
imgdata = base64.b64decode(preview)
|
||||
filename = 'some_image.png'
|
||||
|
||||
os.makedirs('migration_projects/projects/'+str(obj['project']), exist_ok=True)
|
||||
with open('migration_projects/projects/'+str(obj['project'])+'/preview_'+str(obj['_id'])+'.png' , 'wb') as f:
|
||||
f.write(imgdata)
|
||||
db.objects.update_one({'_id': obj['_id']}, {'$set': {'previewNew': 'preview_'+str(obj['_id'])+'.png'}})
|
||||
#exit()
|
||||
for obj in db.objects.find(
|
||||
{"preview": {"$regex": "^data:"}}, {"preview": 1, "project": 1}
|
||||
):
|
||||
preview = obj["preview"]
|
||||
preview = preview.replace("data:image/png;base64,", "")
|
||||
|
||||
imgdata = base64.b64decode(preview)
|
||||
filename = "some_image.png"
|
||||
|
||||
os.makedirs("migration_projects/projects/" + str(obj["project"]), exist_ok=True)
|
||||
with open(
|
||||
"migration_projects/projects/"
|
||||
+ str(obj["project"])
|
||||
+ "/preview_"
|
||||
+ str(obj["_id"])
|
||||
+ ".png",
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(imgdata)
|
||||
db.objects.update_one(
|
||||
{"_id": obj["_id"]},
|
||||
{"$set": {"previewNew": "preview_" + str(obj["_id"]) + ".png"}},
|
||||
)
|
||||
# exit()
|
||||
|
235
api/poetry.lock
generated
235
api/poetry.lock
generated
@ -81,17 +81,17 @@ testing = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.35.4"
|
||||
version = "1.35.34"
|
||||
description = "The AWS SDK for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "boto3-1.35.4-py3-none-any.whl", hash = "sha256:96c39593afb7b55ebb74d08c8e3201041d105b557c8c8536c9054c9f13da5f2a"},
|
||||
{file = "boto3-1.35.4.tar.gz", hash = "sha256:d997b82c468bd5c2d5cd29810d47079b66b178d2b5ae021aebe262c4d78d4c94"},
|
||||
{file = "boto3-1.35.34-py3-none-any.whl", hash = "sha256:291e7b97a34967ed93297e6171f1bebb8529e64633dd48426760e3fdef1cdea8"},
|
||||
{file = "boto3-1.35.34.tar.gz", hash = "sha256:57e6ee8504e7929bc094bb2afc879943906064179a1e88c23b4812e2c6f61532"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.35.4,<1.36.0"
|
||||
botocore = ">=1.35.34,<1.36.0"
|
||||
jmespath = ">=0.7.1,<2.0.0"
|
||||
s3transfer = ">=0.10.0,<0.11.0"
|
||||
|
||||
@ -100,13 +100,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.35.4"
|
||||
version = "1.35.34"
|
||||
description = "Low-level, data-driven core of boto 3."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "botocore-1.35.4-py3-none-any.whl", hash = "sha256:10195e5ca764745f02b9a51df048b996ddbdc1899a44a2caf35dfb225dfea489"},
|
||||
{file = "botocore-1.35.4.tar.gz", hash = "sha256:4cc51a6a486915aedc140f9d027b7e156646b7a0f7b33b1000762c81aff9a12f"},
|
||||
{file = "botocore-1.35.34-py3-none-any.whl", hash = "sha256:ccb0fe397b11b81c9abc0c87029d17298e17bf658d8db5c0c5a551a12a207e7a"},
|
||||
{file = "botocore-1.35.34.tar.gz", hash = "sha256:789b6501a3bb4a9591c1fe10da200cc315c1fa5df5ada19c720d8ef06439b3e3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -115,7 +115,7 @@ python-dateutil = ">=2.1,<3.0.0"
|
||||
urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
crt = ["awscrt (==0.21.2)"]
|
||||
crt = ["awscrt (==0.22.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "cachecontrol"
|
||||
@ -450,21 +450,22 @@ wmi = ["wmi (>=1.5.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "firebase-admin"
|
||||
version = "4.5.3"
|
||||
version = "6.5.0"
|
||||
description = "Firebase Admin Python SDK"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "firebase_admin-4.5.3-py3-none-any.whl", hash = "sha256:471045bf72bb68ccf6d9d19a35836609b7d525fcdac73aa619d274e4c5585e0a"},
|
||||
{file = "firebase_admin-4.5.3.tar.gz", hash = "sha256:f0c1c9a6e56b497c8bbaa55a679a402f79c34c2c24971b11ea909031b520ed32"},
|
||||
{file = "firebase_admin-6.5.0-py3-none-any.whl", hash = "sha256:fe34ee3ca0e625c5156b3931ca4b4b69b5fc344dbe51bba9706ff674ce277898"},
|
||||
{file = "firebase_admin-6.5.0.tar.gz", hash = "sha256:e716dde1447f0a1cd1523be76ff872df33c4e1a3c079564ace033b2ad60bcc4f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cachecontrol = ">=0.12.6"
|
||||
google-api-core = {version = ">=1.14.0,<2.0.0dev", extras = ["grpc"], markers = "platform_python_implementation != \"PyPy\""}
|
||||
google-api-core = {version = ">=1.22.1,<3.0.0dev", extras = ["grpc"], markers = "platform_python_implementation != \"PyPy\""}
|
||||
google-api-python-client = ">=1.7.8"
|
||||
google-cloud-firestore = {version = ">=1.4.0", markers = "platform_python_implementation != \"PyPy\""}
|
||||
google-cloud-storage = ">=1.18.0"
|
||||
google-cloud-firestore = {version = ">=2.9.1", markers = "platform_python_implementation != \"PyPy\""}
|
||||
google-cloud-storage = ">=1.37.1"
|
||||
pyjwt = {version = ">=2.5.0", extras = ["crypto"]}
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
@ -490,13 +491,13 @@ dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-cors"
|
||||
version = "4.0.1"
|
||||
version = "5.0.0"
|
||||
description = "A Flask extension adding a decorator for CORS support"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "Flask_Cors-4.0.1-py2.py3-none-any.whl", hash = "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677"},
|
||||
{file = "flask_cors-4.0.1.tar.gz", hash = "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4"},
|
||||
{file = "Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc"},
|
||||
{file = "flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1078,6 +1079,25 @@ files = [
|
||||
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
version = "3.22.0"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "marshmallow-3.22.0-py3-none-any.whl", hash = "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9"},
|
||||
{file = "marshmallow-3.22.0.tar.gz", hash = "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=17.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
|
||||
docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
@ -1385,6 +1405,9 @@ files = [
|
||||
{file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
|
||||
|
||||
[package.extras]
|
||||
crypto = ["cryptography (>=3.4.0)"]
|
||||
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||
@ -1393,61 +1416,70 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pymongo"
|
||||
version = "4.8.0"
|
||||
version = "4.10.1"
|
||||
description = "Python driver for MongoDB <http://www.mongodb.org>"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pymongo-4.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2b7bec27e047e84947fbd41c782f07c54c30c76d14f3b8bf0c89f7413fac67a"},
|
||||
{file = "pymongo-4.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c68fe128a171493018ca5c8020fc08675be130d012b7ab3efe9e22698c612a1"},
|
||||
{file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920d4f8f157a71b3cb3f39bc09ce070693d6e9648fb0e30d00e2657d1dca4e49"},
|
||||
{file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52b4108ac9469febba18cea50db972605cc43978bedaa9fea413378877560ef8"},
|
||||
{file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:180d5eb1dc28b62853e2f88017775c4500b07548ed28c0bd9c005c3d7bc52526"},
|
||||
{file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aec2b9088cdbceb87e6ca9c639d0ff9b9d083594dda5ca5d3c4f6774f4c81b33"},
|
||||
{file = "pymongo-4.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0cf61450feadca81deb1a1489cb1a3ae1e4266efd51adafecec0e503a8dcd84"},
|
||||
{file = "pymongo-4.8.0-cp310-cp310-win32.whl", hash = "sha256:8b18c8324809539c79bd6544d00e0607e98ff833ca21953df001510ca25915d1"},
|
||||
{file = "pymongo-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e5df28f74002e37bcbdfdc5109799f670e4dfef0fb527c391ff84f078050e7b5"},
|
||||
{file = "pymongo-4.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b50040d9767197b77ed420ada29b3bf18a638f9552d80f2da817b7c4a4c9c68"},
|
||||
{file = "pymongo-4.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:417369ce39af2b7c2a9c7152c1ed2393edfd1cbaf2a356ba31eb8bcbd5c98dd7"},
|
||||
{file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf821bd3befb993a6db17229a2c60c1550e957de02a6ff4dd0af9476637b2e4d"},
|
||||
{file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9365166aa801c63dff1a3cb96e650be270da06e3464ab106727223123405510f"},
|
||||
{file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc8b8582f4209c2459b04b049ac03c72c618e011d3caa5391ff86d1bda0cc486"},
|
||||
{file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e5019f75f6827bb5354b6fef8dfc9d6c7446894a27346e03134d290eb9e758"},
|
||||
{file = "pymongo-4.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b5802151fc2b51cd45492c80ed22b441d20090fb76d1fd53cd7760b340ff554"},
|
||||
{file = "pymongo-4.8.0-cp311-cp311-win32.whl", hash = "sha256:4bf58e6825b93da63e499d1a58de7de563c31e575908d4e24876234ccb910eba"},
|
||||
{file = "pymongo-4.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b747c0e257b9d3e6495a018309b9e0c93b7f0d65271d1d62e572747f4ffafc88"},
|
||||
{file = "pymongo-4.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e6a720a3d22b54183352dc65f08cd1547204d263e0651b213a0a2e577e838526"},
|
||||
{file = "pymongo-4.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31e4d21201bdf15064cf47ce7b74722d3e1aea2597c6785882244a3bb58c7eab"},
|
||||
{file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b804bb4f2d9dc389cc9e827d579fa327272cdb0629a99bfe5b83cb3e269ebf"},
|
||||
{file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fbdb87fe5075c8beb17a5c16348a1ea3c8b282a5cb72d173330be2fecf22f5"},
|
||||
{file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd39455b7ee70aabee46f7399b32ab38b86b236c069ae559e22be6b46b2bbfc4"},
|
||||
{file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:940d456774b17814bac5ea7fc28188c7a1338d4a233efbb6ba01de957bded2e8"},
|
||||
{file = "pymongo-4.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:236bbd7d0aef62e64caf4b24ca200f8c8670d1a6f5ea828c39eccdae423bc2b2"},
|
||||
{file = "pymongo-4.8.0-cp312-cp312-win32.whl", hash = "sha256:47ec8c3f0a7b2212dbc9be08d3bf17bc89abd211901093e3ef3f2adea7de7a69"},
|
||||
{file = "pymongo-4.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84bc7707492f06fbc37a9f215374d2977d21b72e10a67f1b31893ec5a140ad8"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:519d1bab2b5e5218c64340b57d555d89c3f6c9d717cecbf826fb9d42415e7750"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87075a1feb1e602e539bdb1ef8f4324a3427eb0d64208c3182e677d2c0718b6f"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f53429515d2b3e86dcc83dadecf7ff881e538c168d575f3688698a8707b80a"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdc20cd1e1141b04696ffcdb7c71e8a4a665db31fe72e51ec706b3bdd2d09f36"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:284d0717d1a7707744018b0b6ee7801b1b1ff044c42f7be7a01bb013de639470"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5bf0eb8b6ef40fa22479f09375468c33bebb7fe49d14d9c96c8fd50355188b0"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ecd71b9226bd1d49416dc9f999772038e56f415a713be51bf18d8676a0841c8"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0061af6e8c5e68b13f1ec9ad5251247726653c5af3c0bbdfbca6cf931e99216"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:658d0170f27984e0d89c09fe5c42296613b711a3ffd847eb373b0dbb5b648d5f"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-win32.whl", hash = "sha256:3ed1c316718a2836f7efc3d75b4b0ffdd47894090bc697de8385acd13c513a70"},
|
||||
{file = "pymongo-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:7148419eedfea9ecb940961cfe465efaba90595568a1fb97585fb535ea63fe2b"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8400587d594761e5136a3423111f499574be5fd53cf0aefa0d0f05b180710b0"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af3e98dd9702b73e4e6fd780f6925352237f5dce8d99405ff1543f3771201704"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de3a860f037bb51f968de320baef85090ff0bbb42ec4f28ec6a5ddf88be61871"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fc18b3a093f3db008c5fea0e980dbd3b743449eee29b5718bc2dc15ab5088bb"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18c9d8f975dd7194c37193583fd7d1eb9aea0c21ee58955ecf35362239ff31ac"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:408b2f8fdbeca3c19e4156f28fff1ab11c3efb0407b60687162d49f68075e63c"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6564780cafd6abeea49759fe661792bd5a67e4f51bca62b88faab497ab5fe89"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d18d86bc9e103f4d3d4f18b85a0471c0e13ce5b79194e4a0389a224bb70edd53"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9097c331577cecf8034422956daaba7ec74c26f7b255d718c584faddd7fa2e3c"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-win32.whl", hash = "sha256:d5428dbcd43d02f6306e1c3c95f692f68b284e6ee5390292242f509004c9e3a8"},
|
||||
{file = "pymongo-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:ef7225755ed27bfdb18730c68f6cb023d06c28f2b734597480fb4c0e500feb6f"},
|
||||
{file = "pymongo-4.8.0.tar.gz", hash = "sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde"},
|
||||
{file = "pymongo-4.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e699aa68c4a7dea2ab5a27067f7d3e08555f8d2c0dc6a0c8c60cfd9ff2e6a4b1"},
|
||||
{file = "pymongo-4.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70645abc714f06b4ad6b72d5bf73792eaad14e3a2cfe29c62a9c81ada69d9e4b"},
|
||||
{file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae2fd94c9fe048c94838badcc6e992d033cb9473eb31e5710b3707cba5e8aee2"},
|
||||
{file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ded27a4a5374dae03a92e084a60cdbcecd595306555bda553b833baf3fc4868"},
|
||||
{file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ecc2455e3974a6c429687b395a0bc59636f2d6aedf5785098cf4e1f180f1c71"},
|
||||
{file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920fee41f7d0259f5f72c1f1eb331bc26ffbdc952846f9bd8c3b119013bb52c"},
|
||||
{file = "pymongo-4.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0a15665b2d6cf364f4cd114d62452ce01d71abfbd9c564ba8c74dcd7bbd6822"},
|
||||
{file = "pymongo-4.10.1-cp310-cp310-win32.whl", hash = "sha256:29e1c323c28a4584b7095378ff046815e39ff82cdb8dc4cc6dfe3acf6f9ad1f8"},
|
||||
{file = "pymongo-4.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:88dc4aa45f8744ccfb45164aedb9a4179c93567bbd98a33109d7dc400b00eb08"},
|
||||
{file = "pymongo-4.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:57ee6becae534e6d47848c97f6a6dff69e3cce7c70648d6049bd586764febe59"},
|
||||
{file = "pymongo-4.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f437a612f4d4f7aca1812311b1e84477145e950fdafe3285b687ab8c52541f3"},
|
||||
{file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a970fd3117ab40a4001c3dad333bbf3c43687d90f35287a6237149b5ccae61d"},
|
||||
{file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c4d0e7cd08ef9f8fbf2d15ba281ed55604368a32752e476250724c3ce36c72e"},
|
||||
{file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca6f700cff6833de4872a4e738f43123db34400173558b558ae079b5535857a4"},
|
||||
{file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cec237c305fcbeef75c0bcbe9d223d1e22a6e3ba1b53b2f0b79d3d29c742b45b"},
|
||||
{file = "pymongo-4.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3337804ea0394a06e916add4e5fac1c89902f1b6f33936074a12505cab4ff05"},
|
||||
{file = "pymongo-4.10.1-cp311-cp311-win32.whl", hash = "sha256:778ac646ce6ac1e469664062dfe9ae1f5c9961f7790682809f5ec3b8fda29d65"},
|
||||
{file = "pymongo-4.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:9df4ab5594fdd208dcba81be815fa8a8a5d8dedaf3b346cbf8b61c7296246a7a"},
|
||||
{file = "pymongo-4.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fbedc4617faa0edf423621bb0b3b8707836687161210d470e69a4184be9ca011"},
|
||||
{file = "pymongo-4.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7bd26b2aec8ceeb95a5d948d5cc0f62b0eb6d66f3f4230705c1e3d3d2c04ec76"},
|
||||
{file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb104c3c2a78d9d85571c8ac90ec4f95bca9b297c6eee5ada71fabf1129e1674"},
|
||||
{file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4924355245a9c79f77b5cda2db36e0f75ece5faf9f84d16014c0a297f6d66786"},
|
||||
{file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11280809e5dacaef4971113f0b4ff4696ee94cfdb720019ff4fa4f9635138252"},
|
||||
{file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5d55f2a82e5eb23795f724991cac2bffbb1c0f219c0ba3bf73a835f97f1bb2e"},
|
||||
{file = "pymongo-4.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e974ab16a60be71a8dfad4e5afccf8dd05d41c758060f5d5bda9a758605d9a5d"},
|
||||
{file = "pymongo-4.10.1-cp312-cp312-win32.whl", hash = "sha256:544890085d9641f271d4f7a47684450ed4a7344d6b72d5968bfae32203b1bb7c"},
|
||||
{file = "pymongo-4.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:dcc07b1277e8b4bf4d7382ca133850e323b7ab048b8353af496d050671c7ac52"},
|
||||
{file = "pymongo-4.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:90bc6912948dfc8c363f4ead54d54a02a15a7fee6cfafb36dc450fc8962d2cb7"},
|
||||
{file = "pymongo-4.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:594dd721b81f301f33e843453638e02d92f63c198358e5a0fa8b8d0b1218dabc"},
|
||||
{file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0783e0c8e95397c84e9cf8ab092ab1e5dd7c769aec0ef3a5838ae7173b98dea0"},
|
||||
{file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fb6a72e88df46d1c1040fd32cd2d2c5e58722e5d3e31060a0393f04ad3283de"},
|
||||
{file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e3a593333e20c87415420a4fb76c00b7aae49b6361d2e2205b6fece0563bf40"},
|
||||
{file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72e2ace7456167c71cfeca7dcb47bd5dceda7db2231265b80fc625c5e8073186"},
|
||||
{file = "pymongo-4.10.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ad05eb9c97e4f589ed9e74a00fcaac0d443ccd14f38d1258eb4c39a35dd722b"},
|
||||
{file = "pymongo-4.10.1-cp313-cp313-win32.whl", hash = "sha256:ee4c86d8e6872a61f7888fc96577b0ea165eb3bdb0d841962b444fa36001e2bb"},
|
||||
{file = "pymongo-4.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:45ee87a4e12337353242bc758accc7fb47a2f2d9ecc0382a61e64c8f01e86708"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:442ca247f53ad24870a01e80a71cd81b3f2318655fd9d66748ee2bd1b1569d9e"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23e1d62df5592518204943b507be7b457fb8a4ad95a349440406fd42db5d0923"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6131bc6568b26e7495a9f3ef2b1700566b76bbecd919f4472bfe90038a61f425"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdeba88c540c9ed0338c0b2062d9f81af42b18d6646b3e6dda05cf6edd46ada9"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a624d752dd3c89d10deb0ef6431559b6d074703cab90a70bb849ece02adc6b"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba164e73fdade9b4614a2497321c5b7512ddf749ed508950bdecc28d8d76a2d9"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9235fa319993405ae5505bf1333366388add2e06848db7b3deee8f990b69808e"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4a65567bd17d19f03157c7ec992c6530eafd8191a4e5ede25566792c4fe3fa2"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f1945d48fb9b8a87d515da07f37e5b2c35b364a435f534c122e92747881f4a7c"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-win32.whl", hash = "sha256:345f8d340802ebce509f49d5833cc913da40c82f2e0daf9f60149cacc9ca680f"},
|
||||
{file = "pymongo-4.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:3a70d5efdc0387ac8cd50f9a5f379648ecfc322d14ec9e1ba8ec957e5d08c372"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15b1492cc5c7cd260229590be7218261e81684b8da6d6de2660cf743445500ce"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95207503c41b97e7ecc7e596d84a61f441b4935f11aa8332828a754e7ada8c82"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb99f003c720c6d83be02c8f1a7787c22384a8ca9a4181e406174db47a048619"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2bc1ee4b1ca2c4e7e6b7a5e892126335ec8d9215bcd3ac2fe075870fefc3358"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:93a0833c10a967effcd823b4e7445ec491f0bf6da5de0ca33629c0528f42b748"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f56707497323150bd2ed5d63067f4ffce940d0549d4ea2dfae180deec7f9363"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:409ab7d6c4223e5c85881697f365239dd3ed1b58f28e4124b846d9d488c86880"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dac78a650dc0637d610905fd06b5fa6419ae9028cf4d04d6a2657bc18a66bbce"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1ec3fa88b541e0481aff3c35194c9fac96e4d57ec5d1c122376000eb28c01431"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-win32.whl", hash = "sha256:e0e961923a7b8a1c801c43552dcb8153e45afa41749d9efbd3a6d33f45489f7a"},
|
||||
{file = "pymongo-4.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:dabe8bf1ad644e6b93f3acf90ff18536d94538ca4d27e583c6db49889e98e48f"},
|
||||
{file = "pymongo-4.10.1.tar.gz", hash = "sha256:a9de02be53b6bb98efe0b9eda84ffa1ec027fcb23a2de62c4f941d9a2f2f3330"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1455,12 +1487,12 @@ dnspython = ">=1.16.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"]
|
||||
docs = ["furo (==2023.9.10)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<8)", "sphinx-rtd-theme (>=2,<3)", "sphinxcontrib-shellcheck (>=1,<2)"]
|
||||
encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.6.0,<2.0.0)"]
|
||||
docs = ["furo (==2023.9.10)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<8)", "sphinx-autobuild (>=2020.9.1)", "sphinx-rtd-theme (>=2,<3)", "sphinxcontrib-shellcheck (>=1,<2)"]
|
||||
encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.10.0,<2.0.0)"]
|
||||
gssapi = ["pykerberos", "winkerberos (>=0.5.0)"]
|
||||
ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]
|
||||
snappy = ["python-snappy"]
|
||||
test = ["pytest (>=7)"]
|
||||
test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"]
|
||||
zstd = ["zstandard"]
|
||||
|
||||
[[package]]
|
||||
@ -1562,6 +1594,33 @@ files = [
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.9"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
|
||||
{file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
|
||||
{file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"},
|
||||
{file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"},
|
||||
{file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"},
|
||||
{file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"},
|
||||
{file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.10.2"
|
||||
@ -1581,17 +1640,20 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.13.0"
|
||||
version = "2.15.0"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6"},
|
||||
{file = "sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260"},
|
||||
{file = "sentry_sdk-2.15.0-py2.py3-none-any.whl", hash = "sha256:8fb0d1a4e1a640172f31502e4503543765a1fe8a9209779134a4ac52d4677303"},
|
||||
{file = "sentry_sdk-2.15.0.tar.gz", hash = "sha256:a599e7d3400787d6f43327b973e55a087b931ba2c592a7a7afa691f8eb5e75e2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
blinker = {version = ">=1.1", optional = true, markers = "extra == \"flask\""}
|
||||
certifi = "*"
|
||||
flask = {version = ">=0.11", optional = true, markers = "extra == \"flask\""}
|
||||
markupsafe = {version = "*", optional = true, markers = "extra == \"flask\""}
|
||||
urllib3 = ">=1.26.11"
|
||||
|
||||
[package.extras]
|
||||
@ -1680,6 +1742,27 @@ h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "webargs"
|
||||
version = "8.6.0"
|
||||
description = "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "webargs-8.6.0-py3-none-any.whl", hash = "sha256:83da4d7105643d0a50499b06d98a6ade1a330ce66d039eaa51f715172c704aba"},
|
||||
{file = "webargs-8.6.0.tar.gz", hash = "sha256:b8d098ab92bd74c659eca705afa31d681475f218cb15c1e57271fa2103c0547a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
marshmallow = ">=3.0.0"
|
||||
packaging = ">=17.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit (>=3.5,<4.0)", "tox", "webargs[tests]"]
|
||||
docs = ["Sphinx (==8.0.2)", "furo (==2024.8.6)", "sphinx-issues (==4.1.0)", "webargs[frameworks]"]
|
||||
frameworks = ["Django (>=2.2.0)", "Flask (>=0.12.5)", "aiohttp (>=3.0.8)", "bottle (>=0.12.13)", "falcon (>=2.0.0)", "pyramid (>=1.9.1)", "tornado (>=4.5.2)"]
|
||||
tests = ["pytest", "pytest-aiohttp (>=0.3.0)", "pytest-asyncio", "webargs[frameworks]", "webtest (==3.0.1)", "webtest-aiohttp (==2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.0.4"
|
||||
@ -1779,4 +1862,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "d5642c7f707d2177d79620387be647b747e9eb9b0c588af1dc79f07f138c3dd7"
|
||||
content-hash = "7b9445a4aa93727bacd9911f2b79067c2cc40abf83b05b26fec1ce7329f92b97"
|
||||
|
@ -9,20 +9,24 @@ python = "^3.12"
|
||||
flask = "^3.0.3"
|
||||
bcrypt = "^4.2.0"
|
||||
pyjwt = "^2.9.0"
|
||||
boto3 = "^1.35.4"
|
||||
flask-cors = "^4.0.1"
|
||||
boto3 = "^1.35.34"
|
||||
flask-cors = "^5.0.0"
|
||||
dnspython = "^2.6.1"
|
||||
requests = "^2.32.3"
|
||||
pymongo = "^4.8.0"
|
||||
pymongo = "^4.10.1"
|
||||
flask_limiter = "^3.8.0"
|
||||
firebase-admin = "^4.3.0"
|
||||
firebase-admin = "^6.5.0"
|
||||
blurhash-python = "^1.2.2"
|
||||
gunicorn = "^23.0.0"
|
||||
sentry-sdk = "^2.13.0"
|
||||
sentry-sdk = {extras = ["flask"], version = "^2.15.0"}
|
||||
pyOpenSSL = "^24.2.1"
|
||||
webargs = "^8.6.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.6.9"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
@ -1,12 +1,12 @@
|
||||
import os
|
||||
from pymongo import MongoClient
|
||||
from flask import g
|
||||
|
||||
db = None
|
||||
|
||||
def get_db():
|
||||
global db
|
||||
|
||||
if db is None:
|
||||
db = MongoClient(os.environ['MONGO_URL'])[os.environ['MONGO_DATABASE']]
|
||||
return db
|
||||
def get_db():
|
||||
global db
|
||||
|
||||
if db is None:
|
||||
db = MongoClient(os.environ["MONGO_URL"])[os.environ["MONGO_DATABASE"]]
|
||||
return db
|
||||
|
@ -2,33 +2,39 @@ import os
|
||||
from threading import Thread
|
||||
import requests
|
||||
|
||||
def handle_send(data):
|
||||
if 'from' not in data:
|
||||
data['from'] = '{} <{}>'.format(os.environ.get('APP_NAME'), os.environ.get('FROM_EMAIL'))
|
||||
if 'to_user' in data:
|
||||
user = data['to_user']
|
||||
data['to'] = user['username'] + ' <' + user['email'] + '>'
|
||||
del data['to_user']
|
||||
data['text'] += '\n\nFrom the team at {0}\n\n\n\n--\n\nDon\'t like this email? Choose which emails you receive from {0} by visiting {1}/settings/notifications\n\nReceived this email in error? Please let us know by contacting {2}'.format(
|
||||
os.environ.get('APP_NAME'),
|
||||
os.environ.get('APP_URL'),
|
||||
os.environ.get('CONTACT_EMAIL')
|
||||
)
|
||||
data['reply-to'] = os.environ.get('CONTACT_EMAIL')
|
||||
|
||||
base_url = os.environ.get('MAILGUN_URL')
|
||||
api_key = os.environ.get('MAILGUN_KEY')
|
||||
if base_url and api_key:
|
||||
auth = ('api', api_key)
|
||||
try:
|
||||
response = requests.post(base_url, auth=auth, data=data)
|
||||
response.raise_for_status()
|
||||
except:
|
||||
print('Unable to send email')
|
||||
else:
|
||||
print('Not sending email. Message pasted below.')
|
||||
print(data)
|
||||
def handle_send(data):
|
||||
if "from" not in data:
|
||||
data["from"] = "{} <{}>".format(
|
||||
os.environ.get("APP_NAME"), os.environ.get("FROM_EMAIL")
|
||||
)
|
||||
if "to_user" in data:
|
||||
user = data["to_user"]
|
||||
data["to"] = user["username"] + " <" + user["email"] + ">"
|
||||
del data["to_user"]
|
||||
data["text"] += (
|
||||
"\n\nFrom the team at {0}\n\n\n\n--\n\nDon't like this email? Choose which emails you receive from {0} by visiting {1}/settings/notifications\n\nReceived this email in error? Please let us know by contacting {2}".format(
|
||||
os.environ.get("APP_NAME"),
|
||||
os.environ.get("APP_URL"),
|
||||
os.environ.get("CONTACT_EMAIL"),
|
||||
)
|
||||
)
|
||||
data["reply-to"] = os.environ.get("CONTACT_EMAIL")
|
||||
|
||||
base_url = os.environ.get("MAILGUN_URL")
|
||||
api_key = os.environ.get("MAILGUN_KEY")
|
||||
if base_url and api_key:
|
||||
auth = ("api", api_key)
|
||||
try:
|
||||
response = requests.post(base_url, auth=auth, data=data)
|
||||
response.raise_for_status()
|
||||
except Exception:
|
||||
print("Unable to send email")
|
||||
else:
|
||||
print("Not sending email. Message pasted below.")
|
||||
print(data)
|
||||
|
||||
|
||||
def send(data):
|
||||
thr = Thread(target=handle_send, args=[data])
|
||||
thr.start()
|
||||
thr = Thread(target=handle_send, args=[data])
|
||||
thr.start()
|
||||
|
103
api/util/push.py
103
api/util/push.py
@ -4,52 +4,63 @@ from firebase_admin import messaging
|
||||
|
||||
default_app = firebase_admin.initialize_app()
|
||||
|
||||
def handle_send_multiple(users, title, body, extra = {}):
|
||||
tokens = []
|
||||
for user in users:
|
||||
if user.get('pushToken'): tokens.append(user['pushToken'])
|
||||
if not tokens: return
|
||||
|
||||
# Create a list containing up to 500 messages.
|
||||
messages = list(map(lambda t: messaging.Message(
|
||||
notification=messaging.Notification(title, body),
|
||||
apns=messaging.APNSConfig(
|
||||
payload=messaging.APNSPayload(
|
||||
aps=messaging.Aps(badge=1, sound='default'),
|
||||
),
|
||||
),
|
||||
token=t,
|
||||
data=extra,
|
||||
), tokens))
|
||||
try:
|
||||
response = messaging.send_all(messages)
|
||||
print('{0} messages were sent successfully'.format(response.success_count))
|
||||
except Exception as e:
|
||||
print('Error sending notification', str(e))
|
||||
def handle_send_multiple(users, title, body, extra={}):
|
||||
tokens = []
|
||||
for user in users:
|
||||
if user.get("pushToken"):
|
||||
tokens.append(user["pushToken"])
|
||||
if not tokens:
|
||||
return
|
||||
|
||||
def send_multiple(users, title, body, extra = {}):
|
||||
thr = Thread(target=handle_send_multiple, args=[users, title, body, extra])
|
||||
thr.start()
|
||||
# Create a list containing up to 500 messages.
|
||||
messages = list(
|
||||
map(
|
||||
lambda t: messaging.Message(
|
||||
notification=messaging.Notification(title, body),
|
||||
apns=messaging.APNSConfig(
|
||||
payload=messaging.APNSPayload(
|
||||
aps=messaging.Aps(badge=1, sound="default"),
|
||||
),
|
||||
),
|
||||
token=t,
|
||||
data=extra,
|
||||
),
|
||||
tokens,
|
||||
)
|
||||
)
|
||||
try:
|
||||
response = messaging.send_all(messages)
|
||||
print("{0} messages were sent successfully".format(response.success_count))
|
||||
except Exception as e:
|
||||
print("Error sending notification", str(e))
|
||||
|
||||
def send_single(user, title, body, extra = {}):
|
||||
token = user.get('pushToken')
|
||||
if not token: return
|
||||
message = messaging.Message(
|
||||
notification=messaging.Notification(
|
||||
title = title,
|
||||
body = body,
|
||||
),
|
||||
apns=messaging.APNSConfig(
|
||||
payload=messaging.APNSPayload(
|
||||
aps=messaging.Aps(badge=1, sound='default'),
|
||||
),
|
||||
),
|
||||
data = extra,
|
||||
token = token,
|
||||
)
|
||||
try:
|
||||
response = messaging.send(message)
|
||||
# Response is a message ID string.
|
||||
print('Successfully sent message:', response)
|
||||
except Exception as e:
|
||||
print('Error sending notification', str(e))
|
||||
|
||||
def send_multiple(users, title, body, extra={}):
|
||||
thr = Thread(target=handle_send_multiple, args=[users, title, body, extra])
|
||||
thr.start()
|
||||
|
||||
|
||||
def send_single(user, title, body, extra={}):
|
||||
token = user.get("pushToken")
|
||||
if not token:
|
||||
return
|
||||
message = messaging.Message(
|
||||
notification=messaging.Notification(
|
||||
title=title,
|
||||
body=body,
|
||||
),
|
||||
apns=messaging.APNSConfig(
|
||||
payload=messaging.APNSPayload(
|
||||
aps=messaging.Aps(badge=1, sound="default"),
|
||||
),
|
||||
),
|
||||
data=extra,
|
||||
token=token,
|
||||
)
|
||||
try:
|
||||
response = messaging.send(message)
|
||||
# Response is a message ID string.
|
||||
print("Successfully sent message:", response)
|
||||
except Exception as e:
|
||||
print("Error sending notification", str(e))
|
||||
|
165
api/util/util.py
165
api/util/util.py
@ -1,4 +1,5 @@
|
||||
import json, datetime
|
||||
import json
|
||||
import datetime
|
||||
from flask import request, Response
|
||||
import werkzeug
|
||||
from flask_limiter.util import get_remote_address
|
||||
@ -10,93 +11,113 @@ from util import util
|
||||
|
||||
errors = werkzeug.exceptions
|
||||
|
||||
def get_user(required = True):
|
||||
headers = request.headers
|
||||
if not headers.get('Authorization') and required:
|
||||
raise util.errors.Unauthorized('This resource requires authentication')
|
||||
if headers.get('Authorization'):
|
||||
user = accounts.get_user_context(headers.get('Authorization').replace('Bearer ', ''))
|
||||
if user is None and required:
|
||||
raise util.errors.Unauthorized('Invalid token')
|
||||
return user
|
||||
return None
|
||||
|
||||
def get_user(required=True):
|
||||
headers = request.headers
|
||||
if not headers.get("Authorization") and required:
|
||||
raise util.errors.Unauthorized("This resource requires authentication")
|
||||
if headers.get("Authorization"):
|
||||
user = accounts.get_user_context(
|
||||
headers.get("Authorization").replace("Bearer ", "")
|
||||
)
|
||||
if user is None and required:
|
||||
raise util.errors.Unauthorized("Invalid token")
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
def limit_by_client():
|
||||
data = request.get_json()
|
||||
if data:
|
||||
if data.get('email'): return data.get('email')
|
||||
if data.get('token'): return data.get('token')
|
||||
return get_remote_address()
|
||||
data = request.get_json()
|
||||
if data:
|
||||
if data.get("email"):
|
||||
return data.get("email")
|
||||
if data.get("token"):
|
||||
return data.get("token")
|
||||
return get_remote_address()
|
||||
|
||||
|
||||
def limit_by_user():
|
||||
user = util.get_user(required = False)
|
||||
return user['_id'] if user else get_remote_address()
|
||||
user = util.get_user(required=False)
|
||||
return user["_id"] if user else get_remote_address()
|
||||
|
||||
|
||||
def is_root(user):
|
||||
return user and 'root' in user.get('roles', [])
|
||||
return user and "root" in user.get("roles", [])
|
||||
|
||||
|
||||
def can_view_project(user, project):
|
||||
if not project: return False
|
||||
if project.get('visibility') == 'public':
|
||||
return True
|
||||
if not user: return False
|
||||
if project.get('visibility') == 'private' and can_edit_project(user, project):
|
||||
return True
|
||||
if set(user.get('groups', [])).intersection(project.get('groupVisibility', [])):
|
||||
return True
|
||||
if 'root' in user.get('roles', []): return True
|
||||
return False
|
||||
if not project:
|
||||
return False
|
||||
if project.get("visibility") == "public":
|
||||
return True
|
||||
if not user:
|
||||
return False
|
||||
if project.get("visibility") == "private" and can_edit_project(user, project):
|
||||
return True
|
||||
if set(user.get("groups", [])).intersection(project.get("groupVisibility", [])):
|
||||
return True
|
||||
if "root" in user.get("roles", []):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def can_edit_project(user, project):
|
||||
if not user or not project: return False
|
||||
return project.get('user') == user['_id'] or is_root(user)
|
||||
if not user or not project:
|
||||
return False
|
||||
return project.get("user") == user["_id"] or is_root(user)
|
||||
|
||||
|
||||
def filter_keys(obj, allowed_keys):
|
||||
filtered = {}
|
||||
for key in allowed_keys:
|
||||
if key in obj:
|
||||
filtered[key] = obj[key]
|
||||
return filtered
|
||||
filtered = {}
|
||||
for key in allowed_keys:
|
||||
if key in obj:
|
||||
filtered[key] = obj[key]
|
||||
return filtered
|
||||
|
||||
|
||||
def build_updater(obj, allowed_keys):
|
||||
if not obj: return {}
|
||||
allowed = filter_keys(obj, allowed_keys)
|
||||
updater = {}
|
||||
for key in allowed:
|
||||
if not allowed[key]:
|
||||
if '$unset' not in updater: updater['$unset'] = {}
|
||||
updater['$unset'][key] = ''
|
||||
else:
|
||||
if '$set' not in updater: updater['$set'] = {}
|
||||
updater['$set'][key] = allowed[key]
|
||||
return updater
|
||||
if not obj:
|
||||
return {}
|
||||
allowed = filter_keys(obj, allowed_keys)
|
||||
updater = {}
|
||||
for key in allowed:
|
||||
if not allowed[key]:
|
||||
if "$unset" not in updater:
|
||||
updater["$unset"] = {}
|
||||
updater["$unset"][key] = ""
|
||||
else:
|
||||
if "$set" not in updater:
|
||||
updater["$set"] = {}
|
||||
updater["$set"][key] = allowed[key]
|
||||
return updater
|
||||
|
||||
|
||||
def generate_rsa_keypair():
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=4096
|
||||
)
|
||||
private_pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
public_key = private_key.public_key()
|
||||
public_pem = public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
return private_pem, public_pem
|
||||
private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
|
||||
private_pem = private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
public_key = private_key.public_key()
|
||||
public_pem = public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
return private_pem, public_pem
|
||||
|
||||
|
||||
class MongoJsonEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, (datetime.datetime, datetime.date)):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, ObjectId):
|
||||
return str(obj)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
def default(self, obj):
|
||||
if isinstance(obj, (datetime.datetime, datetime.date)):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, ObjectId):
|
||||
return str(obj)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def jsonify(*args, **kwargs):
|
||||
resp_data = json.dumps(dict(*args, **kwargs), cls=MongoJsonEncoder)
|
||||
resp = Response(resp_data)
|
||||
resp.headers['Content-Type'] = 'application/json'
|
||||
return resp
|
||||
resp_data = json.dumps(dict(*args, **kwargs), cls=MongoJsonEncoder)
|
||||
resp = Response(resp_data)
|
||||
resp.headers["Content-Type"] = "application/json"
|
||||
return resp
|
||||
|
817
api/util/wif.py
817
api/util/wif.py
@ -1,410 +1,523 @@
|
||||
import io, time
|
||||
import io
|
||||
import configparser
|
||||
from PIL import Image, ImageDraw
|
||||
from api import uploads
|
||||
|
||||
|
||||
def normalise_colour(max_color, triplet):
|
||||
color_factor = 256/max_color
|
||||
components = triplet.split(',')
|
||||
new_components = []
|
||||
for component in components:
|
||||
new_components.append(str(int(float(color_factor) * int(component))))
|
||||
return ','.join(new_components)
|
||||
color_factor = 256 / max_color
|
||||
components = triplet.split(",")
|
||||
new_components = []
|
||||
for component in components:
|
||||
new_components.append(str(int(float(color_factor) * int(component))))
|
||||
return ",".join(new_components)
|
||||
|
||||
|
||||
def denormalise_colour(max_color, triplet):
|
||||
color_factor = max_color/256
|
||||
components = triplet.split(',')
|
||||
new_components = []
|
||||
for component in components:
|
||||
new_components.append(str(int(float(color_factor) * int(component))))
|
||||
return ','.join(new_components)
|
||||
color_factor = max_color / 256
|
||||
components = triplet.split(",")
|
||||
new_components = []
|
||||
for component in components:
|
||||
new_components.append(str(int(float(color_factor) * int(component))))
|
||||
return ",".join(new_components)
|
||||
|
||||
|
||||
def colour_tuple(triplet):
|
||||
if not triplet: return None
|
||||
components = triplet.split(',')
|
||||
return tuple(map(lambda c: int(c), components))
|
||||
if not triplet:
|
||||
return None
|
||||
components = triplet.split(",")
|
||||
return tuple(map(lambda c: int(c), components))
|
||||
|
||||
|
||||
def darken_colour(c_tuple, val):
|
||||
def darken(c):
|
||||
c = c * val
|
||||
if c < 0: c = 0
|
||||
if c > 255: c = 255
|
||||
return int(c)
|
||||
return tuple(map(darken, c_tuple))
|
||||
def darken(c):
|
||||
c = c * val
|
||||
if c < 0:
|
||||
c = 0
|
||||
if c > 255:
|
||||
c = 255
|
||||
return int(c)
|
||||
|
||||
return tuple(map(darken, c_tuple))
|
||||
|
||||
|
||||
def get_colour_index(colours, colour):
|
||||
for (index, c) in enumerate(colours):
|
||||
if c == colour: return index + 1
|
||||
return 1
|
||||
for index, c in enumerate(colours):
|
||||
if c == colour:
|
||||
return index + 1
|
||||
return 1
|
||||
|
||||
|
||||
def dumps(obj):
|
||||
if not obj or not obj['pattern']: raise Exception('Invalid pattern')
|
||||
wif = []
|
||||
if not obj or not obj["pattern"]:
|
||||
raise Exception("Invalid pattern")
|
||||
wif = []
|
||||
|
||||
wif.append('[WIF]')
|
||||
wif.append('Version=1.1')
|
||||
wif.append('Source Program=Treadl')
|
||||
wif.append('Source Version=1')
|
||||
wif.append("[WIF]")
|
||||
wif.append("Version=1.1")
|
||||
wif.append("Source Program=Treadl")
|
||||
wif.append("Source Version=1")
|
||||
|
||||
wif.append('\n[CONTENTS]')
|
||||
wif.append('COLOR PALETTE=true')
|
||||
wif.append('TEXT=true')
|
||||
wif.append('WEAVING=true')
|
||||
wif.append('WARP=true')
|
||||
wif.append('WARP COLORS=true')
|
||||
wif.append('WEFT COLORS=true')
|
||||
wif.append('WEFT=true')
|
||||
wif.append('COLOR TABLE=true')
|
||||
wif.append('THREADING=true')
|
||||
wif.append('TIEUP=true')
|
||||
wif.append('TREADLING=true')
|
||||
wif.append("\n[CONTENTS]")
|
||||
wif.append("COLOR PALETTE=true")
|
||||
wif.append("TEXT=true")
|
||||
wif.append("WEAVING=true")
|
||||
wif.append("WARP=true")
|
||||
wif.append("WARP COLORS=true")
|
||||
wif.append("WEFT COLORS=true")
|
||||
wif.append("WEFT=true")
|
||||
wif.append("COLOR TABLE=true")
|
||||
wif.append("THREADING=true")
|
||||
wif.append("TIEUP=true")
|
||||
wif.append("TREADLING=true")
|
||||
|
||||
wif.append('\n[TEXT]')
|
||||
wif.append('Title={0}'.format(obj['name']))
|
||||
wif.append("\n[TEXT]")
|
||||
wif.append("Title={0}".format(obj["name"]))
|
||||
|
||||
wif.append('\n[COLOR TABLE]')
|
||||
for (index, colour) in enumerate(obj['pattern']['colours']):
|
||||
wif.append('{0}={1}'.format(index + 1, denormalise_colour(999, colour)))
|
||||
wif.append("\n[COLOR TABLE]")
|
||||
for index, colour in enumerate(obj["pattern"]["colours"]):
|
||||
wif.append("{0}={1}".format(index + 1, denormalise_colour(999, colour)))
|
||||
|
||||
wif.append('\n[COLOR PALETTE]')
|
||||
wif.append('Range=0,999')
|
||||
wif.append('Entries={0}'.format(len(obj['pattern']['colours'])))
|
||||
wif.append("\n[COLOR PALETTE]")
|
||||
wif.append("Range=0,999")
|
||||
wif.append("Entries={0}".format(len(obj["pattern"]["colours"])))
|
||||
|
||||
wif.append('\n[WEAVING]')
|
||||
wif.append('Rising Shed=true')
|
||||
wif.append('Treadles={0}'.format(obj['pattern']['weft']['treadles']))
|
||||
wif.append('Shafts={0}'.format(obj['pattern']['warp']['shafts']))
|
||||
|
||||
wif.append('\n[WARP]')
|
||||
wif.append('Units=centimeters')
|
||||
wif.append('Color={0}'.format(get_colour_index(obj['pattern']['colours'], obj['pattern']['warp']['defaultColour'])))
|
||||
wif.append('Threads={0}'.format(len(obj['pattern']['warp']['threading'])))
|
||||
wif.append('Spacing=0.212')
|
||||
wif.append('Thickness=0.212')
|
||||
wif.append("\n[WEAVING]")
|
||||
wif.append("Rising Shed=true")
|
||||
wif.append("Treadles={0}".format(obj["pattern"]["weft"]["treadles"]))
|
||||
wif.append("Shafts={0}".format(obj["pattern"]["warp"]["shafts"]))
|
||||
|
||||
wif.append('\n[WARP COLORS]')
|
||||
for (index, thread) in enumerate(obj['pattern']['warp']['threading']):
|
||||
if 'colour' in thread:
|
||||
wif.append('{0}={1}'.format(index + 1, get_colour_index(obj['pattern']['colours'], thread['colour'])))
|
||||
wif.append("\n[WARP]")
|
||||
wif.append("Units=centimeters")
|
||||
wif.append(
|
||||
"Color={0}".format(
|
||||
get_colour_index(
|
||||
obj["pattern"]["colours"], obj["pattern"]["warp"]["defaultColour"]
|
||||
)
|
||||
)
|
||||
)
|
||||
wif.append("Threads={0}".format(len(obj["pattern"]["warp"]["threading"])))
|
||||
wif.append("Spacing=0.212")
|
||||
wif.append("Thickness=0.212")
|
||||
|
||||
wif.append('\n[THREADING]')
|
||||
for (index, thread) in enumerate(obj['pattern']['warp']['threading']):
|
||||
wif.append('{0}={1}'.format(index + 1, thread['shaft']))
|
||||
wif.append("\n[WARP COLORS]")
|
||||
for index, thread in enumerate(obj["pattern"]["warp"]["threading"]):
|
||||
if "colour" in thread:
|
||||
wif.append(
|
||||
"{0}={1}".format(
|
||||
index + 1,
|
||||
get_colour_index(obj["pattern"]["colours"], thread["colour"]),
|
||||
)
|
||||
)
|
||||
|
||||
wif.append('\n[WEFT]')
|
||||
wif.append('Units=centimeters')
|
||||
wif.append('Color={0}'.format(get_colour_index(obj['pattern']['colours'], obj['pattern']['weft']['defaultColour'])))
|
||||
wif.append('Threads={0}'.format(len(obj['pattern']['weft']['treadling'])))
|
||||
wif.append('Spacing=0.212')
|
||||
wif.append('Thickness=0.212')
|
||||
wif.append("\n[THREADING]")
|
||||
for index, thread in enumerate(obj["pattern"]["warp"]["threading"]):
|
||||
wif.append("{0}={1}".format(index + 1, thread["shaft"]))
|
||||
|
||||
wif.append('\n[WEFT COLORS]')
|
||||
for (index, thread) in enumerate(obj['pattern']['weft']['treadling']):
|
||||
if 'colour' in thread:
|
||||
wif.append('{0}={1}'.format(index + 1, get_colour_index(obj['pattern']['colours'], thread['colour'])))
|
||||
wif.append("\n[WEFT]")
|
||||
wif.append("Units=centimeters")
|
||||
wif.append(
|
||||
"Color={0}".format(
|
||||
get_colour_index(
|
||||
obj["pattern"]["colours"], obj["pattern"]["weft"]["defaultColour"]
|
||||
)
|
||||
)
|
||||
)
|
||||
wif.append("Threads={0}".format(len(obj["pattern"]["weft"]["treadling"])))
|
||||
wif.append("Spacing=0.212")
|
||||
wif.append("Thickness=0.212")
|
||||
|
||||
wif.append('\n[TREADLING]')
|
||||
for (index, thread) in enumerate(obj['pattern']['weft']['treadling']):
|
||||
wif.append('{0}={1}'.format(index + 1, thread['treadle']))
|
||||
wif.append("\n[WEFT COLORS]")
|
||||
for index, thread in enumerate(obj["pattern"]["weft"]["treadling"]):
|
||||
if "colour" in thread:
|
||||
wif.append(
|
||||
"{0}={1}".format(
|
||||
index + 1,
|
||||
get_colour_index(obj["pattern"]["colours"], thread["colour"]),
|
||||
)
|
||||
)
|
||||
|
||||
wif.append('\n[TIEUP]')
|
||||
for (index, tieup) in enumerate(obj['pattern']['tieups']):
|
||||
wif.append('{0}={1}'.format(str(index + 1), ','.join(str(x) for x in tieup)))
|
||||
wif.append("\n[TREADLING]")
|
||||
for index, thread in enumerate(obj["pattern"]["weft"]["treadling"]):
|
||||
wif.append("{0}={1}".format(index + 1, thread["treadle"]))
|
||||
|
||||
wif.append("\n[TIEUP]")
|
||||
for index, tieup in enumerate(obj["pattern"]["tieups"]):
|
||||
wif.append("{0}={1}".format(str(index + 1), ",".join(str(x) for x in tieup)))
|
||||
|
||||
return "\n".join(wif)
|
||||
|
||||
return '\n'.join(wif)
|
||||
|
||||
def loads(wif_file):
|
||||
config = configparser.ConfigParser(allow_no_value=True, strict=False)
|
||||
config.read_string(wif_file.lower())
|
||||
DEFAULT_TITLE = 'Untitled Pattern'
|
||||
draft = {}
|
||||
config = configparser.ConfigParser(allow_no_value=True, strict=False)
|
||||
config.read_string(wif_file.lower())
|
||||
DEFAULT_TITLE = "Untitled Pattern"
|
||||
draft = {}
|
||||
|
||||
if 'text' in config:
|
||||
text = config['text']
|
||||
draft['name'] = text.get('title') or DEFAULT_TITLE
|
||||
if not draft.get('name'):
|
||||
draft['name'] = DEFAULT_TITLE
|
||||
if "text" in config:
|
||||
text = config["text"]
|
||||
draft["name"] = text.get("title") or DEFAULT_TITLE
|
||||
if not draft.get("name"):
|
||||
draft["name"] = DEFAULT_TITLE
|
||||
|
||||
min_color = 0
|
||||
max_color = 255
|
||||
if 'color palette' in config:
|
||||
color_palette = config['color palette']
|
||||
color_range = color_palette.get('range').split(',')
|
||||
min_color = int(color_range[0])
|
||||
max_color = int(color_range[1])
|
||||
max_color = 255
|
||||
if "color palette" in config:
|
||||
color_palette = config["color palette"]
|
||||
color_range = color_palette.get("range").split(",")
|
||||
max_color = int(color_range[1])
|
||||
|
||||
if 'color table' in config:
|
||||
color_table = config['color table']
|
||||
draft['colours'] = [None]*len(color_table)
|
||||
for x in color_table:
|
||||
draft['colours'][int(x)-1] = normalise_colour(max_color, color_table[x])
|
||||
if not draft.get('colours'): draft['colours'] = []
|
||||
if len(draft['colours']) < 2:
|
||||
draft['colours'] += [normalise_colour(255, '255,255,255'), normalise_colour(255, '0,0,255')]
|
||||
if "color table" in config:
|
||||
color_table = config["color table"]
|
||||
draft["colours"] = [None] * len(color_table)
|
||||
for x in color_table:
|
||||
draft["colours"][int(x) - 1] = normalise_colour(max_color, color_table[x])
|
||||
if not draft.get("colours"):
|
||||
draft["colours"] = []
|
||||
if len(draft["colours"]) < 2:
|
||||
draft["colours"] += [
|
||||
normalise_colour(255, "255,255,255"),
|
||||
normalise_colour(255, "0,0,255"),
|
||||
]
|
||||
|
||||
weaving = config['weaving']
|
||||
weaving = config["weaving"]
|
||||
|
||||
threading = config['threading']
|
||||
warp = config['warp']
|
||||
draft['warp'] = {}
|
||||
draft['warp']['shafts'] = weaving.getint('shafts')
|
||||
draft['warp']['threading'] = []
|
||||
|
||||
|
||||
if warp.get('color'):
|
||||
warp_colour_index = warp.getint('color') - 1
|
||||
draft['warp']['defaultColour'] = draft['colours'][warp_colour_index]
|
||||
threading = config["threading"]
|
||||
warp = config["warp"]
|
||||
draft["warp"] = {}
|
||||
draft["warp"]["shafts"] = weaving.getint("shafts")
|
||||
draft["warp"]["threading"] = []
|
||||
|
||||
else:
|
||||
# In case of no color table or colour index out of bounds
|
||||
draft['warp']['defaultColour'] = draft['colours'][0]
|
||||
if warp.get("color"):
|
||||
warp_colour_index = warp.getint("color") - 1
|
||||
draft["warp"]["defaultColour"] = draft["colours"][warp_colour_index]
|
||||
|
||||
for x in threading:
|
||||
shaft = threading[x]
|
||||
if ',' in shaft:
|
||||
shaft = shaft.split(",")[0]
|
||||
shaft = int(shaft)
|
||||
while int(x) >= len(draft['warp']['threading']) - 1:
|
||||
draft['warp']['threading'].append({'shaft': 0})
|
||||
draft['warp']['threading'][int(x) - 1] = {'shaft': shaft}
|
||||
try:
|
||||
warp_colours = config['warp colors']
|
||||
for x in warp_colours:
|
||||
draft['warp']['threading'][int(x) - 1]['colour'] = draft['colours'][warp_colours.getint(x)-1]
|
||||
except Exception as e:
|
||||
pass
|
||||
else:
|
||||
# In case of no color table or colour index out of bounds
|
||||
draft["warp"]["defaultColour"] = draft["colours"][0]
|
||||
|
||||
treadling = config['treadling']
|
||||
weft = config['weft']
|
||||
draft['weft'] = {}
|
||||
draft['weft']['treadles'] = weaving.getint('treadles')
|
||||
draft['weft']['treadling'] = []
|
||||
|
||||
if weft.get('color'):
|
||||
weft_colour_index = weft.getint('color') - 1
|
||||
draft['weft']['defaultColour'] = draft['colours'][weft_colour_index]
|
||||
else:
|
||||
# In case of no color table or colour index out of bounds
|
||||
draft['weft']['defaultColour'] = draft['colours'][1]
|
||||
|
||||
for x in treadling:
|
||||
shaft = treadling[x]
|
||||
if ',' in shaft:
|
||||
shaft = shaft.split(",")[0]
|
||||
shaft = int(shaft)
|
||||
while int(x) >= len(draft['weft']['treadling']) - 1:
|
||||
draft['weft']['treadling'].append({'treadle': 0})
|
||||
draft['weft']['treadling'][int(x) - 1] = {'treadle': shaft}
|
||||
try:
|
||||
weft_colours = config['weft colors']
|
||||
for x in weft_colours:
|
||||
draft['weft']['treadling'][int(x) - 1]['colour'] = draft['colours'][weft_colours.getint(x)-1]
|
||||
except: pass
|
||||
|
||||
tieup = config['tieup']
|
||||
draft['tieups'] = []#[0]*len(tieup)
|
||||
for x in tieup:
|
||||
while int(x) >= len(draft['tieups']) - 1:
|
||||
draft['tieups'].append([])
|
||||
split = tieup[x].split(',')
|
||||
for x in threading:
|
||||
shaft = threading[x]
|
||||
if "," in shaft:
|
||||
shaft = shaft.split(",")[0]
|
||||
shaft = int(shaft)
|
||||
while int(x) >= len(draft["warp"]["threading"]) - 1:
|
||||
draft["warp"]["threading"].append({"shaft": 0})
|
||||
draft["warp"]["threading"][int(x) - 1] = {"shaft": shaft}
|
||||
try:
|
||||
draft['tieups'][int(x)-1] = [int(i) for i in split]
|
||||
except:
|
||||
draft['tieups'][int(x)-1] = []
|
||||
warp_colours = config["warp colors"]
|
||||
for x in warp_colours:
|
||||
draft["warp"]["threading"][int(x) - 1]["colour"] = draft["colours"][
|
||||
warp_colours.getint(x) - 1
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
treadling = config["treadling"]
|
||||
weft = config["weft"]
|
||||
draft["weft"] = {}
|
||||
draft["weft"]["treadles"] = weaving.getint("treadles")
|
||||
draft["weft"]["treadling"] = []
|
||||
|
||||
if weft.get("color"):
|
||||
weft_colour_index = weft.getint("color") - 1
|
||||
draft["weft"]["defaultColour"] = draft["colours"][weft_colour_index]
|
||||
else:
|
||||
# In case of no color table or colour index out of bounds
|
||||
draft["weft"]["defaultColour"] = draft["colours"][1]
|
||||
|
||||
for x in treadling:
|
||||
shaft = treadling[x]
|
||||
if "," in shaft:
|
||||
shaft = shaft.split(",")[0]
|
||||
shaft = int(shaft)
|
||||
while int(x) >= len(draft["weft"]["treadling"]) - 1:
|
||||
draft["weft"]["treadling"].append({"treadle": 0})
|
||||
draft["weft"]["treadling"][int(x) - 1] = {"treadle": shaft}
|
||||
try:
|
||||
weft_colours = config["weft colors"]
|
||||
for x in weft_colours:
|
||||
draft["weft"]["treadling"][int(x) - 1]["colour"] = draft["colours"][
|
||||
weft_colours.getint(x) - 1
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
tieup = config["tieup"]
|
||||
draft["tieups"] = [] # [0]*len(tieup)
|
||||
for x in tieup:
|
||||
while int(x) >= len(draft["tieups"]) - 1:
|
||||
draft["tieups"].append([])
|
||||
split = tieup[x].split(",")
|
||||
try:
|
||||
draft["tieups"][int(x) - 1] = [int(i) for i in split]
|
||||
except Exception:
|
||||
draft["tieups"][int(x) - 1] = []
|
||||
|
||||
return draft
|
||||
|
||||
return draft
|
||||
|
||||
def generate_images(obj):
|
||||
try:
|
||||
return {
|
||||
'preview': draw_image(obj),
|
||||
'fullPreview': draw_image(obj, with_plan=True)
|
||||
}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {}
|
||||
try:
|
||||
return {
|
||||
"preview": draw_image(obj),
|
||||
"fullPreview": draw_image(obj, with_plan=True),
|
||||
}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {}
|
||||
|
||||
|
||||
def draw_image(obj, with_plan=False):
|
||||
if not obj or not obj['pattern']: raise Exception('Invalid pattern')
|
||||
BASE_SIZE = 10
|
||||
pattern = obj['pattern']
|
||||
warp = pattern['warp']
|
||||
weft = pattern['weft']
|
||||
tieups = pattern['tieups']
|
||||
if not obj or not obj["pattern"]:
|
||||
raise Exception("Invalid pattern")
|
||||
BASE_SIZE = 10
|
||||
pattern = obj["pattern"]
|
||||
warp = pattern["warp"]
|
||||
weft = pattern["weft"]
|
||||
tieups = pattern["tieups"]
|
||||
|
||||
full_width = len(warp['threading']) * BASE_SIZE + BASE_SIZE + weft['treadles'] * BASE_SIZE + BASE_SIZE if with_plan else len(warp['threading']) * BASE_SIZE
|
||||
full_height = warp['shafts'] * BASE_SIZE + len(weft['treadling']) * BASE_SIZE + BASE_SIZE * 2 if with_plan else len(weft['treadling']) * BASE_SIZE
|
||||
full_width = (
|
||||
len(warp["threading"]) * BASE_SIZE
|
||||
+ BASE_SIZE
|
||||
+ weft["treadles"] * BASE_SIZE
|
||||
+ BASE_SIZE
|
||||
if with_plan
|
||||
else len(warp["threading"]) * BASE_SIZE
|
||||
)
|
||||
full_height = (
|
||||
warp["shafts"] * BASE_SIZE + len(weft["treadling"]) * BASE_SIZE + BASE_SIZE * 2
|
||||
if with_plan
|
||||
else len(weft["treadling"]) * BASE_SIZE
|
||||
)
|
||||
|
||||
warp_top = 0
|
||||
warp_left = 0
|
||||
warp_right = len(warp['threading']) * BASE_SIZE
|
||||
warp_bottom = warp['shafts'] * BASE_SIZE + BASE_SIZE
|
||||
warp_top = 0
|
||||
warp_left = 0
|
||||
warp_right = len(warp["threading"]) * BASE_SIZE
|
||||
warp_bottom = warp["shafts"] * BASE_SIZE + BASE_SIZE
|
||||
|
||||
weft_left = warp_right + BASE_SIZE
|
||||
weft_top = warp['shafts'] * BASE_SIZE + BASE_SIZE * 2
|
||||
weft_right = warp_right + BASE_SIZE + weft['treadles'] * BASE_SIZE + BASE_SIZE
|
||||
weft_bottom = weft_top + len(weft['treadling']) * BASE_SIZE
|
||||
weft_left = warp_right + BASE_SIZE
|
||||
weft_top = warp["shafts"] * BASE_SIZE + BASE_SIZE * 2
|
||||
weft_right = warp_right + BASE_SIZE + weft["treadles"] * BASE_SIZE + BASE_SIZE
|
||||
weft_bottom = weft_top + len(weft["treadling"]) * BASE_SIZE
|
||||
|
||||
tieup_left = warp_right + BASE_SIZE
|
||||
tieup_top = BASE_SIZE
|
||||
tieup_right = tieup_left + weft['treadles'] * BASE_SIZE
|
||||
tieup_bottom = warp_bottom
|
||||
tieup_left = warp_right + BASE_SIZE
|
||||
tieup_top = BASE_SIZE
|
||||
tieup_right = tieup_left + weft["treadles"] * BASE_SIZE
|
||||
tieup_bottom = warp_bottom
|
||||
|
||||
drawdown_top = warp_bottom + BASE_SIZE if with_plan else 0
|
||||
drawdown_right = warp_right if with_plan else full_width
|
||||
drawdown_left = warp_left if with_plan else 0
|
||||
drawdown_bottom = weft_bottom if with_plan else full_height
|
||||
drawdown_top = warp_bottom + BASE_SIZE if with_plan else 0
|
||||
drawdown_right = warp_right if with_plan else full_width
|
||||
drawdown_left = warp_left if with_plan else 0
|
||||
drawdown_bottom = weft_bottom if with_plan else full_height
|
||||
|
||||
WHITE=(255,255,255)
|
||||
GREY = (150,150,150)
|
||||
BLACK = (0,0,0)
|
||||
img = Image.new("RGBA", (full_width, full_height), WHITE)
|
||||
draw = ImageDraw.Draw(img)
|
||||
WHITE = (255, 255, 255)
|
||||
GREY = (150, 150, 150)
|
||||
BLACK = (0, 0, 0)
|
||||
img = Image.new("RGBA", (full_width, full_height), WHITE)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Draw warp
|
||||
if with_plan:
|
||||
draw.rectangle([
|
||||
(warp_left, warp_top),
|
||||
(warp_right, warp_bottom)
|
||||
], fill=None, outline=GREY, width=1)
|
||||
for y in range(1, warp['shafts'] + 1):
|
||||
ycoord = y * BASE_SIZE
|
||||
draw.line([
|
||||
(warp_left, ycoord),
|
||||
(warp_right, ycoord),
|
||||
],
|
||||
fill=GREY, width=1, joint=None)
|
||||
for (i, x) in enumerate(range(len(warp['threading'])-1, 0, -1)):
|
||||
thread = warp['threading'][i]
|
||||
xcoord = x * BASE_SIZE
|
||||
draw.line([
|
||||
(xcoord, warp_top),
|
||||
(xcoord, warp_bottom),
|
||||
],
|
||||
fill=GREY, width=1, joint=None)
|
||||
if thread.get('shaft', 0) > 0:
|
||||
ycoord = warp_bottom - (thread['shaft'] * BASE_SIZE)
|
||||
draw.rectangle([
|
||||
(xcoord, ycoord),
|
||||
(xcoord + BASE_SIZE, ycoord + BASE_SIZE)
|
||||
], fill=BLACK, outline=None, width=1)
|
||||
colour = warp['defaultColour']
|
||||
if thread and thread.get('colour'):
|
||||
colour = thread['colour']
|
||||
draw.rectangle([
|
||||
(xcoord, warp_top),
|
||||
(xcoord + BASE_SIZE, warp_top + BASE_SIZE),
|
||||
], fill=colour_tuple(colour))
|
||||
|
||||
# Draw weft
|
||||
draw.rectangle([
|
||||
(weft_left, weft_top),
|
||||
(weft_right, weft_bottom)
|
||||
], fill=None, outline=GREY, width=1)
|
||||
for x in range(1, weft['treadles'] + 1):
|
||||
xcoord = weft_left + x * BASE_SIZE
|
||||
draw.line([
|
||||
(xcoord, weft_top),
|
||||
(xcoord, weft_bottom),
|
||||
],
|
||||
fill=GREY, width=1, joint=None)
|
||||
for (i, y) in enumerate(range(0, len(weft['treadling']))):
|
||||
thread = weft['treadling'][i]
|
||||
ycoord = weft_top + y * BASE_SIZE
|
||||
draw.line([
|
||||
(weft_left, ycoord),
|
||||
(weft_right, ycoord),
|
||||
],
|
||||
fill=GREY, width=1, joint=None)
|
||||
if thread.get('treadle', 0) > 0:
|
||||
xcoord = weft_left + (thread['treadle'] - 1) * BASE_SIZE
|
||||
draw.rectangle([
|
||||
(xcoord, ycoord),
|
||||
(xcoord + BASE_SIZE, ycoord + BASE_SIZE)
|
||||
], fill=BLACK, outline=None, width=1)
|
||||
colour = weft['defaultColour']
|
||||
if thread and thread.get('colour'):
|
||||
colour = thread['colour']
|
||||
draw.rectangle([
|
||||
(weft_right - BASE_SIZE, ycoord),
|
||||
(weft_right, ycoord + BASE_SIZE),
|
||||
], fill=colour_tuple(colour))
|
||||
# Draw warp
|
||||
if with_plan:
|
||||
draw.rectangle(
|
||||
[(warp_left, warp_top), (warp_right, warp_bottom)],
|
||||
fill=None,
|
||||
outline=GREY,
|
||||
width=1,
|
||||
)
|
||||
for y in range(1, warp["shafts"] + 1):
|
||||
ycoord = y * BASE_SIZE
|
||||
draw.line(
|
||||
[
|
||||
(warp_left, ycoord),
|
||||
(warp_right, ycoord),
|
||||
],
|
||||
fill=GREY,
|
||||
width=1,
|
||||
joint=None,
|
||||
)
|
||||
for i, x in enumerate(range(len(warp["threading"]) - 1, 0, -1)):
|
||||
thread = warp["threading"][i]
|
||||
xcoord = x * BASE_SIZE
|
||||
draw.line(
|
||||
[
|
||||
(xcoord, warp_top),
|
||||
(xcoord, warp_bottom),
|
||||
],
|
||||
fill=GREY,
|
||||
width=1,
|
||||
joint=None,
|
||||
)
|
||||
if thread.get("shaft", 0) > 0:
|
||||
ycoord = warp_bottom - (thread["shaft"] * BASE_SIZE)
|
||||
draw.rectangle(
|
||||
[(xcoord, ycoord), (xcoord + BASE_SIZE, ycoord + BASE_SIZE)],
|
||||
fill=BLACK,
|
||||
outline=None,
|
||||
width=1,
|
||||
)
|
||||
colour = warp["defaultColour"]
|
||||
if thread and thread.get("colour"):
|
||||
colour = thread["colour"]
|
||||
draw.rectangle(
|
||||
[
|
||||
(xcoord, warp_top),
|
||||
(xcoord + BASE_SIZE, warp_top + BASE_SIZE),
|
||||
],
|
||||
fill=colour_tuple(colour),
|
||||
)
|
||||
|
||||
# Draw tieups
|
||||
draw.rectangle([
|
||||
(tieup_left, tieup_top),
|
||||
(tieup_right, tieup_bottom)
|
||||
], fill=None, outline=GREY, width=1)
|
||||
for y in range(1, warp['shafts'] + 1):
|
||||
ycoord = y * BASE_SIZE
|
||||
draw.line([
|
||||
(tieup_left, ycoord),
|
||||
(tieup_right, ycoord),
|
||||
],
|
||||
fill=GREY, width=1, joint=None)
|
||||
for (x, tieup) in enumerate(tieups):
|
||||
xcoord = tieup_left + x * BASE_SIZE
|
||||
draw.line([
|
||||
(xcoord, tieup_top),
|
||||
(xcoord, tieup_bottom),
|
||||
],
|
||||
fill=GREY, width=1, joint=None)
|
||||
for entry in tieup:
|
||||
if entry > 0:
|
||||
ycoord = tieup_bottom - (entry * BASE_SIZE)
|
||||
draw.rectangle([
|
||||
(xcoord, ycoord),
|
||||
(xcoord + BASE_SIZE, ycoord + BASE_SIZE)
|
||||
], fill=BLACK, outline=None, width=1)
|
||||
# Draw weft
|
||||
draw.rectangle(
|
||||
[(weft_left, weft_top), (weft_right, weft_bottom)],
|
||||
fill=None,
|
||||
outline=GREY,
|
||||
width=1,
|
||||
)
|
||||
for x in range(1, weft["treadles"] + 1):
|
||||
xcoord = weft_left + x * BASE_SIZE
|
||||
draw.line(
|
||||
[
|
||||
(xcoord, weft_top),
|
||||
(xcoord, weft_bottom),
|
||||
],
|
||||
fill=GREY,
|
||||
width=1,
|
||||
joint=None,
|
||||
)
|
||||
for i, y in enumerate(range(0, len(weft["treadling"]))):
|
||||
thread = weft["treadling"][i]
|
||||
ycoord = weft_top + y * BASE_SIZE
|
||||
draw.line(
|
||||
[
|
||||
(weft_left, ycoord),
|
||||
(weft_right, ycoord),
|
||||
],
|
||||
fill=GREY,
|
||||
width=1,
|
||||
joint=None,
|
||||
)
|
||||
if thread.get("treadle", 0) > 0:
|
||||
xcoord = weft_left + (thread["treadle"] - 1) * BASE_SIZE
|
||||
draw.rectangle(
|
||||
[(xcoord, ycoord), (xcoord + BASE_SIZE, ycoord + BASE_SIZE)],
|
||||
fill=BLACK,
|
||||
outline=None,
|
||||
width=1,
|
||||
)
|
||||
colour = weft["defaultColour"]
|
||||
if thread and thread.get("colour"):
|
||||
colour = thread["colour"]
|
||||
draw.rectangle(
|
||||
[
|
||||
(weft_right - BASE_SIZE, ycoord),
|
||||
(weft_right, ycoord + BASE_SIZE),
|
||||
],
|
||||
fill=colour_tuple(colour),
|
||||
)
|
||||
|
||||
# Draw drawdown
|
||||
draw.rectangle([
|
||||
(drawdown_left, drawdown_top),
|
||||
(drawdown_right, drawdown_bottom)
|
||||
], fill=None, outline=(0,0,0), width=1)
|
||||
for (y, weft_thread) in enumerate(weft['treadling']):
|
||||
for (x, warp_thread) in enumerate(warp['threading']):
|
||||
# Ensure selected treadle and shaft is within configured pattern range
|
||||
treadle = 0 if weft_thread['treadle'] > weft['treadles'] else weft_thread['treadle']
|
||||
shaft = 0 if warp_thread['shaft'] > warp['shafts'] else warp_thread['shaft']
|
||||
# Draw tieups
|
||||
draw.rectangle(
|
||||
[(tieup_left, tieup_top), (tieup_right, tieup_bottom)],
|
||||
fill=None,
|
||||
outline=GREY,
|
||||
width=1,
|
||||
)
|
||||
for y in range(1, warp["shafts"] + 1):
|
||||
ycoord = y * BASE_SIZE
|
||||
draw.line(
|
||||
[
|
||||
(tieup_left, ycoord),
|
||||
(tieup_right, ycoord),
|
||||
],
|
||||
fill=GREY,
|
||||
width=1,
|
||||
joint=None,
|
||||
)
|
||||
for x, tieup in enumerate(tieups):
|
||||
xcoord = tieup_left + x * BASE_SIZE
|
||||
draw.line(
|
||||
[
|
||||
(xcoord, tieup_top),
|
||||
(xcoord, tieup_bottom),
|
||||
],
|
||||
fill=GREY,
|
||||
width=1,
|
||||
joint=None,
|
||||
)
|
||||
for entry in tieup:
|
||||
if entry > 0:
|
||||
ycoord = tieup_bottom - (entry * BASE_SIZE)
|
||||
draw.rectangle(
|
||||
[(xcoord, ycoord), (xcoord + BASE_SIZE, ycoord + BASE_SIZE)],
|
||||
fill=BLACK,
|
||||
outline=None,
|
||||
width=1,
|
||||
)
|
||||
|
||||
# Work out if should be warp or weft in "front"
|
||||
tieup = tieups[treadle-1] if treadle > 0 else []
|
||||
tieup = [t for t in tieup if t <= warp['shafts']]
|
||||
thread_type = 'warp' if shaft in tieup else 'weft'
|
||||
# Draw drawdown
|
||||
draw.rectangle(
|
||||
[(drawdown_left, drawdown_top), (drawdown_right, drawdown_bottom)],
|
||||
fill=None,
|
||||
outline=(0, 0, 0),
|
||||
width=1,
|
||||
)
|
||||
for y, weft_thread in enumerate(weft["treadling"]):
|
||||
for x, warp_thread in enumerate(warp["threading"]):
|
||||
# Ensure selected treadle and shaft is within configured pattern range
|
||||
treadle = (
|
||||
0
|
||||
if weft_thread["treadle"] > weft["treadles"]
|
||||
else weft_thread["treadle"]
|
||||
)
|
||||
shaft = 0 if warp_thread["shaft"] > warp["shafts"] else warp_thread["shaft"]
|
||||
|
||||
# Calculate current colour
|
||||
weft_colour = weft_thread.get('colour') or weft.get('defaultColour')
|
||||
warp_colour = warp_thread.get('colour') or warp.get('defaultColour')
|
||||
colour = colour_tuple(warp_colour if thread_type == 'warp' else weft_colour)
|
||||
# Work out if should be warp or weft in "front"
|
||||
tieup = tieups[treadle - 1] if treadle > 0 else []
|
||||
tieup = [t for t in tieup if t <= warp["shafts"]]
|
||||
thread_type = "warp" if shaft in tieup else "weft"
|
||||
|
||||
# Calculate drawdown coordinates
|
||||
x1 = drawdown_right - (x + 1) * BASE_SIZE
|
||||
x2 = drawdown_right - x * BASE_SIZE
|
||||
y1 = drawdown_top + y * BASE_SIZE
|
||||
y2 = drawdown_top + (y + 1) * BASE_SIZE
|
||||
# Calculate current colour
|
||||
weft_colour = weft_thread.get("colour") or weft.get("defaultColour")
|
||||
warp_colour = warp_thread.get("colour") or warp.get("defaultColour")
|
||||
colour = colour_tuple(warp_colour if thread_type == "warp" else weft_colour)
|
||||
|
||||
# Draw the thread, with shadow
|
||||
d = [0.6, 0.8, 0.9, 1.1, 1.3, 1.3, 1.1, 0.9, 0.8, 0.6, 0.5]
|
||||
if thread_type == 'warp':
|
||||
for (i, grad_x) in enumerate(range(x1, x2)):
|
||||
draw.line([
|
||||
(grad_x, y1), (grad_x, y2),
|
||||
],
|
||||
fill=(darken_colour(colour, d[i])), width=1, joint=None)
|
||||
else:
|
||||
for (i, grad_y) in enumerate(range(y1, y2)):
|
||||
draw.line([
|
||||
(x1, grad_y), (x2, grad_y),
|
||||
],
|
||||
fill=(darken_colour(colour, d[i])), width=1, joint=None)
|
||||
# Calculate drawdown coordinates
|
||||
x1 = drawdown_right - (x + 1) * BASE_SIZE
|
||||
x2 = drawdown_right - x * BASE_SIZE
|
||||
y1 = drawdown_top + y * BASE_SIZE
|
||||
y2 = drawdown_top + (y + 1) * BASE_SIZE
|
||||
|
||||
in_mem_file = io.BytesIO()
|
||||
img.save(in_mem_file, 'PNG')
|
||||
in_mem_file.seek(0)
|
||||
file_name = 'preview-{0}_{1}.png'.format(
|
||||
'full' if with_plan else 'base', obj['_id']
|
||||
)
|
||||
path = 'projects/{}/{}'.format(obj['project'], file_name)
|
||||
uploads.upload_file(path, in_mem_file)
|
||||
return file_name
|
||||
# Draw the thread, with shadow
|
||||
d = [0.6, 0.8, 0.9, 1.1, 1.3, 1.3, 1.1, 0.9, 0.8, 0.6, 0.5]
|
||||
if thread_type == "warp":
|
||||
for i, grad_x in enumerate(range(x1, x2)):
|
||||
draw.line(
|
||||
[
|
||||
(grad_x, y1),
|
||||
(grad_x, y2),
|
||||
],
|
||||
fill=(darken_colour(colour, d[i])),
|
||||
width=1,
|
||||
joint=None,
|
||||
)
|
||||
else:
|
||||
for i, grad_y in enumerate(range(y1, y2)):
|
||||
draw.line(
|
||||
[
|
||||
(x1, grad_y),
|
||||
(x2, grad_y),
|
||||
],
|
||||
fill=(darken_colour(colour, d[i])),
|
||||
width=1,
|
||||
joint=None,
|
||||
)
|
||||
|
||||
in_mem_file = io.BytesIO()
|
||||
img.save(in_mem_file, "PNG")
|
||||
in_mem_file.seek(0)
|
||||
file_name = "preview-{0}_{1}.png".format(
|
||||
"full" if with_plan else "base", obj["_id"]
|
||||
)
|
||||
path = "projects/{}/{}".format(obj["project"], file_name)
|
||||
uploads.upload_file(path, in_mem_file)
|
||||
return file_name
|
||||
|
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-code-frame-npm-7.25.7-40a9f53f43-f235cdf9c5.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-code-frame-npm-7.25.7-40a9f53f43-f235cdf9c5.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-compat-data-npm-7.25.7-81c45097d1-d1188aed1f.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-compat-data-npm-7.25.7-81c45097d1-d1188aed1f.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-core-npm-7.25.7-27df82c8ce-80560a962e.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-core-npm-7.25.7-27df82c8ce-80560a962e.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-generator-npm-7.25.7-68dd72ad91-f81cf9dc01.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-generator-npm-7.25.7-68dd72ad91-f81cf9dc01.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-helper-compilation-targets-npm-7.25.7-912ef98d47-5b57e7d4b9.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-helper-compilation-targets-npm-7.25.7-912ef98d47-5b57e7d4b9.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-helper-module-imports-npm-7.25.7-f7b3a083a0-a7255755e9.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-helper-module-imports-npm-7.25.7-f7b3a083a0-a7255755e9.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-helper-module-transforms-npm-7.25.7-01310522f0-b1daeded78.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-helper-module-transforms-npm-7.25.7-01310522f0-b1daeded78.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-helper-plugin-utils-npm-7.25.7-0b7fcf14ca-eef4450361.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-helper-plugin-utils-npm-7.25.7-0b7fcf14ca-eef4450361.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-helper-simple-access-npm-7.25.7-3a9e5cd6e8-684d0b0330.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-helper-simple-access-npm-7.25.7-3a9e5cd6e8-684d0b0330.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-helper-string-parser-npm-7.25.7-352069de58-0835fda5ef.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-helper-string-parser-npm-7.25.7-352069de58-0835fda5ef.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-helper-validator-identifier-npm-7.25.7-1c758f0472-062f55208d.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-helper-validator-identifier-npm-7.25.7-1c758f0472-062f55208d.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-helper-validator-option-npm-7.25.7-8c969bf588-87b801fe7d.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-helper-validator-option-npm-7.25.7-8c969bf588-87b801fe7d.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-helpers-npm-7.25.7-267b4cec46-a732428509.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-helpers-npm-7.25.7-267b4cec46-a732428509.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-highlight-npm-7.25.7-308b20da71-b6aa45c5bf.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-highlight-npm-7.25.7-308b20da71-b6aa45c5bf.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-parser-npm-7.25.7-3b9bdaa40d-7c40c2881e.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-parser-npm-7.25.7-3b9bdaa40d-7c40c2881e.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-plugin-transform-react-jsx-self-npm-7.25.7-1a6e2e6ec0-bce354e287.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-plugin-transform-react-jsx-self-npm-7.25.7-1a6e2e6ec0-bce354e287.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-plugin-transform-react-jsx-source-npm-7.25.7-1f60cac636-1f87d8fa16.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-plugin-transform-react-jsx-source-npm-7.25.7-1f60cac636-1f87d8fa16.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-runtime-npm-7.25.7-67dab27f3f-1d6133ed1c.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-runtime-npm-7.25.7-67dab27f3f-1d6133ed1c.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-template-npm-7.25.7-4db3590fa2-83f025a4a7.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-template-npm-7.25.7-4db3590fa2-83f025a4a7.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-traverse-npm-7.25.7-0ca7b771fb-4d329b6e7a.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-traverse-npm-7.25.7-0ca7b771fb-4d329b6e7a.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@babel-types-npm-7.25.7-067945f10b-a63a3ecdac.zip
vendored
Normal file
BIN
web/.yarn/cache/@babel-types-npm-7.25.7-067945f10b-a63a3ecdac.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@emotion-is-prop-valid-npm-1.2.2-53f93f2b2d-61f6b128ea.zip
vendored
Normal file
BIN
web/.yarn/cache/@emotion-is-prop-valid-npm-1.2.2-53f93f2b2d-61f6b128ea.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@emotion-memoize-npm-0.8.1-9b1e35ff15-a19cc01a29.zip
vendored
Normal file
BIN
web/.yarn/cache/@emotion-memoize-npm-0.8.1-9b1e35ff15-a19cc01a29.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
web/.yarn/cache/@emotion-unitless-npm-0.8.1-bcf0a8f565-385e21d184.zip
vendored
Normal file
BIN
web/.yarn/cache/@emotion-unitless-npm-0.8.1-bcf0a8f565-385e21d184.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@esbuild-darwin-arm64-npm-0.21.5-62349c1520-8.zip
vendored
Normal file
BIN
web/.yarn/cache/@esbuild-darwin-arm64-npm-0.21.5-62349c1520-8.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@eslint-community-eslint-utils-npm-4.4.0-d1791bd5a3-cdfe3ae42b.zip
vendored
Normal file
BIN
web/.yarn/cache/@eslint-community-eslint-utils-npm-4.4.0-d1791bd5a3-cdfe3ae42b.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@eslint-community-regexpp-npm-4.11.1-37bbb67aaa-6986685529.zip
vendored
Normal file
BIN
web/.yarn/cache/@eslint-community-regexpp-npm-4.11.1-37bbb67aaa-6986685529.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@eslint-eslintrc-npm-2.1.4-1ff4b5f908-10957c7592.zip
vendored
Normal file
BIN
web/.yarn/cache/@eslint-eslintrc-npm-2.1.4-1ff4b5f908-10957c7592.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@eslint-js-npm-8.57.1-dec269f278-2afb77454c.zip
vendored
Normal file
BIN
web/.yarn/cache/@eslint-js-npm-8.57.1-dec269f278-2afb77454c.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@fluentui-react-component-event-listener-npm-0.63.1-c9f86c7846-9ad5657a08.zip
vendored
Normal file
BIN
web/.yarn/cache/@fluentui-react-component-event-listener-npm-0.63.1-c9f86c7846-9ad5657a08.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@fluentui-react-component-ref-npm-0.63.1-e73ab076ce-e63ab04445.zip
vendored
Normal file
BIN
web/.yarn/cache/@fluentui-react-component-ref-npm-0.63.1-e73ab076ce-e63ab04445.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@humanwhocodes-config-array-npm-0.13.0-843095a032-eae69ff913.zip
vendored
Normal file
BIN
web/.yarn/cache/@humanwhocodes-config-array-npm-0.13.0-843095a032-eae69ff913.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@humanwhocodes-module-importer-npm-1.0.1-9d07ed2e4a-0fd22007db.zip
vendored
Normal file
BIN
web/.yarn/cache/@humanwhocodes-module-importer-npm-1.0.1-9d07ed2e4a-0fd22007db.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@humanwhocodes-object-schema-npm-2.0.3-4f0e508cd9-d3b78f6c58.zip
vendored
Normal file
BIN
web/.yarn/cache/@humanwhocodes-object-schema-npm-2.0.3-4f0e508cd9-d3b78f6c58.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-a970d595bd.zip
vendored
Normal file
BIN
web/.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-a970d595bd.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@nodelib-fs.stat-npm-2.0.5-01f4dd3030-012480b5ca.zip
vendored
Normal file
BIN
web/.yarn/cache/@nodelib-fs.stat-npm-2.0.5-01f4dd3030-012480b5ca.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@nodelib-fs.walk-npm-1.2.8-b4a89da548-190c643f15.zip
vendored
Normal file
BIN
web/.yarn/cache/@nodelib-fs.walk-npm-1.2.8-b4a89da548-190c643f15.zip
vendored
Normal file
Binary file not shown.
BIN
web/.yarn/cache/@popperjs-core-npm-2.11.8-f1692e11a0-e5c69fdebf.zip
vendored
Normal file
BIN
web/.yarn/cache/@popperjs-core-npm-2.11.8-f1692e11a0-e5c69fdebf.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user