Compare commits

..

15 Commits

Author SHA1 Message Date
059fc0d966 Update woodpecker file
Some checks are pending
ci/woodpecker/manual/woodpecker Pipeline is pending
ci/woodpecker/push/woodpecker Pipeline was successful
2024-10-06 19:41:59 +01:00
f8f06f8b68 Final tweaks 2024-10-06 19:40:10 +01:00
e74a7461fa Add all webargs: 2024-10-06 19:33:10 +01:00
22ebf35382 arg checks for projects and objects 2024-10-06 17:36:34 +01:00
2398ef5cf9 Use webarg guarding on more endpoints 2024-10-06 13:42:22 +01:00
c0a5f32060 Added webarg checks for account endpoints 2024-10-06 11:28:09 +01:00
849ff0a1e9 Add API Sentry 2024-10-06 10:35:28 +01:00
f3b3ce3d57 Add API linter 2024-10-06 10:30:49 +01:00
032e737ab9 Add web Sentry 2024-10-06 10:17:54 +01:00
1428c83050 Linted web code 2024-10-05 21:25:50 +01:00
6ad9105c82 Add lint tool 2024-10-05 19:40:50 +01:00
c060a6fc41 Additional package changes 2024-10-05 19:37:33 +01:00
48db95ff6e Removing some web deps 2024-10-05 19:33:45 +01:00
ddb723ab88 Update web deps 2024-10-05 19:26:21 +01:00
933e601572 Update all API deps 2024-10-05 19:18:15 +01:00
588 changed files with 11910 additions and 8657 deletions

View File

@ -1,4 +1,4 @@
pipeline: steps:
buildweb: buildweb:
group: build group: build
image: node image: node
@ -7,6 +7,7 @@ pipeline:
environment: environment:
- VITE_API_URL=https://api.treadl.com - VITE_API_URL=https://api.treadl.com
- VITE_IMAGINARY_URL=https://images.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_SOURCE_REPO_URL=https://git.wilw.dev/wilw/treadl
- VITE_PATREON_URL=https://www.patreon.com/treadl - VITE_PATREON_URL=https://www.patreon.com/treadl
- VITE_KOFI_URL=https://ko-fi.com/wilw88 - VITE_KOFI_URL=https://ko-fi.com/wilw88
@ -44,4 +45,5 @@ pipeline:
- s3cmd -c /root/.s3cfg sync --no-mime-magic --guess-mime-type dist/* s3://treadl.com - 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' - 'curl -X POST -H "AccessKey: $BUNNY_KEY" https://api.bunny.net/pullzone/782753/purgeCache'
branches: main when:
branch: main

View File

@ -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 bson.objectid import ObjectId
from util import database, mail, util 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 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: def register(username, email, password, how_find_us):
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) if not username or len(username) < 4 or not email or len(email) < 6:
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']}}) raise util.errors.BadRequest("Your username or email is too short or invalid.")
mail.send({ username = username.lower()
'to': os.environ.get('ADMIN_EMAIL'), email = email.lower()
'subject': '{} signup'.format(os.environ.get('APP_NAME')), if not re.match("^[a-z0-9_]+$", username):
'text': 'A new user signed up with username {0} and email {1}, discovered from {2}'.format(username, email, how_find_us) raise util.errors.BadRequest(
}) "Usernames can only contain letters, numbers, and underscores"
mail.send({ )
'to': email, if not password or len(password) < MIN_PASSWORD_LENGTH:
'subject': 'Welcome to {}!'.format(os.environ.get('APP_NAME')), raise util.errors.BadRequest(
'text': '''Dear {0}, "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. 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, Best wishes,
The {3} Team The {3} Team
'''.format( """.format(
username, username,
os.environ.get('APP_URL'), os.environ.get("APP_URL"),
os.environ.get('CONTACT_EMAIL'), os.environ.get("CONTACT_EMAIL"),
os.environ.get('APP_NAME'), os.environ.get("APP_NAME"),
)}) ),
return {'token': generate_access_token(result.inserted_id)} }
except Exception as e: )
print(e) return {"token": generate_access_token(result.inserted_id)}
raise util.errors.BadRequest('Unable to register your account. Please try again later') except Exception as e:
print(e)
raise util.errors.BadRequest(
"Unable to register your account. Please try again later"
)
def login(email, password): def login(email, password):
db = database.get_db() db = database.get_db()
user = db.users.find_one({'$or': [{'username': email.lower()}, {'email': email.lower()}]}) user = db.users.find_one(
try: {"$or": [{"username": email.lower()}, {"email": email.lower()}]}
if user and bcrypt.checkpw(password.encode("utf-8"), user['password']): )
return {'token': generate_access_token(user['_id'])} try:
else: if user and bcrypt.checkpw(password.encode("utf-8"), user["password"]):
raise util.errors.BadRequest('Your username or password is incorrect.') return {"token": generate_access_token(user["_id"])}
except Exception as e: else:
raise util.errors.BadRequest('Your username or password is incorrect.') 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): def logout(user):
db = database.get_db() db = database.get_db()
db.users.update_one({'_id': user['_id']}, {'$pull': {'tokens.login': user['currentToken']}}) db.users.update_one(
return {'loggedOut': True} {"_id": user["_id"]}, {"$pull": {"tokens.login": user["currentToken"]}}
)
return {"loggedOut": True}
def update_email(user, data): def update_email(user, data):
if not data: raise util.errors.BadRequest('Invalid request') if not data:
if 'email' not in data: raise util.errors.BadRequest('Invalid request') raise util.errors.BadRequest("Invalid request")
if len(data['email']) < 4: raise util.errors.BadRequest('New email is too short') if "email" not in data:
db = database.get_db() raise util.errors.BadRequest("Invalid request")
db.users.update_one({'_id': user['_id']}, {'$set': {'email': data['email']}}) if len(data["email"]) < 4:
mail.send({ raise util.errors.BadRequest("New email is too short")
'to': user['email'], db = database.get_db()
'subject': 'Your email address has changed on {}'.format(os.environ.get('APP_NAME')), db.users.update_one({"_id": user["_id"]}, {"$set": {"email": data["email"]}})
'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( mail.send(
user['username'], {
data['email'], "to": user["email"],
os.environ.get('APP_NAME'), "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(
mail.send({ {
'to': data['email'], "to": data["email"],
'subject': 'Your email address has changed on {}'.format(os.environ.get('APP_NAME')), "subject": "Your email address has changed on {}".format(
'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( os.environ.get("APP_NAME")
user['username'], ),
data['email'], "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(
os.environ.get('APP_NAME'), user["username"],
data["email"],
os.environ.get("APP_NAME"),
),
}
) )
}) return {"email": data["email"]}
return {'email': data['email']}
def update_password(user, data): def update_password(user, data):
if not data: raise util.errors.BadRequest('Invalid request') if not data:
if 'newPassword' not in data: raise util.errors.BadRequest('Invalid request') 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 "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() db = database.get_db()
if 'currentPassword' in data: if "currentPassword" in data:
if not user: raise util.errors.BadRequest('User context is required') if not user:
if not bcrypt.checkpw(data['currentPassword'].encode('utf-8'), user['password']): raise util.errors.BadRequest("User context is required")
raise util.errors.BadRequest('Incorrect password') if not bcrypt.checkpw(
elif 'token' in data: data["currentPassword"].encode("utf-8"), user["password"]
try: ):
id = jwt.decode(data['token'], jwt_secret, algorithms='HS256')['sub'] raise util.errors.BadRequest("Incorrect password")
user = db.users.find_one({'_id': ObjectId(id), 'tokens.passwordReset': data['token']}) elif "token" in data:
if not user: raise Exception try:
except Exception as e: id = jwt.decode(data["token"], jwt_secret, algorithms="HS256")["sub"]
raise util.errors.BadRequest('There was a problem updating your password. Your token may be invalid or out of date') user = db.users.find_one(
else: {"_id": ObjectId(id), "tokens.passwordReset": data["token"]}
raise util.errors.BadRequest('Current password or reset token is required') )
if not user: raise util.errors.BadRequest('Unable to change your password') 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()) hashed_password = bcrypt.hashpw(
db.users.update_one({'_id': user['_id']}, {'$set': {'password': hashed_password}, '$unset': {'tokens.passwordReset': ''}}) data["newPassword"].encode("utf-8"), bcrypt.gensalt()
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'),
) )
}) db.users.update_one(
return {'passwordUpdated': True} {"_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): def delete(user, password):
if not password or not bcrypt.checkpw(password.encode('utf-8'), user['password']): if not password or not bcrypt.checkpw(password.encode("utf-8"), user["password"]):
raise util.errors.BadRequest('Incorrect password') raise util.errors.BadRequest("Incorrect password")
db = database.get_db() db = database.get_db()
for project in db.projects.find({'user': user['_id']}): for project in db.projects.find({"user": user["_id"]}):
db.objects.delete_many({'project': project['_id']}) db.objects.delete_many({"project": project["_id"]})
db.projects.delete_one({'_id': project['_id']}) db.projects.delete_one({"_id": project["_id"]})
db.comments.delete_many({'user': user['_id']}) db.comments.delete_many({"user": user["_id"]})
db.users.update_many({'following.user': user['_id']}, {'$pull': {'following': {'user': user['_id']}}}) db.users.update_many(
db.users.delete_one({'_id': user['_id']}) {"following.user": user["_id"]}, {"$pull": {"following": {"user": user["_id"]}}}
return {'deletedUser': user['_id']} )
db.users.delete_one({"_id": user["_id"]})
return {"deletedUser": user["_id"]}
def generate_access_token(user_id): def generate_access_token(user_id):
payload = { payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=30), "exp": datetime.datetime.utcnow() + datetime.timedelta(days=30),
'iat': datetime.datetime.utcnow(), "iat": datetime.datetime.utcnow(),
'sub': str(user_id) "sub": str(user_id),
} }
token = jwt.encode(payload, jwt_secret, algorithm='HS256') token = jwt.encode(payload, jwt_secret, algorithm="HS256")
db = database.get_db() db = database.get_db()
db.users.update_one({'_id': user_id}, {'$addToSet': {'tokens.login': token}}) db.users.update_one({"_id": user_id}, {"$addToSet": {"tokens.login": token}})
return token return token
def get_user_context(token): def get_user_context(token):
if not token: return None if not token:
try: return None
payload = jwt.decode(token, jwt_secret, algorithms='HS256') try:
id = payload['sub'] payload = jwt.decode(token, jwt_secret, algorithms="HS256")
if id: id = payload["sub"]
db = database.get_db() if id:
user = db.users.find_one({'_id': ObjectId(id), 'tokens.login': token}) db = database.get_db()
db.users.update_one({'_id': user['_id']}, {'$set': {'lastSeenAt': datetime.datetime.now()}}) user = db.users.find_one({"_id": ObjectId(id), "tokens.login": token})
user['currentToken'] = token db.users.update_one(
return user {"_id": user["_id"]}, {"$set": {"lastSeenAt": datetime.datetime.now()}}
except Exception as e: )
print(e) user["currentToken"] = token
return None return user
except Exception as e:
print(e)
return None
def reset_password(data): def reset_password(data):
if not data or not 'email' in data: raise util.errors.BadRequest('Invalid request') if not data or "email" not in data:
if len(data['email']) < 5: raise util.errors.BadRequest('Your email is too short') raise util.errors.BadRequest("Invalid request")
db = database.get_db() if len(data["email"]) < 5:
user = db.users.find_one({'email': data['email'].lower()}) raise util.errors.BadRequest("Your email is too short")
if user: db = database.get_db()
payload = { user = db.users.find_one({"email": data["email"].lower()})
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1), if user:
'iat': datetime.datetime.utcnow(), payload = {
'sub': str(user['_id']) "exp": datetime.datetime.utcnow() + datetime.timedelta(days=1),
} "iat": datetime.datetime.utcnow(),
token = jwt.encode(payload, jwt_secret, algorithm='HS256') "sub": str(user["_id"]),
mail.send({ }
'to_user': user, token = jwt.encode(payload, jwt_secret, algorithm="HS256")
'subject': 'Reset your password', mail.send(
'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'], "to_user": user,
'{}/password/reset?token={}'.format(os.environ.get('APP_URL'), token), "subject": "Reset your password",
os.environ.get('APP_NAME'), "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(
db.users.update_one({'_id': user['_id']}, {'$set': {'tokens.passwordReset': token}}) os.environ.get("APP_URL"), token
return {'passwordResetEmailSent': True} ),
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): def update_push_token(user, data):
if not data or 'pushToken' not in data: raise util.errors.BadRequest('Push token is required') if not data or "pushToken" not in data:
db = database.get_db() raise util.errors.BadRequest("Push token is required")
db.users.update_one({'_id': user['_id']}, {'$set': {'pushToken': data['pushToken']}}) db = database.get_db()
return {'addedPushToken': data['pushToken']} db.users.update_one(
{"_id": user["_id"]}, {"$set": {"pushToken": data["pushToken"]}}
)
return {"addedPushToken": data["pushToken"]}

View File

@ -1,165 +1,190 @@
import os, re import os
import re
from util import database, util from util import database, util
from api import uploads from api import uploads
DOMAIN = os.environ.get('APP_DOMAIN') DOMAIN = os.environ.get("APP_DOMAIN")
def webfinger(resource): def webfinger(resource):
if not resource: raise util.errors.BadRequest('Resource required') if not resource:
resource = resource.lower() raise util.errors.BadRequest("Resource required")
exp = re.compile('acct:([a-z0-9_-]+)@([a-z0-9_\-\.]+)', re.IGNORECASE) resource = resource.lower()
matches = exp.findall(resource) exp = re.compile("acct:([a-z0-9_-]+)@([a-z0-9_\-\.]+)", re.IGNORECASE)
if not matches or not matches[0]: raise util.errors.BadRequest('Resource invalid') matches = exp.findall(resource)
username, host = matches[0] if not matches or not matches[0]:
if not username or not host: raise util.errors.BadRequest('Resource invalid') raise util.errors.BadRequest("Resource invalid")
if host != DOMAIN: raise util.errors.NotFound('Host unknown') 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() db = database.get_db()
user = db.users.find_one({'username': username}) user = db.users.find_one({"username": username})
if not user: raise util.errors.NotFound('User unknown') 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): def user(username):
if not username: raise util.errors.BadRequest('Username required') if not username:
username = username.lower() raise util.errors.BadRequest("Username required")
db = database.get_db() username = username.lower()
user = db.users.find_one({'username': username}) db = database.get_db()
if not user: raise util.errors.NotFound('User unknown') user = db.users.find_one({"username": username})
avatar_url = user.get('avatar') and uploads.get_presigned_url('users/{0}/{1}'.format(user['_id'], user['avatar'])) 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 pub_key = None
if user.get('services', {}).get('activityPub', {}).get('publicKey'): if user.get("services", {}).get("activityPub", {}).get("publicKey"):
pub_key = user['services']['activityPub']['publicKey'] pub_key = user["services"]["activityPub"]["publicKey"]
else: else:
priv_key, pub_key = util.generate_rsa_keypair() priv_key, pub_key = util.generate_rsa_keypair()
db.users.update_one({'_id': user['_id']}, {'$set': { db.users.update_one(
'services.activityPub.publicKey': pub_key, {"_id": user["_id"]},
'services.activityPub.privateKey': priv_key, {
}}) "$set": {
"services.activityPub.publicKey": pub_key,
"services.activityPub.privateKey": priv_key,
}
},
)
resp = { resp = {
"@context": [ "@context": [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1", "https://w3id.org/security/v1",
], ],
"id": "https://{}/u/{}".format(DOMAIN, username), "id": "https://{}/u/{}".format(DOMAIN, username),
"type": "Person", "type": "Person",
#"following": "https://fosstodon.org/users/wilw/following", # "following": "https://fosstodon.org/users/wilw/following",
#"followers": "https://fosstodon.org/users/wilw/followers", # "followers": "https://fosstodon.org/users/wilw/followers",
"inbox": "https://{}/inbox".format(DOMAIN), "inbox": "https://{}/inbox".format(DOMAIN),
"outbox": "https://{}/u/{}/outbox".format(DOMAIN, username), "outbox": "https://{}/u/{}/outbox".format(DOMAIN, username),
"preferredUsername": username, "preferredUsername": username,
"name": username, "name": username,
"summary": user.get('bio', ''), "summary": user.get("bio", ""),
"url": "https://{}/{}".format(DOMAIN, username), "url": "https://{}/{}".format(DOMAIN, username),
"discoverable": True, "discoverable": True,
"published": "2021-01-27T00:00:00Z", "published": "2021-01-27T00:00:00Z",
"publicKey": { "publicKey": {
"id": "https://{}/u/{}#main-key".format(DOMAIN, username), "id": "https://{}/u/{}#main-key".format(DOMAIN, username),
"owner": "https://{}/u/{}".format(DOMAIN, username), "owner": "https://{}/u/{}".format(DOMAIN, username),
"publicKeyPem": pub_key.decode('utf-8') "publicKeyPem": pub_key.decode("utf-8"),
}, },
"attachment": [], "attachment": [],
"endpoints": { "endpoints": {"sharedInbox": "https://{}/inbox".format(DOMAIN)},
"sharedInbox": "https://{}/inbox".format(DOMAIN) "icon": {"type": "Image", "mediaType": "image/jpeg", "url": avatar_url},
}, "image": {"type": "Image", "mediaType": "image/jpeg", "url": avatar_url},
"icon": {
"type": "Image",
"mediaType": "image/jpeg",
"url": avatar_url
},
"image": {
"type": "Image",
"mediaType": "image/jpeg",
"url": avatar_url
} }
}
if user.get('website'): if user.get("website"):
resp['attachment'].append({ resp["attachment"].append(
"type": "PropertyValue", {
"name": "Website", "type": "PropertyValue",
"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']) "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): def outbox(username, page, min_id, max_id):
if not username: raise util.errors.BadRequest('Username required') if not username:
username = username.lower() raise util.errors.BadRequest("Username required")
db = database.get_db() username = username.lower()
user = db.users.find_one({'username': username}) db = database.get_db()
if not user: raise util.errors.NotFound('User unknown') user = db.users.find_one({"username": username})
if not user:
raise util.errors.NotFound("User unknown")
if not page or page != 'true': if not page or page != "true":
return { return {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://{}/u/{}/outbox".format(DOMAIN, username), "id": "https://{}/u/{}/outbox".format(DOMAIN, username),
"type": "OrderedCollection", "type": "OrderedCollection",
"first": "https://{}/u/{}/outbox?page=true".format(DOMAIN, username) "first": "https://{}/u/{}/outbox?page=true".format(DOMAIN, username),
} }
if page == 'true': if page == "true":
min_string = '&min_id={}'.format(min_id) if min_id else '' min_string = "&min_id={}".format(min_id) if min_id else ""
max_string = '&max_id={}'.format(max_id) if max_id else '' max_string = "&max_id={}".format(max_id) if max_id else ""
ret = { ret = {
"id": "https://{}/u/{}/outbox?page=true{}{}".format(DOMAIN, username, min_string, max_string), "id": "https://{}/u/{}/outbox?page=true{}{}".format(
"type": "OrderedCollectionPage", DOMAIN, username, min_string, max_string
#"next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true", ),
#"prev": "https://example.org/users/whatever/outbox?min_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true", "type": "OrderedCollectionPage",
"partOf": "https://{}/u/{}/outbox".format(DOMAIN, username), # "next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
"orderedItems": [] # "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 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

View File

@ -1,268 +1,419 @@
import datetime, re, os import datetime
import re
import os
import pymongo import pymongo
from bson.objectid import ObjectId from bson.objectid import ObjectId
from util import database, util, mail, push from util import database, util, mail, push
from api import uploads from api import uploads
APP_NAME = os.environ.get('APP_NAME') APP_NAME = os.environ.get("APP_NAME")
APP_URL = os.environ.get('APP_URL') APP_URL = os.environ.get("APP_URL")
def create(user, data): def create(user, data):
if not data: raise util.errors.BadRequest('Invalid request') if not data:
if len(data.get('name')) < 3: raise util.errors.BadRequest('A longer name is required') raise util.errors.BadRequest("Invalid request")
db = database.get_db() 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): def get(user):
db = database.get_db() db = database.get_db()
groups = list(db.groups.find({'_id': {'$in': user.get('groups', [])}})) groups = list(db.groups.find({"_id": {"$in": user.get("groups", [])}}))
return {'groups': groups} return {"groups": groups}
def get_one(user, id): def get_one(user, id):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
group = db.groups.find_one({'_id': id}) group = db.groups.find_one({"_id": id})
if not group: raise util.errors.NotFound('Group not found') if not group:
group['adminUsers'] = list(db.users.find({'_id': {'$in': group.get('admins', [])}}, {'username': 1, 'avatar': 1})) raise util.errors.NotFound("Group not found")
for u in group['adminUsers']: group["adminUsers"] = list(
if 'avatar' in u: db.users.find(
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar'])) {"_id": {"$in": group.get("admins", [])}}, {"username": 1, "avatar": 1}
return group )
)
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): def update(user, id, update):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
group = db.groups.find_one({'_id': id}, {'admins': 1}) group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You\'re not a group admin') raise util.errors.NotFound("Group not found")
allowed_keys = ['name', 'description', 'closed'] if user["_id"] not in group.get("admins", []):
updater = util.build_updater(update, allowed_keys) raise util.errors.Forbidden("You're not a group admin")
if updater: db.groups.update_one({'_id': id}, updater) allowed_keys = ["name", "description", "closed"]
return get_one(user, id) 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): def delete(user, id):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
group = db.groups.find_one({'_id': id}, {'admins': 1}) group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You\'re not a group admin') raise util.errors.NotFound("Group not found")
db.groups.delete_one({'_id': id}) if user["_id"] not in group.get("admins", []):
db.groupEntries.delete_many({'group': id}) raise util.errors.Forbidden("You're not a group admin")
db.users.update_many({'groups': id}, {'$pull': {'groups': id}}) db.groups.delete_one({"_id": id})
return {'deletedGroup': 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): def create_entry(user, id, data):
if not data or 'content' not in data: raise util.errors.BadRequest('Invalid request') if not data or "content" not in data:
db = database.get_db() raise util.errors.BadRequest("Invalid request")
id = ObjectId(id) db = database.get_db()
group = db.groups.find_one({'_id': id}, {'admins': 1, 'name': 1}) id = ObjectId(id)
if not group: raise util.errors.NotFound('Group not found') group = db.groups.find_one({"_id": id}, {"admins": 1, "name": 1})
if group['_id'] not in user.get('groups', []): raise util.errors.Forbidden('You must be a member to write in the feed') if not group:
entry = { raise util.errors.NotFound("Group not found")
'createdAt': datetime.datetime.now(), if group["_id"] not in user.get("groups", []):
'group': id, raise util.errors.Forbidden("You must be a member to write in the feed")
'user': user['_id'], entry = {
'content': data['content'], "createdAt": datetime.datetime.now(),
} "group": id,
if 'attachments' in data: "user": user["_id"],
entry['attachments'] = data['attachments'] "content": data["content"],
for attachment in entry['attachments']: }
if re.search(r'(.jpg)|(.png)|(.jpeg)|(.gif)$', attachment['storedName'].lower()): if "attachments" in data:
attachment['isImage'] = True entry["attachments"] = data["attachments"]
if attachment['type'] == 'file': for attachment in entry["attachments"]:
attachment['url'] = uploads.get_presigned_url('groups/{0}/{1}'.format(id, attachment['storedName'])) 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) result = db.groupEntries.insert_one(entry)
entry['_id'] = result.inserted_id entry["_id"] = result.inserted_id
entry['authorUser'] = {'_id': user['_id'], 'username': user['username'], 'avatar': user.get('avatar')} entry["authorUser"] = {
if 'avatar' in user: "_id": user["_id"],
entry['authorUser']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(user['_id'], user['avatar'])) "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): def get_entries(user, id):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
group = db.groups.find_one({'_id': id}, {'admins': 1}) group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
if id not in user.get('groups', []): raise util.errors.BadRequest('You\'re not a member of this group') raise util.errors.NotFound("Group not found")
entries = list(db.groupEntries.find({'group': id}).sort('createdAt', pymongo.DESCENDING)) if id not in user.get("groups", []):
authors = list(db.users.find({'_id': {'$in': [e['user'] for e in entries]}}, {'username': 1, 'avatar': 1})) raise util.errors.BadRequest("You're not a member of this group")
for entry in entries: entries = list(
if 'attachments' in entry: db.groupEntries.find({"group": id}).sort("createdAt", pymongo.DESCENDING)
for attachment in entry['attachments']: )
attachment['url'] = uploads.get_presigned_url('groups/{0}/{1}'.format(id, attachment['storedName'])) authors = list(
for author in authors: db.users.find(
if entry['user'] == author['_id']: {"_id": {"$in": [e["user"] for e in entries]}}, {"username": 1, "avatar": 1}
entry['authorUser'] = author )
if 'avatar' in author: )
entry['authorUser']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(author['_id'], author['avatar'])) for entry in entries:
return {'entries': 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): def delete_entry(user, id, entry_id):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
entry_id = ObjectId(entry_id) entry_id = ObjectId(entry_id)
group = db.groups.find_one({'_id': id}, {'admins': 1}) group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
entry = db.groupEntries.find_one(entry_id, {'user': 1, 'group': 1}) raise util.errors.NotFound("Group not found")
if not entry or entry['group'] != id: raise util.errors.NotFound('Entry not found') entry = db.groupEntries.find_one(entry_id, {"user": 1, "group": 1})
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') if not entry or entry["group"] != id:
db.groupEntries.delete_one({'$or': [{'_id': entry_id}, {'inReplyTo': entry_id}]}) raise util.errors.NotFound("Entry not found")
return {'deletedEntry': entry_id} 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): def create_entry_reply(user, id, entry_id, data):
if not data or 'content' not in data: raise util.errors.BadRequest('Invalid request') if not data or "content" not in data:
db = database.get_db() raise util.errors.BadRequest("Invalid request")
id = ObjectId(id) db = database.get_db()
entry_id = ObjectId(entry_id) id = ObjectId(id)
group = db.groups.find_one({'_id': id}, {'admins': 1, 'name': 1}) entry_id = ObjectId(entry_id)
if not group: raise util.errors.NotFound('Group not found') group = db.groups.find_one({"_id": id}, {"admins": 1, "name": 1})
entry = db.groupEntries.find_one({'_id': entry_id}) if not group:
if not entry or entry.get('group') != group['_id']: raise util.errors.NotFound('Entry to reply to not found') 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 = db.groupEntries.find_one({"_id": entry_id})
reply = { if not entry or entry.get("group") != group["_id"]:
'createdAt': datetime.datetime.now(), raise util.errors.NotFound("Entry to reply to not found")
'group': id, if group["_id"] not in user.get("groups", []):
'inReplyTo': entry_id, raise util.errors.Forbidden("You must be a member to write in the feed")
'user': user['_id'], reply = {
'content': data['content'], "createdAt": datetime.datetime.now(),
} "group": id,
if 'attachments' in data: "inReplyTo": entry_id,
reply['attachments'] = data['attachments'] "user": user["_id"],
for attachment in reply['attachments']: "content": data["content"],
if re.search(r'(.jpg)|(.png)|(.jpeg)|(.gif)$', attachment['storedName'].lower()): }
attachment['isImage'] = True if "attachments" in data:
if attachment['type'] == 'file': reply["attachments"] = data["attachments"]
attachment['url'] = uploads.get_presigned_url('groups/{0}/{1}'.format(id, attachment['storedName'])) 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): def delete_entry_reply(user, id, entry_id, reply_id):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
entry_id = ObjectId(entry_id) entry_id = ObjectId(entry_id)
reply_id = ObjectId(reply_id) reply_id = ObjectId(reply_id)
group = db.groups.find_one({'_id': id}, {'admins': 1}) group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
entry = db.groupEntries.find_one(entry_id, {'user': 1, 'group': 1}) raise util.errors.NotFound("Group not found")
if not entry or entry['group'] != id: raise util.errors.NotFound('Entry not found') entry = db.groupEntries.find_one(entry_id, {"user": 1, "group": 1})
reply = db.groupEntries.find_one(reply_id) if not entry or entry["group"] != id:
if not reply or reply.get('inReplyTo') != entry_id: raise util.errors.NotFound('Reply not found') raise util.errors.NotFound("Entry 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') reply = db.groupEntries.find_one(reply_id)
db.groupEntries.delete_one({'_id': entry_id}) if not reply or reply.get("inReplyTo") != entry_id:
return {'deletedEntry': 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): def get_members(user, id):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
group = db.groups.find_one({'_id': id}, {'admins': 1}) group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
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') raise util.errors.NotFound("Group not found")
members = list(db.users.find({'groups': id}, {'username': 1, 'avatar': 1, 'bio': 1, 'groups': 1})) if id not in user.get("groups", []) and "root" not in user.get("roles", []):
for m in members: raise util.errors.Forbidden("You need to be a member to see the member list")
if 'avatar' in m: members = list(
m['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(m['_id'], m['avatar'])) db.users.find(
return {'members': members} {"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): def delete_member(user, id, user_id):
id = ObjectId(id) id = ObjectId(id)
user_id = ObjectId(user_id) user_id = ObjectId(user_id)
db = database.get_db() db = database.get_db()
group = db.groups.find_one({'_id': id}, {'admins': 1}) group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
if user_id != user['_id'] and user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You can\'t remove this user') raise util.errors.NotFound("Group not found")
if user_id in group.get('admins', []) and len(group['admins']) == 1: if user_id != user["_id"] and user["_id"] not in group.get("admins", []):
raise util.errors.Forbidden('There needs to be at least one admin in this group') raise util.errors.Forbidden("You can't remove this user")
db.users.update_one({'_id': user_id}, {'$pull': {'groups': id, 'subscriptions.email': 'groupFeed-' + str(id)}}) if user_id in group.get("admins", []) and len(group["admins"]) == 1:
db.groups.update_one({'_id': id}, {'$pull': {'admins': user_id}}) raise util.errors.Forbidden(
return {'deletedMember': user_id} "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): def get_projects(user, id):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
group = db.groups.find_one({'_id': id}, {'admins': 1}) group = db.groups.find_one({"_id": id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
if id not in user.get('groups', []): raise util.errors.Forbidden('You need to be a member to see the project list') raise util.errors.NotFound("Group not found")
projects = list(db.projects.find({'groupVisibility': id}, {'name': 1, 'path': 1, 'user': 1, 'description': 1, 'visibility': 1})) if id not in user.get("groups", []):
authors = list(db.users.find({'groups': id, '_id': {'$in': list(map(lambda p: p['user'], projects))}}, {'username': 1, 'avatar': 1, 'bio': 1})) raise util.errors.Forbidden("You need to be a member to see the project list")
for a in authors: projects = list(
if 'avatar' in a: db.projects.find(
a['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(a['_id'], a['avatar'])) {"groupVisibility": id},
for project in projects: {"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: for a in authors:
if project['user'] == a['_id']: if "avatar" in a:
project['owner'] = a a["avatarUrl"] = uploads.get_presigned_url(
project['fullName'] = a['username'] + '/' + project['path'] "users/{0}/{1}".format(a["_id"], a["avatar"])
break )
return {'projects': projects} 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}

View File

@ -1,171 +1,252 @@
import re, datetime, os import datetime
import pymongo import os
from bson.objectid import ObjectId from bson.objectid import ObjectId
from util import database, util, mail from util import database, util, mail
from api import uploads, groups from api import uploads, groups
APP_NAME = os.environ.get('APP_NAME') APP_NAME = os.environ.get("APP_NAME")
APP_URL = os.environ.get('APP_URL') APP_URL = os.environ.get("APP_URL")
def get(user): def get(user):
db = database.get_db() db = database.get_db()
admin_groups = list(db.groups.find({'admins': user['_id']})) 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))}}]})) invites = list(
inviters = list(db.users.find({'_id': {'$in': [i['user'] for i in invites]}}, {'username': 1, 'avatar': 1})) db.invitations.find(
for invite in invites: {
invite['recipient'] = user['_id'] "$or": [
if invite['type'] in ['group', 'groupJoinRequest']: invite['group'] = db.groups.find_one({'_id': invite['typeId']}, {'name': 1}) {"recipient": user["_id"]},
for u in inviters: {
if u['_id'] == invite['user']: "recipientGroup": {
if 'avatar' in u: "$in": list(map(lambda g: g["_id"], admin_groups))
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}) inviters = list(
for u in recipients: db.users.find(
if u['_id'] == invite.get('recipient'): {"_id": {"$in": [i["user"] for i in invites]}}, {"username": 1, "avatar": 1}
if 'avatar' in u: )
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar'])) )
invite['invitedBy'] = u for invite in invites:
break invite["recipient"] = user["_id"]
return {'invitations': invites, 'sentInvitations': sent_invites} 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): def accept(user, id):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
invite = db.invitations.find_one({'_id': id}) invite = db.invitations.find_one({"_id": id})
if not invite: raise util.errors.NotFound('Invitation not found') if not invite:
if invite['type'] == 'group': raise util.errors.NotFound("Invitation not found")
if invite['recipient'] != user['_id']: raise util.errors.Forbidden('This invitation is not yours to accept') if invite["type"] == "group":
group = db.groups.find_one({'_id': invite['typeId']}, {'name': 1}) if invite["recipient"] != user["_id"]:
if not group: raise util.errors.Forbidden("This invitation is not yours to accept")
db.invitations.delete_one({'_id': id}) group = db.groups.find_one({"_id": invite["typeId"]}, {"name": 1})
return {'acceptedInvitation': id} if not group:
groups.create_member(user, group['_id'], user['_id'], invited = True) db.invitations.delete_one({"_id": id})
db.invitations.delete_one({'_id': id}) return {"acceptedInvitation": id}
return {'acceptedInvitation': id, 'group': group} groups.create_member(user, group["_id"], user["_id"], invited=True)
if invite['type'] == 'groupJoinRequest': db.invitations.delete_one({"_id": id})
group = db.groups.find_one({'_id': invite['typeId']}) return {"acceptedInvitation": id, "group": group}
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') if invite["type"] == "groupJoinRequest":
requester = db.users.find_one({'_id': invite['user']}) group = db.groups.find_one({"_id": invite["typeId"]})
if not group or not requester: if user["_id"] not in group.get("admins", []):
db.invitations.delete_one({'_id': id}) raise util.errors.Forbidden(
return {'acceptedInvitation': id} "You need to be an admin of this group to accept this request"
groups.create_member(requester, group['_id'], requester['_id'], invited = True) )
db.invitations.delete_one({'_id': id}) requester = db.users.find_one({"_id": invite["user"]})
return {'acceptedInvitation': id, 'group': group} 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): def delete(user, id):
db = database.get_db() db = database.get_db()
id = ObjectId(id) id = ObjectId(id)
invite = db.invitations.find_one({'_id': id}) invite = db.invitations.find_one({"_id": id})
if not invite: raise util.errors.NotFound('Invitation not found') if not invite:
if invite['type'] == 'group': raise util.errors.NotFound("Invitation not found")
if invite['recipient'] != user['_id']: raise util.errors.Forbidden('This invitation is not yours to decline') if invite["type"] == "group":
if invite['type'] == 'groupJoinRequest': if invite["recipient"] != user["_id"]:
group = db.groups.find_one({'_id': invite['typeId']}) raise util.errors.Forbidden("This invitation is not yours to decline")
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') if invite["type"] == "groupJoinRequest":
db.invitations.delete_one({'_id': id}) group = db.groups.find_one({"_id": invite["typeId"]})
return {'deletedInvitation': id} 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): def create_group_invitation(user, group_id, data):
if not data or 'user' not in data: raise util.errors.BadRequest('Invalid request') if not data or "user" not in data:
db = database.get_db() raise util.errors.BadRequest("Invalid request")
recipient_id = ObjectId(data['user']) db = database.get_db()
group_id = ObjectId(group_id) recipient_id = ObjectId(data["user"])
group = db.groups.find_one({'_id': group_id}, {'admins': 1, 'name': 1}) group_id = ObjectId(group_id)
if not group: raise util.errors.NotFound('Group not found') group = db.groups.find_one({"_id": group_id}, {"admins": 1, "name": 1})
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You need to be a group admin to invite users') if not group:
recipient = db.users.find_one({'_id': recipient_id}, {'groups': 1, 'username': 1, 'email': 1, 'subscriptions': 1}) raise util.errors.NotFound("Group not found")
if not recipient: raise util.errors.NotFound('User not found') if user["_id"] not in group.get("admins", []):
if group_id in recipient.get('groups', []): raise util.errors.BadRequest('This user is already in this group') raise util.errors.Forbidden("You need to be a group admin to invite users")
if db.invitations.find_one({'recipient': recipient_id, 'typeId': group_id, 'type': 'group'}): recipient = db.users.find_one(
raise util.errors.BadRequest('This user has already been invited to this group') {"_id": recipient_id},
invite = { {"groups": 1, "username": 1, "email": 1, "subscriptions": 1},
'createdAt': datetime.datetime.now(), )
'user': user['_id'], if not recipient:
'recipient': recipient_id, raise util.errors.NotFound("User not found")
'type': 'group', if group_id in recipient.get("groups", []):
'typeId': group_id raise util.errors.BadRequest("This user is already in this group")
} if db.invitations.find_one(
result = db.invitations.insert_one(invite) {"recipient": recipient_id, "typeId": group_id, "type": "group"}
if 'groups.invited' in recipient.get('subscriptions', {}).get('email', []): ):
mail.send({ raise util.errors.BadRequest("This user has already been invited to this group")
'to_user': recipient, invite = {
'subject': 'You\'ve been invited to a group on {}!'.format(APP_NAME), "createdAt": datetime.datetime.now(),
'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( "user": user["_id"],
recipient['username'], "recipient": recipient_id,
group['name'], "type": "group",
APP_URL, "typeId": group_id,
APP_NAME, }
) result = db.invitations.insert_one(invite)
}) if "groups.invited" in recipient.get("subscriptions", {}).get("email", []):
invite['_id'] = result.inserted_id mail.send(
return invite {
"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): def create_group_request(user, group_id):
db = database.get_db() db = database.get_db()
group_id = ObjectId(group_id) group_id = ObjectId(group_id)
group = db.groups.find_one({'_id': group_id}, {'admins': 1, 'name': 1}) group = db.groups.find_one({"_id": group_id}, {"admins": 1, "name": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
if group_id in user.get('groups', []): raise util.errors.BadRequest('You are already a member of this group') raise util.errors.NotFound("Group not found")
admin = db.users.find_one({'_id': {'$in': group.get('admins', [])}}, {'groups': 1, 'username': 1, 'email': 1, 'subscriptions': 1}) if group_id in user.get("groups", []):
if not admin: raise util.errors.NotFound('No users can approve you to join this group') raise util.errors.BadRequest("You are already a member of this group")
if db.invitations.find_one({'recipient': user['_id'], 'typeId': group_id, 'type': 'group'}): admin = db.users.find_one(
raise util.errors.BadRequest('You have already been invited to this group') {"_id": {"$in": group.get("admins", [])}},
if db.invitations.find_one({'user': user['_id'], 'typeId': group_id, 'type': 'groupJoinRequest'}): {"groups": 1, "username": 1, "email": 1, "subscriptions": 1},
raise util.errors.BadRequest('You have already requested access to this group') )
invite = { if not admin:
'createdAt': datetime.datetime.now(), raise util.errors.NotFound("No users can approve you to join this group")
'user': user['_id'], if db.invitations.find_one(
'recipientGroup': group['_id'], {"recipient": user["_id"], "typeId": group_id, "type": "group"}
'type': 'groupJoinRequest', ):
'typeId': group_id raise util.errors.BadRequest("You have already been invited to this group")
} if db.invitations.find_one(
result = db.invitations.insert_one(invite) {"user": user["_id"], "typeId": group_id, "type": "groupJoinRequest"}
if 'groups.joinRequested' in admin.get('subscriptions', {}).get('email', []): ):
mail.send({ raise util.errors.BadRequest("You have already requested access to this group")
'to_user': admin, invite = {
'subject': 'Someone wants to join your group', "createdAt": datetime.datetime.now(),
'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( "user": user["_id"],
admin['username'], "recipientGroup": group["_id"],
user['username'], "type": "groupJoinRequest",
group['name'], "typeId": group_id,
APP_URL, }
APP_NAME, result = db.invitations.insert_one(invite)
) if "groups.joinRequested" in admin.get("subscriptions", {}).get("email", []):
}) mail.send(
invite['_id'] = result.inserted_id {
return invite "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): def get_group_invitations(user, id):
db = database.get_db() db = database.get_db()
group_id = ObjectId(id) group_id = ObjectId(id)
group = db.groups.find_one({'_id': group_id}, {'admins': 1}) group = db.groups.find_one({"_id": group_id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You need to be a group admin to see invitations') raise util.errors.NotFound("Group not found")
invites = list(db.invitations.find({'type': 'group', 'typeId': group_id})) if user["_id"] not in group.get("admins", []):
recipients = list(db.users.find({'_id': {'$in': [i['recipient'] for i in invites]}}, {'username': 1, 'avatar': 1})) raise util.errors.Forbidden("You need to be a group admin to see invitations")
for invite in invites: invites = list(db.invitations.find({"type": "group", "typeId": group_id}))
for recipient in recipients: recipients = list(
if invite['recipient'] == recipient['_id']: db.users.find(
if 'avatar' in recipient: {"_id": {"$in": [i["recipient"] for i in invites]}},
recipient['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(recipient['_id'], recipient['avatar'])) {"username": 1, "avatar": 1},
invite['recipientUser'] = recipient )
break )
return {'invitations': invites} 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): def delete_group_invitation(user, id, invite_id):
db = database.get_db() db = database.get_db()
group_id = ObjectId(id) group_id = ObjectId(id)
invite_id = ObjectId(invite_id) invite_id = ObjectId(invite_id)
group = db.groups.find_one({'_id': group_id}, {'admins': 1}) group = db.groups.find_one({"_id": group_id}, {"admins": 1})
if not group: raise util.errors.NotFound('Group not found') if not group:
if user['_id'] not in group.get('admins', []): raise util.errors.Forbidden('You need to be a group admin to see invitations') raise util.errors.NotFound("Group not found")
invite = db.invitations.find_one({'_id': invite_id}) if user["_id"] not in group.get("admins", []):
if not invite or invite['typeId'] != group_id: raise util.errors.NotFound('This invite could not be found') raise util.errors.Forbidden("You need to be a group admin to see invitations")
db.invitations.delete_one({'_id': invite_id}) invite = db.invitations.find_one({"_id": invite_id})
return {'deletedInvite': 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}

View File

@ -1,199 +1,256 @@
import datetime, base64, os import datetime
import base64
import os
from bson.objectid import ObjectId from bson.objectid import ObjectId
import requests import requests
from util import database, wif, util, mail from util import database, wif, util, mail
from api import uploads from api import uploads
APP_NAME = os.environ.get('APP_NAME') APP_NAME = os.environ.get("APP_NAME")
APP_URL = os.environ.get('APP_URL') APP_URL = os.environ.get("APP_URL")
def delete(user, id): def delete(user, id):
db = database.get_db() db = database.get_db()
obj = db.objects.find_one(ObjectId(id), {'project': 1}) obj = db.objects.find_one(ObjectId(id), {"project": 1})
if not obj: if not obj:
raise util.errors.NotFound('Object not found') raise util.errors.NotFound("Object not found")
project = db.projects.find_one(obj.get('project'), {'user': 1}) project = db.projects.find_one(obj.get("project"), {"user": 1})
if not project: if not project:
raise util.errors.NotFound('Project not found') raise util.errors.NotFound("Project not found")
if not util.can_edit_project(user, project): if not util.can_edit_project(user, project):
raise util.errors.Forbidden('Forbidden', 403) raise util.errors.Forbidden("Forbidden", 403)
db.objects.delete_one({'_id': ObjectId(id)}) db.objects.delete_one({"_id": ObjectId(id)})
return {'deletedObject': id} return {"deletedObject": id}
def get(user, id): def get(user, id):
db = database.get_db() db = database.get_db()
obj = db.objects.find_one({'_id': ObjectId(id)}) obj = db.objects.find_one({"_id": ObjectId(id)})
if not obj: raise util.errors.NotFound('Object not found') if not obj:
proj = db.projects.find_one({'_id': obj['project']}) raise util.errors.NotFound("Object not found")
if not proj: raise util.errors.NotFound('Project not found') proj = db.projects.find_one({"_id": obj["project"]})
is_owner = user and (user.get('_id') == proj['user']) if not proj:
if not is_owner and proj['visibility'] != 'public': raise util.errors.NotFound("Project not found")
raise util.errors.BadRequest('Forbidden') is_owner = user and (user.get("_id") == proj["user"])
owner = db.users.find_one({'_id': proj['user']}, {'username': 1, 'avatar': 1}) if not is_owner and proj["visibility"] != "public":
if obj['type'] == 'file' and 'storedName' in obj: raise util.errors.BadRequest("Forbidden")
obj['url'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['storedName'])) owner = db.users.find_one({"_id": proj["user"]}, {"username": 1, "avatar": 1})
if obj['type'] == 'pattern' and 'preview' in obj and '.png' in obj['preview']: if obj["type"] == "file" and "storedName" in obj:
obj['previewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['preview'])) obj["url"] = uploads.get_presigned_url(
del obj['preview'] "projects/{0}/{1}".format(proj["_id"], obj["storedName"])
if obj.get('fullPreview'): )
obj['fullPreviewUrl'] = uploads.get_presigned_url('projects/{0}/{1}'.format(proj['_id'], obj['fullPreview'])) if obj["type"] == "pattern" and "preview" in obj and ".png" in obj["preview"]:
obj['projectObject'] = proj obj["previewUrl"] = uploads.get_presigned_url(
if owner: "projects/{0}/{1}".format(proj["_id"], obj["preview"])
if 'avatar' in owner: )
owner['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(owner['_id']), owner['avatar'])) del obj["preview"]
obj['projectObject']['owner'] = owner if obj.get("fullPreview"):
return obj 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): def copy_to_project(user, id, project_id):
db = database.get_db() db = database.get_db()
obj = db.objects.find_one(ObjectId(id)) obj = db.objects.find_one(ObjectId(id))
if not obj: raise util.errors.NotFound('This object could not be found') if not obj:
original_project = db.projects.find_one(obj['project']) raise util.errors.NotFound("This object could not be found")
if not original_project: original_project = db.projects.find_one(obj["project"])
raise util.errors.NotFound('Project not found') if not original_project:
if not original_project.get('openSource') and not util.can_edit_project(user, original_project): raise util.errors.NotFound("Project not found")
raise util.errors.Forbidden('This project is not open-source') if not original_project.get("openSource") and not util.can_edit_project(
if original_project.get('visibility') != 'public' and not util.can_edit_project(user, original_project): user, original_project
raise util.errors.Forbidden('This project is not public') ):
target_project = db.projects.find_one(ObjectId(project_id)) raise util.errors.Forbidden("This project is not open-source")
if not target_project or not util.can_edit_project(user, target_project): if original_project.get("visibility") != "public" and not util.can_edit_project(
raise util.errors.Forbidden('You don\'t own the target 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): def get_wif(user, id):
db = database.get_db() db = database.get_db()
obj = db.objects.find_one(ObjectId(id)) obj = db.objects.find_one(ObjectId(id))
if not obj: raise util.errors.NotFound('Object not found') if not obj:
project = db.projects.find_one(obj['project']) raise util.errors.NotFound("Object not found")
if not project.get('openSource') and not util.can_edit_project(user, project): project = db.projects.find_one(obj["project"])
raise util.errors.Forbidden('This project is not open-source') if not project.get("openSource") and not util.can_edit_project(user, project):
if project.get('visibility') != 'public' and not util.can_edit_project(user, project): raise util.errors.Forbidden("This project is not open-source")
raise util.errors.Forbidden('This project is not public') if project.get("visibility") != "public" and not util.can_edit_project(
try: user, project
output = wif.dumps(obj).replace('\n', '\\n') ):
return {'wif': output} raise util.errors.Forbidden("This project is not public")
except Exception as e: try:
raise util.errors.BadRequest('Unable to create WIF file') 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): def get_pdf(user, id):
db = database.get_db() db = database.get_db()
obj = db.objects.find_one(ObjectId(id)) obj = db.objects.find_one(ObjectId(id))
if not obj: raise util.errors.NotFound('Object not found') if not obj:
project = db.projects.find_one(obj['project']) raise util.errors.NotFound("Object not found")
if not project.get('openSource') and not util.can_edit_project(user, project): project = db.projects.find_one(obj["project"])
raise util.errors.Forbidden('This project is not open-source') if not project.get("openSource") and not util.can_edit_project(user, project):
if project.get('visibility') != 'public' and not util.can_edit_project(user, project): raise util.errors.Forbidden("This project is not open-source")
raise util.errors.Forbidden('This project is not public') if project.get("visibility") != "public" and not util.can_edit_project(
try: user, project
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() raise util.errors.Forbidden("This project is not public")
pdf = uploads.get_file('objects/' + id + '/export.pdf') try:
body64 = base64.b64encode(pdf['Body'].read()) response = requests.get(
bytes_str = str(body64).replace("b'", '')[:-1] "https://h2io6k3ovg.execute-api.eu-west-1.amazonaws.com/prod/pdf?object="
return {'pdf': body64.decode('ascii')} + id
except Exception as e: + "&landscape=true&paperWidth=23.39&paperHeight=33.11"
print(e) )
raise util.errors.BadRequest('Unable to export PDF') 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): def update(user, id, data):
db = database.get_db() db = database.get_db()
obj = db.objects.find_one(ObjectId(id), {'project': 1}) obj = db.objects.find_one(ObjectId(id), {"project": 1})
if not obj: raise util.errors.NotFound('Object not found') if not obj:
project = db.projects.find_one(obj.get('project'), {'user': 1}) raise util.errors.NotFound("Object not found")
if not project: raise util.errors.NotFound('Project not found') project = db.projects.find_one(obj.get("project"), {"user": 1})
if not util.can_edit_project(user, project): if not project:
raise util.errors.Forbidden('Forbidden') raise util.errors.NotFound("Project not found")
allowed_keys = ['name', 'description', 'pattern'] if not util.can_edit_project(user, project):
raise util.errors.Forbidden("Forbidden")
allowed_keys = ["name", "description", "pattern"]
if data.get('pattern'): if data.get("pattern"):
obj.update(data) obj.update(data)
images = wif.generate_images(obj) images = wif.generate_images(obj)
if images: if images:
data.update(images) data.update(images)
allowed_keys += ['preview', 'fullPreview'] 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): def create_comment(user, id, data):
if not data or not data.get('content'): raise util.errors.BadRequest('Comment data is required') if not data or not data.get("content"):
db = database.get_db() raise util.errors.BadRequest("Comment data is required")
obj = db.objects.find_one({'_id': ObjectId(id)}) db = database.get_db()
if not obj: raise util.errors.NotFound('We could not find the specified object') obj = db.objects.find_one({"_id": ObjectId(id)})
project = db.projects.find_one({'_id': obj['project']}) if not obj:
comment = { raise util.errors.NotFound("We could not find the specified object")
'content': data.get('content', ''), project = db.projects.find_one({"_id": obj["project"]})
'object': ObjectId(id), comment = {
'user': user['_id'], "content": data.get("content", ""),
'createdAt': datetime.datetime.now() "object": ObjectId(id),
} "user": user["_id"],
result = db.comments.insert_one(comment) "createdAt": datetime.datetime.now(),
db.objects.update_one({'_id': ObjectId(id)}, {'$inc': {'commentCount': 1}}) }
comment['_id'] = result.inserted_id result = db.comments.insert_one(comment)
comment['authorUser'] = { db.objects.update_one({"_id": ObjectId(id)}, {"$inc": {"commentCount": 1}})
'username': user['username'], comment["_id"] = result.inserted_id
'avatar': user.get('avatar'), comment["authorUser"] = {
'avatarUrl': uploads.get_presigned_url('users/{0}/{1}'.format(user['_id'], user.get('avatar'))) "username": user["username"],
} "avatar": user.get("avatar"),
project_owner = db.users.find_one({'_id': project['user'], 'subscriptions.email': 'projects.commented'}) "avatarUrl": uploads.get_presigned_url(
if project_owner and project_owner['_id'] != user['_id']: "users/{0}/{1}".format(user["_id"], user.get("avatar"))
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, }
) project_owner = db.users.find_one(
}) {"_id": project["user"], "subscriptions.email": "projects.commented"}
return comment )
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): def get_comments(user, id):
id = ObjectId(id) id = ObjectId(id)
db = database.get_db() db = database.get_db()
obj = db.objects.find_one({'_id': id}, {'project': 1}) obj = db.objects.find_one({"_id": id}, {"project": 1})
if not obj: raise util.errors.NotFound('Object not found') if not obj:
proj = db.projects.find_one({'_id': obj['project']}, {'user': 1, 'visibility': 1}) raise util.errors.NotFound("Object not found")
if not proj: raise util.errors.NotFound('Project not found') proj = db.projects.find_one({"_id": obj["project"]}, {"user": 1, "visibility": 1})
is_owner = user and (user.get('_id') == proj['user']) if not proj:
if not is_owner and proj['visibility'] != 'public': raise util.errors.NotFound("Project not found")
raise util.errors.Forbidden('This project is private') is_owner = user and (user.get("_id") == proj["user"])
comments = list(db.comments.find({'object': id})) if not is_owner and proj["visibility"] != "public":
user_ids = list(map(lambda c:c['user'], comments)) raise util.errors.Forbidden("This project is private")
users = list(db.users.find({'_id': {'$in': user_ids}}, {'username': 1, 'avatar': 1})) comments = list(db.comments.find({"object": id}))
for comment in comments: user_ids = list(map(lambda c: c["user"], comments))
for u in users: users = list(
if comment['user'] == u['_id']: db.users.find({"_id": {"$in": user_ids}}, {"username": 1, "avatar": 1})
comment['authorUser'] = u )
if 'avatar' in u: for comment in comments:
comment['authorUser']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar'])) for u in users:
return {'comments': comments} 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): def delete_comment(user, id, comment_id):
db = database.get_db() db = database.get_db()
comment = db.comments.find_one({'_id': ObjectId(comment_id)}) comment = db.comments.find_one({"_id": ObjectId(comment_id)})
obj = db.objects.find_one({'_id': ObjectId(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') if not comment or not obj or obj["_id"] != comment["object"]:
project = db.projects.find_one({'_id': obj['project']}) raise util.errors.NotFound("Comment not found")
if comment['user'] != user['_id'] and not util.can_edit_project(user, project): raise util.errors.Forbidden('You can\'t delete this comment') project = db.projects.find_one({"_id": obj["project"]})
db.comments.delete_one({'_id': comment['_id']}) if comment["user"] != user["_id"] and not util.can_edit_project(user, project):
db.objects.update_one({'_id': ObjectId(id)}, {'$inc': {'commentCount': -1}}) raise util.errors.Forbidden("You can't delete this comment")
return {'deletedComment': comment['_id']} db.comments.delete_one({"_id": comment["_id"]})
db.objects.update_one({"_id": ObjectId(id)}, {"$inc": {"commentCount": -1}})
return {"deletedComment": comment["_id"]}

View File

@ -1,189 +1,345 @@
import datetime, re import datetime
import re
from bson.objectid import ObjectId from bson.objectid import ObjectId
from util import database, wif, util from util import database, wif, util
from api import uploads, objects from api import uploads, objects
default_pattern = { default_pattern = {
'warp': { "warp": {
'shafts': 8, "shafts": 8,
'threading': [{'shaft': 0}] * 100, "threading": [{"shaft": 0}] * 100,
'defaultColour': '178,53,111', "defaultColour": "178,53,111",
'defaultSpacing': 1, "defaultSpacing": 1,
'defaultThickness': 1, "defaultThickness": 1,
}, },
'weft': { "weft": {
'treadles': 8, "treadles": 8,
'treadling': [{'treadle': 0}] * 50, "treadling": [{"treadle": 0}] * 50,
'defaultColour': '53,69,178', "defaultColour": "53,69,178",
'defaultSpacing': 1, "defaultSpacing": 1,
'defaultThickness': 1 "defaultThickness": 1,
}, },
'tieups': [[]] * 8, "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'], "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): def derive_path(name):
path = name.replace(' ', '-').lower() path = name.replace(" ", "-").lower()
return re.sub('[^0-9a-z\-]+', '', path) return re.sub("[^0-9a-z\-]+", "", path)
def get_by_username(username, project_path): def get_by_username(username, project_path):
db = database.get_db() db = database.get_db()
owner = db.users.find_one({'username': username}, {'_id': 1, 'username': 1}) owner = db.users.find_one({"username": username}, {"_id": 1, "username": 1})
if not owner: if not owner:
raise util.errors.BadRequest('User not found') raise util.errors.BadRequest("User not found")
project = db.projects.find_one({'user': owner['_id'], 'path': project_path}) project = db.projects.find_one({"user": owner["_id"], "path": project_path})
if not project: if not project:
raise util.errors.NotFound('Project not found') raise util.errors.NotFound("Project not found")
project['owner'] = owner project["owner"] = owner
project['fullName'] = owner['username'] + '/' + project['path'] project["fullName"] = owner["username"] + "/" + project["path"]
return project return project
def create(user, data): def create(user, data):
if not data: raise util.errors.BadRequest('Invalid request') if not data:
name = data.get('name', '') raise util.errors.BadRequest("Invalid request")
if len(name) < 3: raise util.errors.BadRequest('A longer name is required') name = data.get("name", "")
db = database.get_db() 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): def get(user, username, path):
db = database.get_db() db = database.get_db()
owner = db.users.find_one({'username': username}, {'_id': 1, 'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}) owner = db.users.find_one(
if not owner: raise util.errors.NotFound('User not found') {"username": username},
project = db.projects.find_one({'user': owner['_id'], 'path': path}) {
if not project: raise util.errors.NotFound('Project not found') "_id": 1,
if not util.can_view_project(user, project): "username": 1,
raise util.errors.Forbidden('This project is private') "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): def update(user, username, project_path, update):
db = database.get_db() db = database.get_db()
project = get_by_username(username, project_path) project = get_by_username(username, project_path)
if not util.can_edit_project(user, project): raise util.errors.Forbidden('Forbidden') 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): def delete(user, username, project_path):
db = database.get_db() db = database.get_db()
project = get_by_username(username, project_path) project = get_by_username(username, project_path)
if not util.can_edit_project(user, project): if not util.can_edit_project(user, project):
raise util.errors.Forbidden('Forbidden') raise util.errors.Forbidden("Forbidden")
db.projects.delete_one({'_id': project['_id']}) db.projects.delete_one({"_id": project["_id"]})
db.objects.delete_many({'project': project['_id']}) db.objects.delete_many({"project": project["_id"]})
return {'deletedProject': project['_id'] } return {"deletedProject": project["_id"]}
def get_objects(user, username, path): def get_objects(user, username, path):
db = database.get_db() db = database.get_db()
project = get_by_username(username, path) project = get_by_username(username, path)
if not project: raise util.errors.NotFound('Project not found') if not project:
if not util.can_view_project(user, project): raise util.errors.NotFound("Project not found")
raise util.errors.Forbidden('This project is private') 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): def create_object(user, username, path, data):
if not data and not data.get('type'): raise util.errors.BadRequest('Invalid request') if not data and not data.get("type"):
if not data.get('type'): raise util.errors.BadRequest('Object type is required.') raise util.errors.BadRequest("Invalid request")
db = database.get_db() if not data.get("type"):
project = get_by_username(username, path) raise util.errors.BadRequest("Object type is required.")
if not util.can_edit_project(user, project): raise util.errors.Forbidden('Forbidden') db = database.get_db()
file_count = db.objects.count_documents({'project': project['_id']}) project = get_by_username(username, path)
if not util.can_edit_project(user, project):
raise util.errors.Forbidden("Forbidden")
if data['type'] == 'file': if data["type"] == "file":
if not 'storedName' in data: if "storedName" not in data:
raise util.errors.BadRequest('File stored name must be included') raise util.errors.BadRequest("File stored name must be included")
obj = { obj = {
'project': project['_id'], "project": project["_id"],
'name': data.get('name', 'Untitled file'), "name": data.get("name", "Untitled file"),
'storedName': data['storedName'], "storedName": data["storedName"],
'createdAt': datetime.datetime.now(), "createdAt": datetime.datetime.now(),
'type': 'file', "type": "file",
} }
if re.search(r'(.jpg)|(.png)|(.jpeg)|(.gif)$', data['storedName'].lower()): if re.search(r"(.jpg)|(.png)|(.jpeg)|(.gif)$", data["storedName"].lower()):
obj['isImage'] = True obj["isImage"] = True
result = db.objects.insert_one(obj) result = db.objects.insert_one(obj)
obj['_id'] = result.inserted_id obj["_id"] = result.inserted_id
obj['url'] = uploads.get_presigned_url('projects/{0}/{1}'.format(project['_id'], obj['storedName'])) obj["url"] = uploads.get_presigned_url(
if obj.get('isImage'): "projects/{0}/{1}".format(project["_id"], obj["storedName"])
def handle_cb(h): )
db.objects.update_one({'_id': obj['_id']}, {'$set': {'imageBlurHash': h}}) if obj.get("isImage"):
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})
return objects.get(user, obj['_id']) def handle_cb(h):
raise util.errors.BadRequest('Unable to create object') 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")

View File

@ -1,35 +1,54 @@
import re, datetime from util import database, util
import pymongo from api import uploads
from bson.objectid import ObjectId
from util import database, util, mail
from api import uploads, groups
def get_users(user): def get_users(user):
db = database.get_db() db = database.get_db()
if not util.is_root(user): raise util.errors.Forbidden('Not allowed') if not util.is_root(user):
users = list(db.users.find({}, {'username': 1, 'avatar': 1, 'email': 1, 'createdAt': 1, 'lastSeenAt': 1, 'roles': 1, 'groups': 1}).sort('lastSeenAt', -1).limit(200)) raise util.errors.Forbidden("Not allowed")
group_ids = [] users = list(
for u in users: group_ids += u.get('groups', []) db.users.find(
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: "username": 1,
if 'avatar' in u: "avatar": 1,
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(u['_id']), u['avatar'])) "email": 1,
u['projects'] = [] "createdAt": 1,
for p in projects: "lastSeenAt": 1,
if p['user'] == u['_id']: "roles": 1,
u['projects'].append(p) "groups": 1,
u['groupMemberships'] = [] },
if u.get('groups'): )
for g in groups: .sort("lastSeenAt", -1)
if g['_id'] in u.get('groups', []): .limit(200)
u['groupMemberships'].append(g) )
return {'users': users} 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): def get_groups(user):
db = database.get_db() db = database.get_db()
if not util.is_root(user): raise util.errors.Forbidden('Not allowed') if not util.is_root(user):
groups = list(db.groups.find({})) raise util.errors.Forbidden("Not allowed")
for group in groups: groups = list(db.groups.find({}))
group['memberCount'] = db.users.count_documents({'groups': group['_id']}) for group in groups:
return {'groups': groups} group["memberCount"] = db.users.count_documents({"groups": group["_id"]})
return {"groups": groups}

View File

@ -1,117 +1,236 @@
import re, random import re
import random
import pymongo import pymongo
from util import database, util from util import database, util
from api import uploads from api import uploads
def all(user, params): def all(user, params):
if not params or 'query' not in params: raise util.errors.BadRequest('Username parameter needed') if not params or "query" not in params:
expression = re.compile(params['query'], re.IGNORECASE) raise util.errors.BadRequest("Query parameter needed")
db = database.get_db() 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)) users = list(
for u in users: db.users.find(
if 'avatar' in u: {"username": expression},
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar'])) {"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})) 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})) objects = list(
for o in objects: db.objects.find(
proj = next(p for p in my_projects if p['_id'] == o['project']) {
if proj: "project": {"$in": list(map(lambda p: p["_id"], my_projects))},
o['path'] = user['username'] + '/' + proj['path'] + '/' + str(o['_id']) "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': [ projects = list(
{'user': user['_id']}, db.projects.find(
{'groupVisibility': {'$in': user.get('groups', [])}}, {
{'visibility': 'public'} "name": expression,
]}, {'name': 1, 'path': 1, 'user': 1}).limit(10)) "$or": [
proj_users = list(db.users.find({'_id': {'$in': list(map(lambda p:p['user'], projects))}}, {'username': 1, 'avatar': 1})) {"user": user["_id"]},
for proj in projects: {"groupVisibility": {"$in": user.get("groups", [])}},
for proj_user in proj_users: {"visibility": "public"},
if proj['user'] == proj_user['_id']: ],
proj['owner'] = proj_user },
proj['fullName'] = proj_user['username'] + '/' + proj['path'] {"name": 1, "path": 1, "user": 1},
if 'avatar' in proj_user: ).limit(10)
proj['owner']['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(proj_user['_id'], proj_user['avatar'])) )
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)) 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): def users(user, params):
if not user: raise util.errors.Forbidden('You need to be logged in') if not user:
if not params or 'username' not in params: raise util.errors.BadRequest('Username parameter needed') raise util.errors.Forbidden("You need to be logged in")
expression = re.compile(params['username'], re.IGNORECASE) if not params or "username" not in params:
db = database.get_db() raise util.errors.BadRequest("Username parameter needed")
users = list(db.users.find({'username': expression}, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1}).limit(5).sort('username', pymongo.ASCENDING)) expression = re.compile(params["username"], re.IGNORECASE)
for u in users: db = database.get_db()
if 'avatar' in u: users = list(
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar'])) db.users.find(
return {'users': users} {"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'} def discover(user, count=3):
if user and user.get('_id'): db = database.get_db()
all_projects_query['user'] = {'$ne': user['_id']} projects = []
all_projects = list(db.projects.find(all_projects_query, {'name': 1, 'path': 1, 'user': 1})) users = []
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
interest_fields = ['bio', 'avatar', 'website', 'facebook', 'twitter', 'instagram', 'location'] all_projects_query = {
all_users_query = {'$or': list(map(lambda f: {f: {'$exists': True}}, interest_fields))} "name": {"$not": re.compile("my new project", re.IGNORECASE)},
if user and user.get('_id'): "visibility": "public",
all_users_query['_id'] = {'$ne': user['_id']} }
all_users = list(db.users.find(all_users_query, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1})) if user and user.get("_id"):
random.shuffle(all_users) all_projects_query["user"] = {"$ne": user["_id"]}
for u in all_users: all_projects = list(
if 'avatar' in u: db.projects.find(all_projects_query, {"name": 1, "path": 1, "user": 1})
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(u['_id'], u['avatar'])) )
if user: random.shuffle(all_projects)
u['following'] = u['_id'] in list(map(lambda f: f['user'], user.get('following', []))) for p in all_projects:
users.append(u) if db.objects.find_one(
if len(users) >= count: break {"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 { interest_fields = [
'highlightProjects': projects, "bio",
'highlightUsers': users, "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): return {
db = database.get_db() "highlightProjects": projects,
per_page = 10 "highlightUsers": users,
}
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} 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}

View File

@ -2,35 +2,40 @@ import datetime
from bson.objectid import ObjectId from bson.objectid import ObjectId
from util import database, util from util import database, util
def list_for_user(user): def list_for_user(user):
db = database.get_db() db = database.get_db()
snippets = db.snippets.find({'user': user['_id']}).sort('createdAt', -1) snippets = db.snippets.find({"user": user["_id"]}).sort("createdAt", -1)
return {'snippets': list(snippets)} return {"snippets": list(snippets)}
def create(user, data): def create(user, data):
if not data: raise util.errors.BadRequest('Invalid request') if not data:
name = data.get('name', '') raise util.errors.BadRequest("Invalid request")
snippet_type = data.get('type', '') name = data.get("name", "")
if len(name) < 3: raise util.errors.BadRequest('A longer name is required') snippet_type = data.get("type", "")
if snippet_type not in ['warp', 'weft']: if len(name) < 3:
raise util.errors.BadRequest('Invalid snippet type') raise util.errors.BadRequest("A longer name is required")
db = database.get_db() if snippet_type not in ["warp", "weft"]:
snippet = { raise util.errors.BadRequest("Invalid snippet type")
'name': name, db = database.get_db()
'user': user['_id'], snippet = {
'createdAt': datetime.datetime.utcnow(), "name": name,
'type': snippet_type, "user": user["_id"],
'threading': data.get('threading', []), "createdAt": datetime.datetime.utcnow(),
'treadling': data.get('treadling', []), "type": snippet_type,
} "threading": data.get("threading", []),
result = db.snippets.insert_one(snippet) "treadling": data.get("treadling", []),
snippet['_id'] = result.inserted_id }
return snippet result = db.snippets.insert_one(snippet)
snippet["_id"] = result.inserted_id
return snippet
def delete(user, id): def delete(user, id):
db = database.get_db() db = database.get_db()
snippet = db.snippets.find_one({'_id': ObjectId(id), 'user': user['_id']}) snippet = db.snippets.find_one({"_id": ObjectId(id), "user": user["_id"]})
if not snippet: if not snippet:
raise util.errors.NotFound('Snippet not found') raise util.errors.NotFound("Snippet not found")
db.snippets.delete_one({'_id': snippet['_id']}) db.snippets.delete_one({"_id": snippet["_id"]})
return {'deletedSnippet': snippet['_id'] } return {"deletedSnippet": snippet["_id"]}

View File

@ -1,90 +1,98 @@
import os, time, re import os
import time
import re
from threading import Thread from threading import Thread
from bson.objectid import ObjectId from bson.objectid import ObjectId
import boto3 import boto3
import blurhash import blurhash
from util import database, util from util import database, util
def sanitise_filename(s): def sanitise_filename(s):
bad_chars = re.compile('[^a-zA-Z0-9_.]') bad_chars = re.compile("[^a-zA-Z0-9_.]")
s = bad_chars.sub('_', s) s = bad_chars.sub("_", s)
return s return s
def get_s3(): 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): def get_presigned_url(path):
return os.environ['AWS_S3_ENDPOINT'] + os.environ['AWS_S3_BUCKET'] + '/' + path return os.environ["AWS_S3_ENDPOINT"] + os.environ["AWS_S3_BUCKET"] + "/" + path
s3 = get_s3() s3 = get_s3()
return s3.generate_presigned_url('get_object', return s3.generate_presigned_url(
Params = { "get_object", Params={"Bucket": os.environ["AWS_S3_BUCKET"], "Key": path}
'Bucket': os.environ['AWS_S3_BUCKET'], )
'Key': path
}
)
def upload_file(path, data): def upload_file(path, data):
s3 = get_s3() s3 = get_s3()
s3.upload_fileobj( s3.upload_fileobj(
data, data,
os.environ['AWS_S3_BUCKET'], os.environ["AWS_S3_BUCKET"],
path, path,
) )
def get_file(key): def get_file(key):
s3 = get_s3() s3 = get_s3()
return s3.get_object( return s3.get_object(Bucket=os.environ["AWS_S3_BUCKET"], Key=key)
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) def generate_file_upload_request(
new_name = sanitise_filename('{0}_{1}{2}'.format(file_body or file_name, int(time.time()), file_extension or '')) user, file_name, file_size, file_type, for_type, for_id
s3 = get_s3() ):
signed_url = s3.generate_presigned_url('put_object', if int(file_size) > (1024 * 1024 * 30): # 30MB
Params = { raise util.errors.BadRequest("File size is too big")
'Bucket': os.environ['AWS_S3_BUCKET'], db = database.get_db()
'Key': path + new_name, allowed = False
'ContentType': file_type path = ""
} if for_type == "project":
) project = db.projects.find_one(ObjectId(for_id))
return { allowed = project and util.can_edit_project(user, project)
'signedRequest': signed_url, path = "projects/" + for_id + "/"
'fileName': new_name 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): def handle_blur_image(key, func):
f = get_file(key)['Body'] f = get_file(key)["Body"]
bhash = blurhash.encode(f, x_components=4, y_components=3) bhash = blurhash.encode(f, x_components=4, y_components=3)
func(bhash) func(bhash)
def blur_image(key, func): def blur_image(key, func):
thr = Thread(target=handle_blur_image, args=[key, func]) thr = Thread(target=handle_blur_image, args=[key, func])
thr.start() thr.start()

View File

@ -3,216 +3,343 @@ from bson.objectid import ObjectId
from util import database, util from util import database, util
from api import uploads from api import uploads
def me(user): def me(user):
db = database.get_db() db = database.get_db()
return { return {
'_id': user['_id'], "_id": user["_id"],
'username': user['username'], "username": user["username"],
'bio': user.get('bio'), "bio": user.get("bio"),
'email': user.get('email'), "email": user.get("email"),
'avatar': user.get('avatar'), "avatar": user.get("avatar"),
'avatarUrl': user.get('avatar') and uploads.get_presigned_url('users/{0}/{1}'.format(user['_id'], user['avatar'])), "avatarUrl": user.get("avatar")
'roles': user.get('roles', []), and uploads.get_presigned_url(
'groups': user.get('groups', []), "users/{0}/{1}".format(user["_id"], user["avatar"])
'subscriptions': user.get('subscriptions'), ),
'finishedTours': user.get('completedTours', []) + user.get('skippedTours', []), "roles": user.get("roles", []),
'isSilverSupporter': user.get('isSilverSupporter'), "groups": user.get("groups", []),
'isGoldSupporter': user.get('isGoldSupporter'), "subscriptions": user.get("subscriptions"),
'followerCount': db.users.count_documents({'following.user': user['_id']}), "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): def get(user, username):
db = database.get_db() 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}) fetch_user = db.users.find_one(
if not fetch_user: {"username": username},
raise util.errors.NotFound('User not found') {
project_query = {'user': fetch_user['_id']} "username": 1,
if not user or not user['_id'] == fetch_user['_id']: "createdAt": 1,
project_query['visibility'] = 'public' "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: if "avatar" in fetch_user:
fetch_user['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(fetch_user['_id']), fetch_user['avatar'])) fetch_user["avatarUrl"] = uploads.get_presigned_url(
if user: "users/{0}/{1}".format(str(fetch_user["_id"]), fetch_user["avatar"])
fetch_user['following'] = fetch_user['_id'] in list(map(lambda f: f['user'], user.get('following', []))) )
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)) user_projects = list(
for project in user_projects: db.projects.find(
project['fullName'] = fetch_user['username'] + '/' + project['path'] project_query, {"name": 1, "path": 1, "description": 1, "visibility": 1}
project['owner'] = { ).limit(15)
'_id': fetch_user['_id'], )
'username': fetch_user['username'], for project in user_projects:
'avatar': fetch_user.get('avatar'), project["fullName"] = fetch_user["username"] + "/" + project["path"]
'avatarUrl': fetch_user.get('avatarUrl'), project["owner"] = {
} "_id": fetch_user["_id"],
fetch_user['projects'] = user_projects "username": fetch_user["username"],
"avatar": fetch_user.get("avatar"),
"avatarUrl": fetch_user.get("avatarUrl"),
}
fetch_user["projects"] = user_projects
return fetch_user
return fetch_user
def update(user, username, data): def update(user, username, data):
if not data: raise util.errors.BadRequest('Invalid request') if not data:
db = database.get_db() raise util.errors.BadRequest("Invalid request")
if user['username'] != username: db = database.get_db()
raise util.errors.Forbidden('Not allowed') if user["username"] != username:
allowed_keys = ['username', 'avatar', 'bio', 'location', 'website', 'twitter', 'facebook', 'linkedIn', 'instagram'] raise util.errors.Forbidden("Not allowed")
if 'username' in data: allowed_keys = [
if not data.get('username') or len(data['username']) < 3: "username",
raise util.errors.BadRequest('New username is not valid') "avatar",
if db.users.count_documents({'username': data['username'].lower()}): "bio",
raise util.errors.BadRequest('A user with this username already exists') "location",
data['username'] = data['username'].lower() "website",
if data.get('avatar') and len(data['avatar']) > 3: # Not a default avatar "twitter",
def handle_cb(h): "facebook",
db.users.update_one({'_id': user['_id']}, {'$set': {'avatarBlurHash': h}}) "linkedIn",
uploads.blur_image('users/' + str(user['_id']) + '/' + data['avatar'], handle_cb) "instagram",
updater = util.build_updater(data, allowed_keys) ]
if updater: if "username" in data:
if 'avatar' in updater.get('$unset', {}): # Also unset blurhash if removing avatar if not data.get("username") or len(data["username"]) < 3:
updater['$unset']['avatarBlurHash'] = '' raise util.errors.BadRequest("New username is not valid")
db.users.update_one({'username': username}, updater) if db.users.count_documents({"username": data["username"].lower()}):
return get(user, data.get('username', username)) 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): def finish_tour(user, username, tour, status):
db = database.get_db() db = database.get_db()
if user['username'] != username: if user["username"] != username:
raise util.errors.Forbidden('Not allowed') raise util.errors.Forbidden("Not allowed")
key = 'completedTours' if status == 'completed' else 'skippedTours' key = "completedTours" if status == "completed" else "skippedTours"
db.users.update_one({'_id': user['_id']}, {'$addToSet': {key: tour}}) db.users.update_one({"_id": user["_id"]}, {"$addToSet": {key: tour}})
return {'finishedTour': tour} return {"finishedTour": tour}
def get_projects(user, id): def get_projects(user, id):
db = database.get_db() db = database.get_db()
u = db.users.find_one(id, {'username': 1, 'avatar': 1}) u = db.users.find_one(id, {"username": 1, "avatar": 1})
if not u: raise util.errors.NotFound('User not found') if not u:
if 'avatar' in u: u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(u['_id']), u['avatar'])) raise util.errors.NotFound("User not found")
projects = [] if "avatar" in u:
project_query = {'user': ObjectId(id)} u["avatarUrl"] = uploads.get_presigned_url(
if not user or not user['_id'] == ObjectId(id): "users/{0}/{1}".format(str(u["_id"]), u["avatar"])
project_query['visibility'] = 'public' )
for project in db.projects.find(project_query): projects = []
project['owner'] = u project_query = {"user": ObjectId(id)}
project['fullName'] = u['username'] + '/' + project['path'] if not user or not user["_id"] == ObjectId(id):
projects.append(project) project_query["visibility"] = "public"
return projects 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): def create_email_subscription(user, username, subscription):
db = database.get_db() db = database.get_db()
if user['username'] != username: raise util.errors.Forbidden('Forbidden') if user["username"] != username:
u = db.users.find_one({'username': username}) raise util.errors.Forbidden("Forbidden")
db.users.update_one({'_id': u['_id']}, {'$addToSet': {'subscriptions.email': subscription}}) u = db.users.find_one({"username": username})
subs = db.users.find_one(u['_id'], {'subscriptions': 1}) db.users.update_one(
return {'subscriptions': subs.get('subscriptions', {})} {"_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): def delete_email_subscription(user, username, subscription):
db = database.get_db() db = database.get_db()
if user['username'] != username: raise util.errors.Forbidden('Forbidden') if user["username"] != username:
u = db.users.find_one({'username': username}) raise util.errors.Forbidden("Forbidden")
db.users.update_one({'_id': u['_id']}, {'$pull': {'subscriptions.email': subscription}}) u = db.users.find_one({"username": username})
subs = db.users.find_one(u['_id'], {'subscriptions': 1}) db.users.update_one(
return {'subscriptions': subs.get('subscriptions', {})} {"_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): def create_follower(user, username):
db = database.get_db() db = database.get_db()
target_user = db.users.find_one({'username': username.lower()}) target_user = db.users.find_one({"username": username.lower()})
if not target_user: raise util.errors.NotFound('User not found') if not target_user:
if target_user['_id'] == user['_id']: raise util.errors.BadRequest('Cannot follow yourself') raise util.errors.NotFound("User not found")
follow_object = { if target_user["_id"] == user["_id"]:
'user': target_user['_id'], raise util.errors.BadRequest("Cannot follow yourself")
'followedAt': datetime.datetime.utcnow(), follow_object = {
} "user": target_user["_id"],
db.users.update_one({'_id': user['_id']}, {'$addToSet': {'following': follow_object}}) "followedAt": datetime.datetime.utcnow(),
return follow_object }
db.users.update_one(
{"_id": user["_id"]}, {"$addToSet": {"following": follow_object}}
)
return follow_object
def delete_follower(user, username): def delete_follower(user, username):
db = database.get_db() db = database.get_db()
target_user = db.users.find_one({'username': username.lower()}) target_user = db.users.find_one({"username": username.lower()})
if not target_user: raise util.errors.NotFound('User not found') if not target_user:
db.users.update_one({'_id': user['_id']}, {'$pull': {'following': {'user': target_user['_id']}}}) raise util.errors.NotFound("User not found")
return {'unfollowed': True} db.users.update_one(
{"_id": user["_id"]}, {"$pull": {"following": {"user": target_user["_id"]}}}
)
return {"unfollowed": True}
def get_feed(user, username): def get_feed(user, username):
db = database.get_db() db = database.get_db()
if user['username'] != username: raise util.errors.Forbidden('Forbidden') if user["username"] != username:
following_user_ids = list(map(lambda f: f['user'], user.get('following', []))) raise util.errors.Forbidden("Forbidden")
following_project_ids = list(map(lambda p: p['_id'], db.projects.find({'user': {'$in': following_user_ids}, 'visibility': 'public'}, {'_id': 1}))) following_user_ids = list(map(lambda f: f["user"], user.get("following", [])))
one_year_ago = datetime.datetime.utcnow() - datetime.timedelta(days = 365) 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 # Fetch the items for the feed
recent_projects = list(db.projects.find({ recent_projects = list(
'_id': {'$in': following_project_ids}, db.projects.find(
'createdAt': {'$gt': one_year_ago}, {
'visibility': 'public', "_id": {"$in": following_project_ids},
}, {'user': 1, 'createdAt': 1, 'name': 1, 'path': 1, 'visibility': 1}).sort('createdAt', -1).limit(20)) "createdAt": {"$gt": one_year_ago},
recent_objects = list(db.objects.find({ "visibility": "public",
'project': {'$in': following_project_ids}, },
'createdAt': {'$gt': one_year_ago} {"user": 1, "createdAt": 1, "name": 1, "path": 1, "visibility": 1},
}, {'project': 1, 'createdAt': 1, 'name': 1}).sort('createdAt', -1).limit(30)) )
recent_comments = list(db.comments.find({ .sort("createdAt", -1)
'user': {'$in': following_user_ids}, .limit(20)
'createdAt': {'$gt': one_year_ago} )
}, {'user': 1, 'createdAt': 1, 'object': 1, 'content': 1}).sort('createdAt', -1).limit(30)) 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) # Process objects (as don't know the user)
object_project_ids = list(map(lambda o: o['project'], recent_objects)) 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})) object_projects = list(
for obj in recent_objects: db.projects.find(
for proj in object_projects: {"_id": {"$in": object_project_ids}, "visibility": "public"}, {"user": 1}
if obj['project'] == proj['_id']: obj['user'] = proj.get('user') )
)
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 # Process comments as don't know the project
comment_object_ids = list(map(lambda c: c['object'], recent_comments)) comment_object_ids = list(map(lambda c: c["object"], recent_comments))
comment_objects = list(db.objects.find({'_id': {'$in': comment_object_ids}}, {'project': 1})) comment_objects = list(
for com in recent_comments: db.objects.find({"_id": {"$in": comment_object_ids}}, {"project": 1})
for obj in comment_objects: )
if com['object'] == obj['_id']: com['project'] = obj.get('project') 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 # Prepare the feed items, and sort it
feed_items = [] feed_items = []
for p in recent_projects: for p in recent_projects:
p['feedType'] = 'project' p["feedType"] = "project"
feed_items.append(p) feed_items.append(p)
for o in recent_objects: for o in recent_objects:
o['feedType'] = 'object' o["feedType"] = "object"
feed_items.append(o) feed_items.append(o)
for c in recent_comments: for c in recent_comments:
c['feedType'] = 'comment' c["feedType"] = "comment"
feed_items.append(c) feed_items.append(c)
feed_items.sort(key=lambda d: d['createdAt'], reverse = True) feed_items.sort(key=lambda d: d["createdAt"], reverse=True)
feed_items = feed_items[:20] feed_items = feed_items[:20]
# Post-process the feed, adding user/project objects # Post-process the feed, adding user/project objects
feed_user_ids = set() feed_user_ids = set()
feed_project_ids = set() feed_project_ids = set()
for f in feed_items: for f in feed_items:
feed_user_ids.add(f.get('user')) feed_user_ids.add(f.get("user"))
feed_project_ids.add(f.get('project')) 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_projects = list(
feed_users = list(db.users.find({'$or': [ db.projects.find(
{'_id': {'$in': list(feed_user_ids)}}, {"_id": {"$in": list(feed_project_ids)}, "visibility": "public"},
{'_id': {'$in': list(map(lambda p: p['user'], feed_projects))}}, {"name": 1, "path": 1, "user": 1, "visibility": 1},
]}, {'username': 1, 'avatar': 1, 'isSilverSupporter': 1, 'isGoldSupporter': 1})) )
for u in feed_users: )
if 'avatar' in u: feed_users = list(
u['avatarUrl'] = uploads.get_presigned_url('users/{0}/{1}'.format(str(u['_id']), u['avatar'])) db.users.find(
feed_user_map = {} {
feed_project_map = {} "$or": [
for u in feed_users: feed_user_map[str(u['_id'])] = u {"_id": {"$in": list(feed_user_ids)}},
for p in feed_projects: feed_project_map[str(p['_id'])] = p {"_id": {"$in": list(map(lambda p: p["user"], feed_projects))}},
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'])) {"username": 1, "avatar": 1, "isSilverSupporter": 1, "isGoldSupporter": 1},
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'])): for u in feed_users:
f['projectObject']['userObject'] = feed_user_map.get(str(f['projectObject']['user'])) 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 # Filter out orphaned or non-public comments/objects
def filter_func(f): def filter_func(f):
if f['feedType'] == 'comment' and not f.get('projectObject'): if f["feedType"] == "comment" and not f.get("projectObject"):
return False return False
if f['feedType'] == 'object' and not f.get('projectObject'): if f["feedType"] == "object" and not f.get("projectObject"):
return False return False
return True return True
feed_items = list(filter(filter_func, feed_items))
return {'feed': feed_items} feed_items = list(filter(filter_func, feed_items))
return {"feed": feed_items}

View File

@ -1,345 +1,709 @@
import os, json import os
import json
from flask import Flask, request, Response, jsonify from flask import Flask, request, Response, jsonify
from flask_limiter import Limiter from flask_limiter import Limiter
from flask_cors import CORS from flask_cors import CORS
import werkzeug import werkzeug
from webargs import fields, validate
from webargs.flaskparser import use_args
import sentry_sdk import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
from util import util 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__) app = Flask(__name__)
CORS(app) 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) @app.errorhandler(werkzeug.exceptions.TooManyRequests)
def handle_429(e): 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) @app.errorhandler(werkzeug.exceptions.BadRequest)
def handle_bad_request(e): def handle_bad_request(e):
return jsonify({'message': e.description}), 400 return jsonify({"message": e.description}), 400
@app.errorhandler(werkzeug.exceptions.Unauthorized) @app.errorhandler(werkzeug.exceptions.Unauthorized)
def handle_not_authorized(e): def handle_not_authorized(e):
return jsonify({'message': e.description}), 401 return jsonify({"message": e.description}), 401
@app.errorhandler(werkzeug.exceptions.Forbidden) @app.errorhandler(werkzeug.exceptions.Forbidden)
def handle_forbidden(e): def handle_forbidden(e):
return jsonify({'message': e.description}), 403 return jsonify({"message": e.description}), 403
@app.errorhandler(werkzeug.exceptions.NotFound) @app.errorhandler(werkzeug.exceptions.NotFound)
def handle_not_found(e): def handle_not_found(e):
return jsonify({'message': e.description}), 404 return jsonify({"message": e.description}), 404
@app.route('/debug-sentry')
@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(): def trigger_error():
division_by_zero = 1 / 0 division_by_zero = 1 / 0 # noqa: F841
# ACCOUNTS # 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(): 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']) @app.route("/accounts", methods=["DELETE"])
def email_address(): @use_args(
body = request.json {
return util.jsonify(accounts.update_email(util.get_user(), body)) "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/email", methods=["PUT"])
@app.route('/accounts/password/reset', methods=['POST']) @use_args(
def reset_password(): {
body = request.json "email": fields.Email(required=True),
return util.jsonify(accounts.reset_password(body)) }
)
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 # UPLOADS
@app.route('/uploads/file/request', methods=['GET'])
def file_request(): @app.route("/uploads/file/request", methods=["GET"])
params = request.args @use_args(
file_name = params.get('name') {
file_size = params.get('size') "name": fields.Str(required=True),
file_type = params.get('type') "size": fields.Int(required=True),
for_type = params.get('forType') "type": fields.Str(required=True),
for_id = params.get('forId') "forType": fields.Str(required=True),
return util.jsonify(uploads.generate_file_upload_request(util.get_user(), file_name, file_size, file_type, for_type, for_id)) "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 # USERS
@app.route('/users/me', methods=['GET'])
@app.route("/users/me", methods=["GET"])
def users_me(): 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): def users_feed(username):
if request.method == 'GET': return util.jsonify(users.get_feed(util.get_user(), username)) return util.jsonify(users.get_feed(util.get_user(), username))
@app.route('/users/<username>/followers', methods=['POST', 'DELETE'])
@app.route("/users/<username>/followers", methods=["POST", "DELETE"])
def users_followers(username): def users_followers(username):
if request.method == 'POST': return util.jsonify(users.create_follower(util.get_user(), username)) if request.method == "POST":
if request.method == 'DELETE': return util.jsonify(users.delete_follower(util.get_user(), username)) 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(): def me_projects_route():
user = util.get_user() user = util.get_user()
return util.jsonify({'projects': users.get_projects(user, user['_id'])}) 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): 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 == "PUT":
if request.method == 'DELETE': return util.jsonify(users.delete_email_subscription(util.get_user(), username, subscription)) 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 # 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): def project_objects_get(username, project_path):
if request.method == 'GET': return util.jsonify(
return util.jsonify({'objects': projects.get_objects(util.get_user(required=False), username, project_path)}) {
if request.method == 'POST': "objects": projects.get_objects(
return util.jsonify(projects.create_object(util.get_user(), username, project_path, request.json)) 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): def project_by_path(username, project_path):
if request.method == 'GET': if request.method == "GET":
return util.jsonify(projects.get(util.get_user(required=False), username, project_path)) return util.jsonify(
if request.method == 'PUT': projects.get(util.get_user(required=False), username, project_path)
return util.jsonify(projects.update(util.get_user(), username, project_path, request.json)) )
if request.method == 'DELETE': if request.method == "DELETE":
return util.jsonify(projects.delete(util.get_user(), username, project_path)) 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 # 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): def copy_object_route(id, project_id):
if request.method == 'PUT':
return util.jsonify(objects.copy_to_project(util.get_user(), id, project_id)) 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): def objects_get_wif(id):
if request.method == 'GET':
return util.jsonify(objects.get_wif(util.get_user(required=False), id)) 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): 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): @app.route("/objects/<id>/comments", methods=["GET"])
if request.method == 'GET': def object_comments_get(id):
return util.jsonify(objects.get_comments(util.get_user(required=False), 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): 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 # SNIPPETS
@app.route('/snippets', methods=['POST', 'GET']) @app.route("/snippets", methods=["GET"])
def snippets_route(): def snippets_route_get():
if request.method == 'POST':
return util.jsonify(snippets.create(util.get_user(), request.json))
if request.method == 'GET':
return util.jsonify(snippets.list_for_user(util.get_user())) 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): def snippet_route(id):
if request.method == 'DELETE':
return util.jsonify(snippets.delete(util.get_user(), id)) return util.jsonify(snippets.delete(util.get_user(), id))
# GROUPS # GROUPS
@app.route('/groups', methods=['POST', 'GET'])
def groups_route(): @app.route("/groups", methods=["GET"])
if request.method == 'GET': def groups_route_get():
return util.jsonify(groups.get(util.get_user(required=True))) 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): def group_route(id):
if request.method == 'GET': if request.method == "GET":
return util.jsonify(groups.get_one(util.get_user(required=False), id)) return util.jsonify(groups.get_one(util.get_user(required=False), id))
if request.method == 'PUT': if request.method == "DELETE":
return util.jsonify(groups.update(util.get_user(required=True), id, request.json)) return util.jsonify(groups.delete(util.get_user(required=True), 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): @app.route("/groups/<id>", methods=["PUT"])
if request.method == 'GET': @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)) 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): 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)) 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): def group_members_route(id):
if request.method == 'GET':
return util.jsonify(groups.get_members(util.get_user(required=True), id)) 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): def group_projects_route(id):
if request.method == 'GET':
return util.jsonify(groups.get_projects(util.get_user(required=True), id)) 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): 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): def group_requests_route(id):
if request.method == 'POST': if request.method == "POST":
return util.jsonify(invitations.create_group_request(util.get_user(required=True), id)) return util.jsonify(
if request.method == 'GET': invitations.create_group_request(util.get_user(required=True), id)
return util.jsonify(invitations.get_group_requests(util.get_user(required=True), id)) )
if request.method == "GET":
return util.jsonify(
invitations.get_group_requests(util.get_user(required=True), id)
)
# SEARCH # 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']) @app.route("/search", methods=["GET"])
def search_users(): @use_args(
params = request.args {
return util.jsonify(search.users(util.get_user(required=True), params)) "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']) @app.route("/search/users", methods=["GET"])
def search_explore(): @use_args(
page = request.args.get('page', 1) {
if page: page = int(page) "username": fields.Str(required=True),
return util.jsonify(search.explore(page=page)) },
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 # 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): def invite_route(id):
if request.method == 'PUT': if request.method == "PUT":
return util.jsonify(invitations.accept(util.get_user(required=True), id)) return util.jsonify(invitations.accept(util.get_user(required=True), id))
if request.method =='DELETE': if request.method == "DELETE":
return util.jsonify(invitations.delete(util.get_user(required=True), id)) return util.jsonify(invitations.delete(util.get_user(required=True), id))
## ROOT ## ROOT
@app.route('/root/users', methods=['GET'])
@app.route("/root/users", methods=["GET"])
def root_users(): def root_users():
return util.jsonify(root.get_users(util.get_user(required=True))) return util.jsonify(root.get_users(util.get_user(required=True)))
@app.route('/root/groups', methods=['GET'])
@app.route("/root/groups", methods=["GET"])
def root_groups(): 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 ## 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): def ap_user(username):
resp_data = activitypub.user(username) resp_data = activitypub.user(username)
resp = Response(json.dumps(resp_data)) resp = Response(json.dumps(resp_data))
resp.headers['Content-Type'] = 'application/activity+json' resp.headers["Content-Type"] = "application/activity+json"
return resp return resp
@app.route('/u/<username>/outbox', methods=['GET'])
def ap_user_outbox(username): @app.route("/u/<username>/outbox", methods=["GET"])
page = request.args.get('page') @use_args(
min_id = request.args.get('min_id') {
max_id = request.args.get('max_id') "page": fields.Int(),
resp_data = activitypub.outbox(username, page, min_id, max_id) "min_id": fields.Str(),
resp = Response(json.dumps(resp_data)) "max_id": fields.Str(),
resp.headers['Content-Type'] = 'application/activity+json' },
return resp 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
View File

@ -0,0 +1,4 @@
#!/bin/bash
ruff format .
ruff check --fix .

View File

@ -1,21 +1,34 @@
# Script to migrate from the old data: string URLs for images to image files directly on S3. # Script to migrate from the old data: string URLs for images to image files directly on S3.
from pymongo import MongoClient 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}): for obj in db.objects.find(
preview = obj['preview'] {"preview": {"$regex": "^data:"}}, {"preview": 1, "project": 1}
preview = preview.replace('data:image/png;base64,', '') ):
preview = obj["preview"]
preview = preview.replace("data:image/png;base64,", "")
imgdata = base64.b64decode(preview) imgdata = base64.b64decode(preview)
filename = 'some_image.png' filename = "some_image.png"
os.makedirs('migration_projects/projects/'+str(obj['project']), exist_ok=True) 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: with open(
f.write(imgdata) "migration_projects/projects/"
db.objects.update_one({'_id': obj['_id']}, {'$set': {'previewNew': 'preview_'+str(obj['_id'])+'.png'}}) + str(obj["project"])
#exit() + "/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
View File

@ -81,17 +81,17 @@ testing = ["pytest"]
[[package]] [[package]]
name = "boto3" name = "boto3"
version = "1.35.4" version = "1.35.34"
description = "The AWS SDK for Python" description = "The AWS SDK for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "boto3-1.35.4-py3-none-any.whl", hash = "sha256:96c39593afb7b55ebb74d08c8e3201041d105b557c8c8536c9054c9f13da5f2a"}, {file = "boto3-1.35.34-py3-none-any.whl", hash = "sha256:291e7b97a34967ed93297e6171f1bebb8529e64633dd48426760e3fdef1cdea8"},
{file = "boto3-1.35.4.tar.gz", hash = "sha256:d997b82c468bd5c2d5cd29810d47079b66b178d2b5ae021aebe262c4d78d4c94"}, {file = "boto3-1.35.34.tar.gz", hash = "sha256:57e6ee8504e7929bc094bb2afc879943906064179a1e88c23b4812e2c6f61532"},
] ]
[package.dependencies] [package.dependencies]
botocore = ">=1.35.4,<1.36.0" botocore = ">=1.35.34,<1.36.0"
jmespath = ">=0.7.1,<2.0.0" jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.10.0,<0.11.0" s3transfer = ">=0.10.0,<0.11.0"
@ -100,13 +100,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]] [[package]]
name = "botocore" name = "botocore"
version = "1.35.4" version = "1.35.34"
description = "Low-level, data-driven core of boto 3." description = "Low-level, data-driven core of boto 3."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "botocore-1.35.4-py3-none-any.whl", hash = "sha256:10195e5ca764745f02b9a51df048b996ddbdc1899a44a2caf35dfb225dfea489"}, {file = "botocore-1.35.34-py3-none-any.whl", hash = "sha256:ccb0fe397b11b81c9abc0c87029d17298e17bf658d8db5c0c5a551a12a207e7a"},
{file = "botocore-1.35.4.tar.gz", hash = "sha256:4cc51a6a486915aedc140f9d027b7e156646b7a0f7b33b1000762c81aff9a12f"}, {file = "botocore-1.35.34.tar.gz", hash = "sha256:789b6501a3bb4a9591c1fe10da200cc315c1fa5df5ada19c720d8ef06439b3e3"},
] ]
[package.dependencies] [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\""} urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}
[package.extras] [package.extras]
crt = ["awscrt (==0.21.2)"] crt = ["awscrt (==0.22.0)"]
[[package]] [[package]]
name = "cachecontrol" name = "cachecontrol"
@ -450,21 +450,22 @@ wmi = ["wmi (>=1.5.1)"]
[[package]] [[package]]
name = "firebase-admin" name = "firebase-admin"
version = "4.5.3" version = "6.5.0"
description = "Firebase Admin Python SDK" description = "Firebase Admin Python SDK"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.7"
files = [ files = [
{file = "firebase_admin-4.5.3-py3-none-any.whl", hash = "sha256:471045bf72bb68ccf6d9d19a35836609b7d525fcdac73aa619d274e4c5585e0a"}, {file = "firebase_admin-6.5.0-py3-none-any.whl", hash = "sha256:fe34ee3ca0e625c5156b3931ca4b4b69b5fc344dbe51bba9706ff674ce277898"},
{file = "firebase_admin-4.5.3.tar.gz", hash = "sha256:f0c1c9a6e56b497c8bbaa55a679a402f79c34c2c24971b11ea909031b520ed32"}, {file = "firebase_admin-6.5.0.tar.gz", hash = "sha256:e716dde1447f0a1cd1523be76ff872df33c4e1a3c079564ace033b2ad60bcc4f"},
] ]
[package.dependencies] [package.dependencies]
cachecontrol = ">=0.12.6" 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-api-python-client = ">=1.7.8"
google-cloud-firestore = {version = ">=1.4.0", markers = "platform_python_implementation != \"PyPy\""} google-cloud-firestore = {version = ">=2.9.1", markers = "platform_python_implementation != \"PyPy\""}
google-cloud-storage = ">=1.18.0" google-cloud-storage = ">=1.37.1"
pyjwt = {version = ">=2.5.0", extras = ["crypto"]}
[[package]] [[package]]
name = "flask" name = "flask"
@ -490,13 +491,13 @@ dotenv = ["python-dotenv"]
[[package]] [[package]]
name = "flask-cors" name = "flask-cors"
version = "4.0.1" version = "5.0.0"
description = "A Flask extension adding a decorator for CORS support" description = "A Flask extension adding a decorator for CORS support"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "Flask_Cors-4.0.1-py2.py3-none-any.whl", hash = "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677"}, {file = "Flask_Cors-5.0.0-py2.py3-none-any.whl", hash = "sha256:b9e307d082a9261c100d8fb0ba909eec6a228ed1b60a8315fd85f783d61910bc"},
{file = "flask_cors-4.0.1.tar.gz", hash = "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4"}, {file = "flask_cors-5.0.0.tar.gz", hash = "sha256:5aadb4b950c4e93745034594d9f3ea6591f734bb3662e16e255ffbf5e89c88ef"},
] ]
[package.dependencies] [package.dependencies]
@ -1078,6 +1079,25 @@ files = [
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, {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]] [[package]]
name = "mdurl" name = "mdurl"
version = "0.1.2" version = "0.1.2"
@ -1385,6 +1405,9 @@ files = [
{file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
] ]
[package.dependencies]
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
[package.extras] [package.extras]
crypto = ["cryptography (>=3.4.0)"] 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"] 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]] [[package]]
name = "pymongo" name = "pymongo"
version = "4.8.0" version = "4.10.1"
description = "Python driver for MongoDB <http://www.mongodb.org>" description = "Python driver for MongoDB <http://www.mongodb.org>"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pymongo-4.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2b7bec27e047e84947fbd41c782f07c54c30c76d14f3b8bf0c89f7413fac67a"}, {file = "pymongo-4.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e699aa68c4a7dea2ab5a27067f7d3e08555f8d2c0dc6a0c8c60cfd9ff2e6a4b1"},
{file = "pymongo-4.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c68fe128a171493018ca5c8020fc08675be130d012b7ab3efe9e22698c612a1"}, {file = "pymongo-4.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70645abc714f06b4ad6b72d5bf73792eaad14e3a2cfe29c62a9c81ada69d9e4b"},
{file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920d4f8f157a71b3cb3f39bc09ce070693d6e9648fb0e30d00e2657d1dca4e49"}, {file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae2fd94c9fe048c94838badcc6e992d033cb9473eb31e5710b3707cba5e8aee2"},
{file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52b4108ac9469febba18cea50db972605cc43978bedaa9fea413378877560ef8"}, {file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ded27a4a5374dae03a92e084a60cdbcecd595306555bda553b833baf3fc4868"},
{file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:180d5eb1dc28b62853e2f88017775c4500b07548ed28c0bd9c005c3d7bc52526"}, {file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ecc2455e3974a6c429687b395a0bc59636f2d6aedf5785098cf4e1f180f1c71"},
{file = "pymongo-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aec2b9088cdbceb87e6ca9c639d0ff9b9d083594dda5ca5d3c4f6774f4c81b33"}, {file = "pymongo-4.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920fee41f7d0259f5f72c1f1eb331bc26ffbdc952846f9bd8c3b119013bb52c"},
{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.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0a15665b2d6cf364f4cd114d62452ce01d71abfbd9c564ba8c74dcd7bbd6822"},
{file = "pymongo-4.8.0-cp310-cp310-win32.whl", hash = "sha256:8b18c8324809539c79bd6544d00e0607e98ff833ca21953df001510ca25915d1"}, {file = "pymongo-4.10.1-cp310-cp310-win32.whl", hash = "sha256:29e1c323c28a4584b7095378ff046815e39ff82cdb8dc4cc6dfe3acf6f9ad1f8"},
{file = "pymongo-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e5df28f74002e37bcbdfdc5109799f670e4dfef0fb527c391ff84f078050e7b5"}, {file = "pymongo-4.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:88dc4aa45f8744ccfb45164aedb9a4179c93567bbd98a33109d7dc400b00eb08"},
{file = "pymongo-4.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b50040d9767197b77ed420ada29b3bf18a638f9552d80f2da817b7c4a4c9c68"}, {file = "pymongo-4.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:57ee6becae534e6d47848c97f6a6dff69e3cce7c70648d6049bd586764febe59"},
{file = "pymongo-4.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:417369ce39af2b7c2a9c7152c1ed2393edfd1cbaf2a356ba31eb8bcbd5c98dd7"}, {file = "pymongo-4.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f437a612f4d4f7aca1812311b1e84477145e950fdafe3285b687ab8c52541f3"},
{file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf821bd3befb993a6db17229a2c60c1550e957de02a6ff4dd0af9476637b2e4d"}, {file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a970fd3117ab40a4001c3dad333bbf3c43687d90f35287a6237149b5ccae61d"},
{file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9365166aa801c63dff1a3cb96e650be270da06e3464ab106727223123405510f"}, {file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c4d0e7cd08ef9f8fbf2d15ba281ed55604368a32752e476250724c3ce36c72e"},
{file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc8b8582f4209c2459b04b049ac03c72c618e011d3caa5391ff86d1bda0cc486"}, {file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca6f700cff6833de4872a4e738f43123db34400173558b558ae079b5535857a4"},
{file = "pymongo-4.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e5019f75f6827bb5354b6fef8dfc9d6c7446894a27346e03134d290eb9e758"}, {file = "pymongo-4.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cec237c305fcbeef75c0bcbe9d223d1e22a6e3ba1b53b2f0b79d3d29c742b45b"},
{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.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3337804ea0394a06e916add4e5fac1c89902f1b6f33936074a12505cab4ff05"},
{file = "pymongo-4.8.0-cp311-cp311-win32.whl", hash = "sha256:4bf58e6825b93da63e499d1a58de7de563c31e575908d4e24876234ccb910eba"}, {file = "pymongo-4.10.1-cp311-cp311-win32.whl", hash = "sha256:778ac646ce6ac1e469664062dfe9ae1f5c9961f7790682809f5ec3b8fda29d65"},
{file = "pymongo-4.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b747c0e257b9d3e6495a018309b9e0c93b7f0d65271d1d62e572747f4ffafc88"}, {file = "pymongo-4.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:9df4ab5594fdd208dcba81be815fa8a8a5d8dedaf3b346cbf8b61c7296246a7a"},
{file = "pymongo-4.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e6a720a3d22b54183352dc65f08cd1547204d263e0651b213a0a2e577e838526"}, {file = "pymongo-4.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fbedc4617faa0edf423621bb0b3b8707836687161210d470e69a4184be9ca011"},
{file = "pymongo-4.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:31e4d21201bdf15064cf47ce7b74722d3e1aea2597c6785882244a3bb58c7eab"}, {file = "pymongo-4.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7bd26b2aec8ceeb95a5d948d5cc0f62b0eb6d66f3f4230705c1e3d3d2c04ec76"},
{file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6b804bb4f2d9dc389cc9e827d579fa327272cdb0629a99bfe5b83cb3e269ebf"}, {file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb104c3c2a78d9d85571c8ac90ec4f95bca9b297c6eee5ada71fabf1129e1674"},
{file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f2fbdb87fe5075c8beb17a5c16348a1ea3c8b282a5cb72d173330be2fecf22f5"}, {file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4924355245a9c79f77b5cda2db36e0f75ece5faf9f84d16014c0a297f6d66786"},
{file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd39455b7ee70aabee46f7399b32ab38b86b236c069ae559e22be6b46b2bbfc4"}, {file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11280809e5dacaef4971113f0b4ff4696ee94cfdb720019ff4fa4f9635138252"},
{file = "pymongo-4.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:940d456774b17814bac5ea7fc28188c7a1338d4a233efbb6ba01de957bded2e8"}, {file = "pymongo-4.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5d55f2a82e5eb23795f724991cac2bffbb1c0f219c0ba3bf73a835f97f1bb2e"},
{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.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e974ab16a60be71a8dfad4e5afccf8dd05d41c758060f5d5bda9a758605d9a5d"},
{file = "pymongo-4.8.0-cp312-cp312-win32.whl", hash = "sha256:47ec8c3f0a7b2212dbc9be08d3bf17bc89abd211901093e3ef3f2adea7de7a69"}, {file = "pymongo-4.10.1-cp312-cp312-win32.whl", hash = "sha256:544890085d9641f271d4f7a47684450ed4a7344d6b72d5968bfae32203b1bb7c"},
{file = "pymongo-4.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84bc7707492f06fbc37a9f215374d2977d21b72e10a67f1b31893ec5a140ad8"}, {file = "pymongo-4.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:dcc07b1277e8b4bf4d7382ca133850e323b7ab048b8353af496d050671c7ac52"},
{file = "pymongo-4.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:519d1bab2b5e5218c64340b57d555d89c3f6c9d717cecbf826fb9d42415e7750"}, {file = "pymongo-4.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:90bc6912948dfc8c363f4ead54d54a02a15a7fee6cfafb36dc450fc8962d2cb7"},
{file = "pymongo-4.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87075a1feb1e602e539bdb1ef8f4324a3427eb0d64208c3182e677d2c0718b6f"}, {file = "pymongo-4.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:594dd721b81f301f33e843453638e02d92f63c198358e5a0fa8b8d0b1218dabc"},
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f53429515d2b3e86dcc83dadecf7ff881e538c168d575f3688698a8707b80a"}, {file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0783e0c8e95397c84e9cf8ab092ab1e5dd7c769aec0ef3a5838ae7173b98dea0"},
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdc20cd1e1141b04696ffcdb7c71e8a4a665db31fe72e51ec706b3bdd2d09f36"}, {file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fb6a72e88df46d1c1040fd32cd2d2c5e58722e5d3e31060a0393f04ad3283de"},
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:284d0717d1a7707744018b0b6ee7801b1b1ff044c42f7be7a01bb013de639470"}, {file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e3a593333e20c87415420a4fb76c00b7aae49b6361d2e2205b6fece0563bf40"},
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5bf0eb8b6ef40fa22479f09375468c33bebb7fe49d14d9c96c8fd50355188b0"}, {file = "pymongo-4.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72e2ace7456167c71cfeca7dcb47bd5dceda7db2231265b80fc625c5e8073186"},
{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.10.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ad05eb9c97e4f589ed9e74a00fcaac0d443ccd14f38d1258eb4c39a35dd722b"},
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0061af6e8c5e68b13f1ec9ad5251247726653c5af3c0bbdfbca6cf931e99216"}, {file = "pymongo-4.10.1-cp313-cp313-win32.whl", hash = "sha256:ee4c86d8e6872a61f7888fc96577b0ea165eb3bdb0d841962b444fa36001e2bb"},
{file = "pymongo-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:658d0170f27984e0d89c09fe5c42296613b711a3ffd847eb373b0dbb5b648d5f"}, {file = "pymongo-4.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:45ee87a4e12337353242bc758accc7fb47a2f2d9ecc0382a61e64c8f01e86708"},
{file = "pymongo-4.8.0-cp38-cp38-win32.whl", hash = "sha256:3ed1c316718a2836f7efc3d75b4b0ffdd47894090bc697de8385acd13c513a70"}, {file = "pymongo-4.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:442ca247f53ad24870a01e80a71cd81b3f2318655fd9d66748ee2bd1b1569d9e"},
{file = "pymongo-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:7148419eedfea9ecb940961cfe465efaba90595568a1fb97585fb535ea63fe2b"}, {file = "pymongo-4.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23e1d62df5592518204943b507be7b457fb8a4ad95a349440406fd42db5d0923"},
{file = "pymongo-4.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8400587d594761e5136a3423111f499574be5fd53cf0aefa0d0f05b180710b0"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6131bc6568b26e7495a9f3ef2b1700566b76bbecd919f4472bfe90038a61f425"},
{file = "pymongo-4.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af3e98dd9702b73e4e6fd780f6925352237f5dce8d99405ff1543f3771201704"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdeba88c540c9ed0338c0b2062d9f81af42b18d6646b3e6dda05cf6edd46ada9"},
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de3a860f037bb51f968de320baef85090ff0bbb42ec4f28ec6a5ddf88be61871"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a624d752dd3c89d10deb0ef6431559b6d074703cab90a70bb849ece02adc6b"},
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fc18b3a093f3db008c5fea0e980dbd3b743449eee29b5718bc2dc15ab5088bb"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba164e73fdade9b4614a2497321c5b7512ddf749ed508950bdecc28d8d76a2d9"},
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18c9d8f975dd7194c37193583fd7d1eb9aea0c21ee58955ecf35362239ff31ac"}, {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.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:408b2f8fdbeca3c19e4156f28fff1ab11c3efb0407b60687162d49f68075e63c"}, {file = "pymongo-4.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e4a65567bd17d19f03157c7ec992c6530eafd8191a4e5ede25566792c4fe3fa2"},
{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.10.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f1945d48fb9b8a87d515da07f37e5b2c35b364a435f534c122e92747881f4a7c"},
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d18d86bc9e103f4d3d4f18b85a0471c0e13ce5b79194e4a0389a224bb70edd53"}, {file = "pymongo-4.10.1-cp38-cp38-win32.whl", hash = "sha256:345f8d340802ebce509f49d5833cc913da40c82f2e0daf9f60149cacc9ca680f"},
{file = "pymongo-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9097c331577cecf8034422956daaba7ec74c26f7b255d718c584faddd7fa2e3c"}, {file = "pymongo-4.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:3a70d5efdc0387ac8cd50f9a5f379648ecfc322d14ec9e1ba8ec957e5d08c372"},
{file = "pymongo-4.8.0-cp39-cp39-win32.whl", hash = "sha256:d5428dbcd43d02f6306e1c3c95f692f68b284e6ee5390292242f509004c9e3a8"}, {file = "pymongo-4.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15b1492cc5c7cd260229590be7218261e81684b8da6d6de2660cf743445500ce"},
{file = "pymongo-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:ef7225755ed27bfdb18730c68f6cb023d06c28f2b734597480fb4c0e500feb6f"}, {file = "pymongo-4.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95207503c41b97e7ecc7e596d84a61f441b4935f11aa8332828a754e7ada8c82"},
{file = "pymongo-4.8.0.tar.gz", hash = "sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde"}, {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] [package.dependencies]
@ -1455,12 +1487,12 @@ dnspython = ">=1.16.0,<3.0.0"
[package.extras] [package.extras]
aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"] 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)"] 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.6.0,<2.0.0)"] encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.10.0,<2.0.0)"]
gssapi = ["pykerberos", "winkerberos (>=0.5.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)"] ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]
snappy = ["python-snappy"] snappy = ["python-snappy"]
test = ["pytest (>=7)"] test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"]
zstd = ["zstandard"] zstd = ["zstandard"]
[[package]] [[package]]
@ -1562,6 +1594,33 @@ files = [
[package.dependencies] [package.dependencies]
pyasn1 = ">=0.1.3" 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]] [[package]]
name = "s3transfer" name = "s3transfer"
version = "0.10.2" version = "0.10.2"
@ -1581,17 +1640,20 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"]
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "2.13.0" version = "2.15.0"
description = "Python client for Sentry (https://sentry.io)" description = "Python client for Sentry (https://sentry.io)"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "sentry_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6"}, {file = "sentry_sdk-2.15.0-py2.py3-none-any.whl", hash = "sha256:8fb0d1a4e1a640172f31502e4503543765a1fe8a9209779134a4ac52d4677303"},
{file = "sentry_sdk-2.13.0.tar.gz", hash = "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260"}, {file = "sentry_sdk-2.15.0.tar.gz", hash = "sha256:a599e7d3400787d6f43327b973e55a087b931ba2c592a7a7afa691f8eb5e75e2"},
] ]
[package.dependencies] [package.dependencies]
blinker = {version = ">=1.1", optional = true, markers = "extra == \"flask\""}
certifi = "*" certifi = "*"
flask = {version = ">=0.11", optional = true, markers = "extra == \"flask\""}
markupsafe = {version = "*", optional = true, markers = "extra == \"flask\""}
urllib3 = ">=1.26.11" urllib3 = ">=1.26.11"
[package.extras] [package.extras]
@ -1680,6 +1742,27 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.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]] [[package]]
name = "werkzeug" name = "werkzeug"
version = "3.0.4" version = "3.0.4"
@ -1779,4 +1862,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "d5642c7f707d2177d79620387be647b747e9eb9b0c588af1dc79f07f138c3dd7" content-hash = "7b9445a4aa93727bacd9911f2b79067c2cc40abf83b05b26fec1ce7329f92b97"

View File

@ -9,20 +9,24 @@ python = "^3.12"
flask = "^3.0.3" flask = "^3.0.3"
bcrypt = "^4.2.0" bcrypt = "^4.2.0"
pyjwt = "^2.9.0" pyjwt = "^2.9.0"
boto3 = "^1.35.4" boto3 = "^1.35.34"
flask-cors = "^4.0.1" flask-cors = "^5.0.0"
dnspython = "^2.6.1" dnspython = "^2.6.1"
requests = "^2.32.3" requests = "^2.32.3"
pymongo = "^4.8.0" pymongo = "^4.10.1"
flask_limiter = "^3.8.0" flask_limiter = "^3.8.0"
firebase-admin = "^4.3.0" firebase-admin = "^6.5.0"
blurhash-python = "^1.2.2" blurhash-python = "^1.2.2"
gunicorn = "^23.0.0" gunicorn = "^23.0.0"
sentry-sdk = "^2.13.0" sentry-sdk = {extras = ["flask"], version = "^2.15.0"}
pyOpenSSL = "^24.2.1" pyOpenSSL = "^24.2.1"
webargs = "^8.6.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.9"
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api" build-backend = "poetry.masonry.api"

View File

@ -1,12 +1,12 @@
import os import os
from pymongo import MongoClient from pymongo import MongoClient
from flask import g
db = None db = None
def get_db():
global db
if db is None: def get_db():
db = MongoClient(os.environ['MONGO_URL'])[os.environ['MONGO_DATABASE']] global db
return db
if db is None:
db = MongoClient(os.environ["MONGO_URL"])[os.environ["MONGO_DATABASE"]]
return db

View File

@ -2,33 +2,39 @@ import os
from threading import Thread from threading import Thread
import requests 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') def handle_send(data):
api_key = os.environ.get('MAILGUN_KEY') if "from" not in data:
if base_url and api_key: data["from"] = "{} <{}>".format(
auth = ('api', api_key) os.environ.get("APP_NAME"), os.environ.get("FROM_EMAIL")
try: )
response = requests.post(base_url, auth=auth, data=data) if "to_user" in data:
response.raise_for_status() user = data["to_user"]
except: data["to"] = user["username"] + " <" + user["email"] + ">"
print('Unable to send email') del data["to_user"]
else: data["text"] += (
print('Not sending email. Message pasted below.') "\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(
print(data) 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): def send(data):
thr = Thread(target=handle_send, args=[data]) thr = Thread(target=handle_send, args=[data])
thr.start() thr.start()

View File

@ -4,52 +4,63 @@ from firebase_admin import messaging
default_app = firebase_admin.initialize_app() 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. def handle_send_multiple(users, title, body, extra={}):
messages = list(map(lambda t: messaging.Message( tokens = []
notification=messaging.Notification(title, body), for user in users:
apns=messaging.APNSConfig( if user.get("pushToken"):
payload=messaging.APNSPayload( tokens.append(user["pushToken"])
aps=messaging.Aps(badge=1, sound='default'), if not tokens:
), return
),
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_multiple(users, title, body, extra = {}): # Create a list containing up to 500 messages.
thr = Thread(target=handle_send_multiple, args=[users, title, body, extra]) messages = list(
thr.start() 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') def send_multiple(users, title, body, extra={}):
if not token: return thr = Thread(target=handle_send_multiple, args=[users, title, body, extra])
message = messaging.Message( thr.start()
notification=messaging.Notification(
title = title,
body = body, def send_single(user, title, body, extra={}):
), token = user.get("pushToken")
apns=messaging.APNSConfig( if not token:
payload=messaging.APNSPayload( return
aps=messaging.Aps(badge=1, sound='default'), message = messaging.Message(
), notification=messaging.Notification(
), title=title,
data = extra, body=body,
token = token, ),
) apns=messaging.APNSConfig(
try: payload=messaging.APNSPayload(
response = messaging.send(message) aps=messaging.Aps(badge=1, sound="default"),
# Response is a message ID string. ),
print('Successfully sent message:', response) ),
except Exception as e: data=extra,
print('Error sending notification', str(e)) 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))

View File

@ -1,4 +1,5 @@
import json, datetime import json
import datetime
from flask import request, Response from flask import request, Response
import werkzeug import werkzeug
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
@ -10,93 +11,113 @@ from util import util
errors = werkzeug.exceptions errors = werkzeug.exceptions
def get_user(required = True):
headers = request.headers def get_user(required=True):
if not headers.get('Authorization') and required: headers = request.headers
raise util.errors.Unauthorized('This resource requires authentication') if not headers.get("Authorization") and required:
if headers.get('Authorization'): raise util.errors.Unauthorized("This resource requires authentication")
user = accounts.get_user_context(headers.get('Authorization').replace('Bearer ', '')) if headers.get("Authorization"):
if user is None and required: user = accounts.get_user_context(
raise util.errors.Unauthorized('Invalid token') headers.get("Authorization").replace("Bearer ", "")
return user )
return None if user is None and required:
raise util.errors.Unauthorized("Invalid token")
return user
return None
def limit_by_client(): def limit_by_client():
data = request.get_json() data = request.get_json()
if data: if data:
if data.get('email'): return data.get('email') if data.get("email"):
if data.get('token'): return data.get('token') return data.get("email")
return get_remote_address() if data.get("token"):
return data.get("token")
return get_remote_address()
def limit_by_user(): def limit_by_user():
user = util.get_user(required = False) user = util.get_user(required=False)
return user['_id'] if user else get_remote_address() return user["_id"] if user else get_remote_address()
def is_root(user): 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): def can_view_project(user, project):
if not project: return False if not project:
if project.get('visibility') == 'public': return False
return True if project.get("visibility") == "public":
if not user: return False return True
if project.get('visibility') == 'private' and can_edit_project(user, project): if not user:
return True return False
if set(user.get('groups', [])).intersection(project.get('groupVisibility', [])): if project.get("visibility") == "private" and can_edit_project(user, project):
return True return True
if 'root' in user.get('roles', []): return True if set(user.get("groups", [])).intersection(project.get("groupVisibility", [])):
return False return True
if "root" in user.get("roles", []):
return True
return False
def can_edit_project(user, project): def can_edit_project(user, project):
if not user or not project: return False if not user or not project:
return project.get('user') == user['_id'] or is_root(user) return False
return project.get("user") == user["_id"] or is_root(user)
def filter_keys(obj, allowed_keys): def filter_keys(obj, allowed_keys):
filtered = {} filtered = {}
for key in allowed_keys: for key in allowed_keys:
if key in obj: if key in obj:
filtered[key] = obj[key] filtered[key] = obj[key]
return filtered return filtered
def build_updater(obj, allowed_keys): def build_updater(obj, allowed_keys):
if not obj: return {} if not obj:
allowed = filter_keys(obj, allowed_keys) return {}
updater = {} allowed = filter_keys(obj, allowed_keys)
for key in allowed: updater = {}
if not allowed[key]: for key in allowed:
if '$unset' not in updater: updater['$unset'] = {} if not allowed[key]:
updater['$unset'][key] = '' if "$unset" not in updater:
else: updater["$unset"] = {}
if '$set' not in updater: updater['$set'] = {} updater["$unset"][key] = ""
updater['$set'][key] = allowed[key] else:
return updater if "$set" not in updater:
updater["$set"] = {}
updater["$set"][key] = allowed[key]
return updater
def generate_rsa_keypair(): def generate_rsa_keypair():
private_key = rsa.generate_private_key( private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
public_exponent=65537, private_pem = private_key.private_bytes(
key_size=4096 encoding=serialization.Encoding.PEM,
) format=serialization.PrivateFormat.PKCS8,
private_pem = private_key.private_bytes( encryption_algorithm=serialization.NoEncryption(),
encoding=serialization.Encoding.PEM, )
format=serialization.PrivateFormat.PKCS8, public_key = private_key.public_key()
encryption_algorithm=serialization.NoEncryption() public_pem = public_key.public_bytes(
) encoding=serialization.Encoding.PEM,
public_key = private_key.public_key() format=serialization.PublicFormat.SubjectPublicKeyInfo,
public_pem = public_key.public_bytes( )
encoding=serialization.Encoding.PEM, return private_pem, public_pem
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return private_pem, public_pem
class MongoJsonEncoder(json.JSONEncoder): class MongoJsonEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
if isinstance(obj, (datetime.datetime, datetime.date)): if isinstance(obj, (datetime.datetime, datetime.date)):
return obj.isoformat() return obj.isoformat()
elif isinstance(obj, ObjectId): elif isinstance(obj, ObjectId):
return str(obj) return str(obj)
return json.JSONEncoder.default(self, obj) return json.JSONEncoder.default(self, obj)
def jsonify(*args, **kwargs): def jsonify(*args, **kwargs):
resp_data = json.dumps(dict(*args, **kwargs), cls=MongoJsonEncoder) resp_data = json.dumps(dict(*args, **kwargs), cls=MongoJsonEncoder)
resp = Response(resp_data) resp = Response(resp_data)
resp.headers['Content-Type'] = 'application/json' resp.headers["Content-Type"] = "application/json"
return resp return resp

View File

@ -1,410 +1,523 @@
import io, time import io
import configparser import configparser
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
from api import uploads from api import uploads
def normalise_colour(max_color, triplet): def normalise_colour(max_color, triplet):
color_factor = 256/max_color color_factor = 256 / max_color
components = triplet.split(',') components = triplet.split(",")
new_components = [] new_components = []
for component in components: for component in components:
new_components.append(str(int(float(color_factor) * int(component)))) new_components.append(str(int(float(color_factor) * int(component))))
return ','.join(new_components) return ",".join(new_components)
def denormalise_colour(max_color, triplet): def denormalise_colour(max_color, triplet):
color_factor = max_color/256 color_factor = max_color / 256
components = triplet.split(',') components = triplet.split(",")
new_components = [] new_components = []
for component in components: for component in components:
new_components.append(str(int(float(color_factor) * int(component)))) new_components.append(str(int(float(color_factor) * int(component))))
return ','.join(new_components) return ",".join(new_components)
def colour_tuple(triplet): def colour_tuple(triplet):
if not triplet: return None if not triplet:
components = triplet.split(',') return None
return tuple(map(lambda c: int(c), components)) components = triplet.split(",")
return tuple(map(lambda c: int(c), components))
def darken_colour(c_tuple, val): def darken_colour(c_tuple, val):
def darken(c): def darken(c):
c = c * val c = c * val
if c < 0: c = 0 if c < 0:
if c > 255: c = 255 c = 0
return int(c) if c > 255:
return tuple(map(darken, c_tuple)) c = 255
return int(c)
return tuple(map(darken, c_tuple))
def get_colour_index(colours, colour): def get_colour_index(colours, colour):
for (index, c) in enumerate(colours): for index, c in enumerate(colours):
if c == colour: return index + 1 if c == colour:
return 1 return index + 1
return 1
def dumps(obj): def dumps(obj):
if not obj or not obj['pattern']: raise Exception('Invalid pattern') if not obj or not obj["pattern"]:
wif = [] raise Exception("Invalid pattern")
wif = []
wif.append('[WIF]') wif.append("[WIF]")
wif.append('Version=1.1') wif.append("Version=1.1")
wif.append('Source Program=Treadl') wif.append("Source Program=Treadl")
wif.append('Source Version=1') wif.append("Source Version=1")
wif.append('\n[CONTENTS]') wif.append("\n[CONTENTS]")
wif.append('COLOR PALETTE=true') wif.append("COLOR PALETTE=true")
wif.append('TEXT=true') wif.append("TEXT=true")
wif.append('WEAVING=true') wif.append("WEAVING=true")
wif.append('WARP=true') wif.append("WARP=true")
wif.append('WARP COLORS=true') wif.append("WARP COLORS=true")
wif.append('WEFT COLORS=true') wif.append("WEFT COLORS=true")
wif.append('WEFT=true') wif.append("WEFT=true")
wif.append('COLOR TABLE=true') wif.append("COLOR TABLE=true")
wif.append('THREADING=true') wif.append("THREADING=true")
wif.append('TIEUP=true') wif.append("TIEUP=true")
wif.append('TREADLING=true') wif.append("TREADLING=true")
wif.append('\n[TEXT]') wif.append("\n[TEXT]")
wif.append('Title={0}'.format(obj['name'])) wif.append("Title={0}".format(obj["name"]))
wif.append('\n[COLOR TABLE]') wif.append("\n[COLOR TABLE]")
for (index, colour) in enumerate(obj['pattern']['colours']): for index, colour in enumerate(obj["pattern"]["colours"]):
wif.append('{0}={1}'.format(index + 1, denormalise_colour(999, colour))) wif.append("{0}={1}".format(index + 1, denormalise_colour(999, colour)))
wif.append('\n[COLOR PALETTE]') wif.append("\n[COLOR PALETTE]")
wif.append('Range=0,999') wif.append("Range=0,999")
wif.append('Entries={0}'.format(len(obj['pattern']['colours']))) wif.append("Entries={0}".format(len(obj["pattern"]["colours"])))
wif.append('\n[WEAVING]') wif.append("\n[WEAVING]")
wif.append('Rising Shed=true') wif.append("Rising Shed=true")
wif.append('Treadles={0}'.format(obj['pattern']['weft']['treadles'])) wif.append("Treadles={0}".format(obj["pattern"]["weft"]["treadles"]))
wif.append('Shafts={0}'.format(obj['pattern']['warp']['shafts'])) wif.append("Shafts={0}".format(obj["pattern"]["warp"]["shafts"]))
wif.append('\n[WARP]') wif.append("\n[WARP]")
wif.append('Units=centimeters') wif.append("Units=centimeters")
wif.append('Color={0}'.format(get_colour_index(obj['pattern']['colours'], obj['pattern']['warp']['defaultColour']))) wif.append(
wif.append('Threads={0}'.format(len(obj['pattern']['warp']['threading']))) "Color={0}".format(
wif.append('Spacing=0.212') get_colour_index(
wif.append('Thickness=0.212') 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[WARP COLORS]') wif.append("\n[WARP COLORS]")
for (index, thread) in enumerate(obj['pattern']['warp']['threading']): for index, thread in enumerate(obj["pattern"]["warp"]["threading"]):
if 'colour' in thread: if "colour" in thread:
wif.append('{0}={1}'.format(index + 1, get_colour_index(obj['pattern']['colours'], thread['colour']))) wif.append(
"{0}={1}".format(
index + 1,
get_colour_index(obj["pattern"]["colours"], thread["colour"]),
)
)
wif.append('\n[THREADING]') wif.append("\n[THREADING]")
for (index, thread) in enumerate(obj['pattern']['warp']['threading']): for index, thread in enumerate(obj["pattern"]["warp"]["threading"]):
wif.append('{0}={1}'.format(index + 1, thread['shaft'])) wif.append("{0}={1}".format(index + 1, thread["shaft"]))
wif.append('\n[WEFT]') wif.append("\n[WEFT]")
wif.append('Units=centimeters') wif.append("Units=centimeters")
wif.append('Color={0}'.format(get_colour_index(obj['pattern']['colours'], obj['pattern']['weft']['defaultColour']))) wif.append(
wif.append('Threads={0}'.format(len(obj['pattern']['weft']['treadling']))) "Color={0}".format(
wif.append('Spacing=0.212') get_colour_index(
wif.append('Thickness=0.212') 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[WEFT COLORS]') wif.append("\n[WEFT COLORS]")
for (index, thread) in enumerate(obj['pattern']['weft']['treadling']): for index, thread in enumerate(obj["pattern"]["weft"]["treadling"]):
if 'colour' in thread: if "colour" in thread:
wif.append('{0}={1}'.format(index + 1, get_colour_index(obj['pattern']['colours'], thread['colour']))) wif.append(
"{0}={1}".format(
index + 1,
get_colour_index(obj["pattern"]["colours"], thread["colour"]),
)
)
wif.append('\n[TREADLING]') wif.append("\n[TREADLING]")
for (index, thread) in enumerate(obj['pattern']['weft']['treadling']): for index, thread in enumerate(obj["pattern"]["weft"]["treadling"]):
wif.append('{0}={1}'.format(index + 1, thread['treadle'])) wif.append("{0}={1}".format(index + 1, thread["treadle"]))
wif.append('\n[TIEUP]') wif.append("\n[TIEUP]")
for (index, tieup) in enumerate(obj['pattern']['tieups']): 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("{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): def loads(wif_file):
config = configparser.ConfigParser(allow_no_value=True, strict=False) config = configparser.ConfigParser(allow_no_value=True, strict=False)
config.read_string(wif_file.lower()) config.read_string(wif_file.lower())
DEFAULT_TITLE = 'Untitled Pattern' DEFAULT_TITLE = "Untitled Pattern"
draft = {} draft = {}
if 'text' in config: if "text" in config:
text = config['text'] text = config["text"]
draft['name'] = text.get('title') or DEFAULT_TITLE draft["name"] = text.get("title") or DEFAULT_TITLE
if not draft.get('name'): if not draft.get("name"):
draft['name'] = DEFAULT_TITLE draft["name"] = DEFAULT_TITLE
min_color = 0 max_color = 255
max_color = 255 if "color palette" in config:
if 'color palette' in config: color_palette = config["color palette"]
color_palette = config['color palette'] color_range = color_palette.get("range").split(",")
color_range = color_palette.get('range').split(',') max_color = int(color_range[1])
min_color = int(color_range[0])
max_color = int(color_range[1])
if 'color table' in config: if "color table" in config:
color_table = config['color table'] color_table = config["color table"]
draft['colours'] = [None]*len(color_table) draft["colours"] = [None] * len(color_table)
for x in color_table: for x in color_table:
draft['colours'][int(x)-1] = normalise_colour(max_color, color_table[x]) draft["colours"][int(x) - 1] = normalise_colour(max_color, color_table[x])
if not draft.get('colours'): draft['colours'] = [] if not draft.get("colours"):
if len(draft['colours']) < 2: draft["colours"] = []
draft['colours'] += [normalise_colour(255, '255,255,255'), normalise_colour(255, '0,0,255')] 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'] threading = config["threading"]
warp = config['warp'] warp = config["warp"]
draft['warp'] = {} draft["warp"] = {}
draft['warp']['shafts'] = weaving.getint('shafts') draft["warp"]["shafts"] = weaving.getint("shafts")
draft['warp']['threading'] = [] draft["warp"]["threading"] = []
if warp.get("color"):
warp_colour_index = warp.getint("color") - 1
draft["warp"]["defaultColour"] = draft["colours"][warp_colour_index]
if warp.get('color'): else:
warp_colour_index = warp.getint('color') - 1 # In case of no color table or colour index out of bounds
draft['warp']['defaultColour'] = draft['colours'][warp_colour_index] draft["warp"]["defaultColour"] = draft["colours"][0]
else: for x in threading:
# In case of no color table or colour index out of bounds shaft = threading[x]
draft['warp']['defaultColour'] = draft['colours'][0] if "," in shaft:
shaft = shaft.split(",")[0]
for x in threading: shaft = int(shaft)
shaft = threading[x] while int(x) >= len(draft["warp"]["threading"]) - 1:
if ',' in shaft: draft["warp"]["threading"].append({"shaft": 0})
shaft = shaft.split(",")[0] draft["warp"]["threading"][int(x) - 1] = {"shaft": shaft}
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
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(',')
try: try:
draft['tieups'][int(x)-1] = [int(i) for i in split] warp_colours = config["warp colors"]
except: for x in warp_colours:
draft['tieups'][int(x)-1] = [] 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): def generate_images(obj):
try: try:
return { return {
'preview': draw_image(obj), "preview": draw_image(obj),
'fullPreview': draw_image(obj, with_plan=True) "fullPreview": draw_image(obj, with_plan=True),
} }
except Exception as e: except Exception as e:
print(e) print(e)
return {} return {}
def draw_image(obj, with_plan=False): def draw_image(obj, with_plan=False):
if not obj or not obj['pattern']: raise Exception('Invalid pattern') if not obj or not obj["pattern"]:
BASE_SIZE = 10 raise Exception("Invalid pattern")
pattern = obj['pattern'] BASE_SIZE = 10
warp = pattern['warp'] pattern = obj["pattern"]
weft = pattern['weft'] warp = pattern["warp"]
tieups = pattern['tieups'] 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_width = (
full_height = warp['shafts'] * BASE_SIZE + len(weft['treadling']) * BASE_SIZE + BASE_SIZE * 2 if with_plan else len(weft['treadling']) * BASE_SIZE 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_top = 0
warp_left = 0 warp_left = 0
warp_right = len(warp['threading']) * BASE_SIZE warp_right = len(warp["threading"]) * BASE_SIZE
warp_bottom = warp['shafts'] * BASE_SIZE + BASE_SIZE warp_bottom = warp["shafts"] * BASE_SIZE + BASE_SIZE
weft_left = warp_right + BASE_SIZE weft_left = warp_right + BASE_SIZE
weft_top = warp['shafts'] * BASE_SIZE + BASE_SIZE * 2 weft_top = warp["shafts"] * BASE_SIZE + BASE_SIZE * 2
weft_right = warp_right + BASE_SIZE + weft['treadles'] * BASE_SIZE + BASE_SIZE weft_right = warp_right + BASE_SIZE + weft["treadles"] * BASE_SIZE + BASE_SIZE
weft_bottom = weft_top + len(weft['treadling']) * BASE_SIZE weft_bottom = weft_top + len(weft["treadling"]) * BASE_SIZE
tieup_left = warp_right + BASE_SIZE tieup_left = warp_right + BASE_SIZE
tieup_top = BASE_SIZE tieup_top = BASE_SIZE
tieup_right = tieup_left + weft['treadles'] * BASE_SIZE tieup_right = tieup_left + weft["treadles"] * BASE_SIZE
tieup_bottom = warp_bottom tieup_bottom = warp_bottom
drawdown_top = warp_bottom + BASE_SIZE if with_plan else 0 drawdown_top = warp_bottom + BASE_SIZE if with_plan else 0
drawdown_right = warp_right if with_plan else full_width drawdown_right = warp_right if with_plan else full_width
drawdown_left = warp_left if with_plan else 0 drawdown_left = warp_left if with_plan else 0
drawdown_bottom = weft_bottom if with_plan else full_height drawdown_bottom = weft_bottom if with_plan else full_height
WHITE=(255,255,255) WHITE = (255, 255, 255)
GREY = (150,150,150) GREY = (150, 150, 150)
BLACK = (0,0,0) BLACK = (0, 0, 0)
img = Image.new("RGBA", (full_width, full_height), WHITE) img = Image.new("RGBA", (full_width, full_height), WHITE)
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
# Draw warp # Draw warp
if with_plan: if with_plan:
draw.rectangle([ draw.rectangle(
(warp_left, warp_top), [(warp_left, warp_top), (warp_right, warp_bottom)],
(warp_right, warp_bottom) fill=None,
], fill=None, outline=GREY, width=1) outline=GREY,
for y in range(1, warp['shafts'] + 1): width=1,
ycoord = y * BASE_SIZE )
draw.line([ for y in range(1, warp["shafts"] + 1):
(warp_left, ycoord), ycoord = y * BASE_SIZE
(warp_right, ycoord), draw.line(
], [
fill=GREY, width=1, joint=None) (warp_left, ycoord),
for (i, x) in enumerate(range(len(warp['threading'])-1, 0, -1)): (warp_right, ycoord),
thread = warp['threading'][i] ],
xcoord = x * BASE_SIZE fill=GREY,
draw.line([ width=1,
(xcoord, warp_top), joint=None,
(xcoord, warp_bottom), )
], for i, x in enumerate(range(len(warp["threading"]) - 1, 0, -1)):
fill=GREY, width=1, joint=None) thread = warp["threading"][i]
if thread.get('shaft', 0) > 0: xcoord = x * BASE_SIZE
ycoord = warp_bottom - (thread['shaft'] * BASE_SIZE) draw.line(
draw.rectangle([ [
(xcoord, ycoord), (xcoord, warp_top),
(xcoord + BASE_SIZE, ycoord + BASE_SIZE) (xcoord, warp_bottom),
], fill=BLACK, outline=None, width=1) ],
colour = warp['defaultColour'] fill=GREY,
if thread and thread.get('colour'): width=1,
colour = thread['colour'] joint=None,
draw.rectangle([ )
(xcoord, warp_top), if thread.get("shaft", 0) > 0:
(xcoord + BASE_SIZE, warp_top + BASE_SIZE), ycoord = warp_bottom - (thread["shaft"] * BASE_SIZE)
], fill=colour_tuple(colour)) 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 weft
draw.rectangle([ draw.rectangle(
(weft_left, weft_top), [(weft_left, weft_top), (weft_right, weft_bottom)],
(weft_right, weft_bottom) fill=None,
], fill=None, outline=GREY, width=1) outline=GREY,
for x in range(1, weft['treadles'] + 1): width=1,
xcoord = weft_left + x * BASE_SIZE )
draw.line([ for x in range(1, weft["treadles"] + 1):
(xcoord, weft_top), xcoord = weft_left + x * BASE_SIZE
(xcoord, weft_bottom), draw.line(
], [
fill=GREY, width=1, joint=None) (xcoord, weft_top),
for (i, y) in enumerate(range(0, len(weft['treadling']))): (xcoord, weft_bottom),
thread = weft['treadling'][i] ],
ycoord = weft_top + y * BASE_SIZE fill=GREY,
draw.line([ width=1,
(weft_left, ycoord), joint=None,
(weft_right, ycoord), )
], for i, y in enumerate(range(0, len(weft["treadling"]))):
fill=GREY, width=1, joint=None) thread = weft["treadling"][i]
if thread.get('treadle', 0) > 0: ycoord = weft_top + y * BASE_SIZE
xcoord = weft_left + (thread['treadle'] - 1) * BASE_SIZE draw.line(
draw.rectangle([ [
(xcoord, ycoord), (weft_left, ycoord),
(xcoord + BASE_SIZE, ycoord + BASE_SIZE) (weft_right, ycoord),
], fill=BLACK, outline=None, width=1) ],
colour = weft['defaultColour'] fill=GREY,
if thread and thread.get('colour'): width=1,
colour = thread['colour'] joint=None,
draw.rectangle([ )
(weft_right - BASE_SIZE, ycoord), if thread.get("treadle", 0) > 0:
(weft_right, ycoord + BASE_SIZE), xcoord = weft_left + (thread["treadle"] - 1) * BASE_SIZE
], fill=colour_tuple(colour)) 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 tieups # Draw tieups
draw.rectangle([ draw.rectangle(
(tieup_left, tieup_top), [(tieup_left, tieup_top), (tieup_right, tieup_bottom)],
(tieup_right, tieup_bottom) fill=None,
], fill=None, outline=GREY, width=1) outline=GREY,
for y in range(1, warp['shafts'] + 1): width=1,
ycoord = y * BASE_SIZE )
draw.line([ for y in range(1, warp["shafts"] + 1):
(tieup_left, ycoord), ycoord = y * BASE_SIZE
(tieup_right, ycoord), draw.line(
], [
fill=GREY, width=1, joint=None) (tieup_left, ycoord),
for (x, tieup) in enumerate(tieups): (tieup_right, ycoord),
xcoord = tieup_left + x * BASE_SIZE ],
draw.line([ fill=GREY,
(xcoord, tieup_top), width=1,
(xcoord, tieup_bottom), joint=None,
], )
fill=GREY, width=1, joint=None) for x, tieup in enumerate(tieups):
for entry in tieup: xcoord = tieup_left + x * BASE_SIZE
if entry > 0: draw.line(
ycoord = tieup_bottom - (entry * BASE_SIZE) [
draw.rectangle([ (xcoord, tieup_top),
(xcoord, ycoord), (xcoord, tieup_bottom),
(xcoord + BASE_SIZE, ycoord + BASE_SIZE) ],
], fill=BLACK, outline=None, width=1) 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 drawdown # Draw drawdown
draw.rectangle([ draw.rectangle(
(drawdown_left, drawdown_top), [(drawdown_left, drawdown_top), (drawdown_right, drawdown_bottom)],
(drawdown_right, drawdown_bottom) fill=None,
], fill=None, outline=(0,0,0), width=1) outline=(0, 0, 0),
for (y, weft_thread) in enumerate(weft['treadling']): width=1,
for (x, warp_thread) in enumerate(warp['threading']): )
# Ensure selected treadle and shaft is within configured pattern range for y, weft_thread in enumerate(weft["treadling"]):
treadle = 0 if weft_thread['treadle'] > weft['treadles'] else weft_thread['treadle'] for x, warp_thread in enumerate(warp["threading"]):
shaft = 0 if warp_thread['shaft'] > warp['shafts'] else warp_thread['shaft'] # 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"]
# Work out if should be warp or weft in "front" # Work out if should be warp or weft in "front"
tieup = tieups[treadle-1] if treadle > 0 else [] tieup = tieups[treadle - 1] if treadle > 0 else []
tieup = [t for t in tieup if t <= warp['shafts']] tieup = [t for t in tieup if t <= warp["shafts"]]
thread_type = 'warp' if shaft in tieup else 'weft' thread_type = "warp" if shaft in tieup else "weft"
# Calculate current colour # Calculate current colour
weft_colour = weft_thread.get('colour') or weft.get('defaultColour') weft_colour = weft_thread.get("colour") or weft.get("defaultColour")
warp_colour = warp_thread.get('colour') or warp.get('defaultColour') warp_colour = warp_thread.get("colour") or warp.get("defaultColour")
colour = colour_tuple(warp_colour if thread_type == 'warp' else weft_colour) colour = colour_tuple(warp_colour if thread_type == "warp" else weft_colour)
# Calculate drawdown coordinates # Calculate drawdown coordinates
x1 = drawdown_right - (x + 1) * BASE_SIZE x1 = drawdown_right - (x + 1) * BASE_SIZE
x2 = drawdown_right - x * BASE_SIZE x2 = drawdown_right - x * BASE_SIZE
y1 = drawdown_top + y * BASE_SIZE y1 = drawdown_top + y * BASE_SIZE
y2 = drawdown_top + (y + 1) * BASE_SIZE y2 = drawdown_top + (y + 1) * BASE_SIZE
# Draw the thread, with shadow # 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] 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': if thread_type == "warp":
for (i, grad_x) in enumerate(range(x1, x2)): for i, grad_x in enumerate(range(x1, x2)):
draw.line([ draw.line(
(grad_x, y1), (grad_x, y2), [
], (grad_x, y1),
fill=(darken_colour(colour, d[i])), width=1, joint=None) (grad_x, y2),
else: ],
for (i, grad_y) in enumerate(range(y1, y2)): fill=(darken_colour(colour, d[i])),
draw.line([ width=1,
(x1, grad_y), (x2, grad_y), joint=None,
], )
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() in_mem_file = io.BytesIO()
img.save(in_mem_file, 'PNG') img.save(in_mem_file, "PNG")
in_mem_file.seek(0) in_mem_file.seek(0)
file_name = 'preview-{0}_{1}.png'.format( file_name = "preview-{0}_{1}.png".format(
'full' if with_plan else 'base', obj['_id'] "full" if with_plan else "base", obj["_id"]
) )
path = 'projects/{}/{}'.format(obj['project'], file_name) path = "projects/{}/{}".format(obj["project"], file_name)
uploads.upload_file(path, in_mem_file) uploads.upload_file(path, in_mem_file)
return file_name return file_name

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.

Some files were not shown because too many files have changed in this diff Show More