Merge branch 'prealpha' into main

This commit is contained in:
tonyrewin 2022-12-02 11:51:53 +03:00
commit 50b2ae4154
19 changed files with 221 additions and 210 deletions

View File

@ -9,7 +9,7 @@ from starlette.requests import HTTPConnection
from auth.credentials import AuthCredentials, AuthUser
from base.orm import local_session
from orm.user import User, Role, UserRole
from orm.user import User, Role
from settings import SESSION_TOKEN_HEADER
from auth.tokenstorage import SessionToken
@ -39,12 +39,14 @@ class JWTAuthenticate(AuthenticationBackend):
user = None
with local_session() as session:
try:
q = select(
User
).filter(
User.id == payload.user_id
).select_from(User)
user = session.execute(q).unique().one()
user = (
session.query(User).options(
joinedload(User.roles).options(joinedload(Role.permissions)),
joinedload(User.ratings)
).filter(
User.id == payload.user_id
).one()
)
except exc.NoResultFound:
user = None
@ -59,7 +61,7 @@ class JWTAuthenticate(AuthenticationBackend):
scopes=scopes,
logged_in=True
),
user,
AuthUser(user_id=user.id),
)
else:
InvalidToken("please try again")

View File

@ -10,7 +10,7 @@ lang_subject = {
}
async def send_auth_email(user, token, template="email_confirmation", lang="ru"):
async def send_auth_email(user, token, lang="ru", template="email_confirmation"):
try:
to = "%s <%s>" % (user.name, user.email)
if lang not in ['ru', 'en']:

View File

@ -41,6 +41,7 @@ async def start_up():
async def dev_start_up():
if exists(DEV_SERVER_STATUS_FILE_NAME):
await redis.connect()
return
else:
with open(DEV_SERVER_STATUS_FILE_NAME, 'w', encoding='utf-8') as f:
@ -71,6 +72,7 @@ app.mount("/", GraphQL(schema, debug=True))
dev_app = app = Starlette(
debug=True,
on_startup=[dev_start_up],
on_shutdown=[shutdown],
middleware=middleware,
routes=routes,
)

View File

@ -9,7 +9,6 @@ from orm.shout import ShoutReactionsFollower
from orm.topic import TopicFollower
from orm.user import User
from orm.shout import Shout
# from services.stat.reacted import ReactedStorage
ts = datetime.now(tz=timezone.utc)
@ -84,7 +83,6 @@ def migrate_ratings(session, entry, reaction_dict):
)
session.add(following2)
session.add(rr)
# await ReactedStorage.react(rr)
except Exception as e:
print("[migration] comment rating error: %r" % re_reaction_dict)

View File

@ -9,7 +9,6 @@ from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutTopic, ShoutReactionsFollower
from orm.user import User
from orm.topic import TopicFollower, Topic
# from services.stat.reacted import ReactedStorage
from services.stat.viewed import ViewedStorage
import re
@ -365,7 +364,6 @@ async def content_ratings_to_reactions(entry, slug):
else:
rea = Reaction.create(**reaction_dict)
session.add(rea)
# await ReactedStorage.react(rea)
# shout_dict['ratings'].append(reaction_dict)
session.commit()

View File

@ -35,11 +35,12 @@ def migrate(entry):
slug = entry["profile"].get("path").lower()
slug = re.sub('[^0-9a-zA-Z]+', '-', slug).strip()
user_dict["slug"] = slug
bio = BeautifulSoup(entry.get("profile").get("bio") or "", features="lxml").text
if bio.startswith('<'):
print('[migration] bio! ' + bio)
bio = BeautifulSoup(bio, features="lxml").text
bio = bio.replace('\(', '(').replace('\)', ')')
bio = (entry.get("profile", {"bio": ""}).get("bio") or "").replace('\(', '(').replace('\)', ')')
bio_html = BeautifulSoup(bio, features="lxml").text
if bio == bio_html:
user_dict["bio"] = bio
else:
user_dict["about"] = bio
# userpic
try:

View File

@ -56,7 +56,8 @@ class User(Base):
email = Column(String, unique=True, nullable=False, comment="Email")
username = Column(String, nullable=False, comment="Login")
password = Column(String, nullable=True, comment="Password")
bio = Column(String, nullable=True, comment="Bio")
bio = Column(String, nullable=True, comment="Bio") # status description
about = Column(String, nullable=True, comment="About") # long and formatted
userpic = Column(String, nullable=True, comment="Userpic")
name = Column(String, nullable=True, comment="Display name")
slug = Column(String, unique=True, comment="User's slug")
@ -100,7 +101,7 @@ class User(Base):
session.commit()
User.default_user = default
async def get_permission(self):
def get_permission(self):
scope = {}
for role in self.roles:
for p in role.permissions:

View File

@ -8,6 +8,7 @@ from starlette.responses import RedirectResponse
from transliterate import translit
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from auth.email import send_auth_email
from auth.identity import Identity, Password
from auth.jwtcodec import JWTCodec
@ -24,20 +25,19 @@ from settings import SESSION_TOKEN_HEADER, FRONTEND_URL
@mutation.field("getSession")
@login_required
async def get_current_user(_, info):
user = info.context["request"].user
token = info.context["request"].headers.get("Authorization")
if user and token:
auth: AuthCredentials = info.context["request"].auth
token = info.context["request"].headers.get(SESSION_TOKEN_HEADER)
with local_session() as session:
user = session.query(User).where(User.id == auth.user_id).one()
user.lastSeen = datetime.now(tz=timezone.utc)
with local_session() as session:
session.add(user)
session.commit()
return {
"token": token,
"user": user,
"news": await user_subscriptions(user.slug),
}
else:
raise Unauthorized("No session token present in request, try to login")
session.commit()
return {
"token": token,
"user": user,
"news": await user_subscriptions(user.id),
}
@mutation.field("confirmEmail")
@ -58,7 +58,7 @@ async def confirm_email(_, info, token):
return {
"token": session_token,
"user": user,
"news": await user_subscriptions(user.slug)
"news": await user_subscriptions(user.id)
}
except InvalidToken as e:
raise InvalidToken(e.message)
@ -174,7 +174,7 @@ async def login(_, info, email: str, password: str = "", lang: str = "ru"):
return {
"token": session_token,
"user": user,
"news": await user_subscriptions(user.slug),
"news": await user_subscriptions(user.id),
}
except InvalidPassword:
print(f"[auth] {email}: invalid password")

View File

@ -1,4 +1,5 @@
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.orm import local_session
from base.resolvers import query, mutation
from base.exceptions import ObjectNotExist, BaseHttpException
@ -10,26 +11,28 @@ from orm.user import User
@query.field("getCollabs")
@login_required
async def get_collabs(_, info):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
collabs = session.query(Collab).filter(user.slug in Collab.authors)
collabs = session.query(Collab).filter(auth.user_id in Collab.authors)
return collabs
@mutation.field("inviteCoauthor")
@login_required
async def invite_coauthor(_, info, author: str, shout: int):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
s = session.query(Shout).where(Shout.id == shout).one()
if not s:
raise ObjectNotExist("invalid shout id")
else:
c = session.query(Collab).where(Collab.shout == shout).one()
if user.slug not in c.authors:
if auth.user_id not in c.authors:
raise BaseHttpException("you are not in authors list")
else:
invited_user = session.query(User).where(User.slug == author).one()
invited_user = session.query(User).where(User.id == author).one()
c.invites.append(invited_user)
session.add(c)
session.commit()
@ -41,16 +44,17 @@ async def invite_coauthor(_, info, author: str, shout: int):
@mutation.field("removeCoauthor")
@login_required
async def remove_coauthor(_, info, author: str, shout: int):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
s = session.query(Shout).where(Shout.id == shout).one()
if not s:
raise ObjectNotExist("invalid shout id")
if user.slug != s.createdBy.slug:
raise BaseHttpException("only onwer can remove coauthors")
if auth.user_id != s.createdBy:
raise BaseHttpException("only owner can remove coauthors")
else:
c = session.query(Collab).where(Collab.shout == shout).one()
ca = session.query(CollabAuthor).where(c.shout == shout, c.author == author).one()
ca = session.query(CollabAuthor).join(User).where(c.shout == shout, User.slug == author).one()
session.remve(ca)
c.invites = filter(lambda x: x.slug == author, c.invites)
c.authors = filter(lambda x: x.slug == author, c.authors)
@ -64,14 +68,15 @@ async def remove_coauthor(_, info, author: str, shout: int):
@mutation.field("acceptCoauthor")
@login_required
async def accept_coauthor(_, info, shout: int):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
s = session.query(Shout).where(Shout.id == shout).one()
if not s:
raise ObjectNotExist("invalid shout id")
else:
c = session.query(Collab).where(Collab.shout == shout).one()
accepted = filter(lambda x: x.slug == user.slug, c.invites).pop()
accepted = filter(lambda x: x.id == auth.user_id, c.invites).pop()
if accepted:
c.authors.append(accepted)
s.authors.append(accepted)

View File

@ -3,6 +3,7 @@ from datetime import datetime, timezone
from sqlalchemy import and_
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.orm import local_session
from base.resolvers import mutation
from orm.rbac import Resource
@ -19,7 +20,7 @@ from orm.collab import Collab
@mutation.field("createShout")
@login_required
async def create_shout(_, info, inp):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
topic_slugs = inp.get("topic_slugs", [])
if topic_slugs:
@ -37,24 +38,24 @@ async def create_shout(_, info, inp):
"mainTopic": inp.get("topics", []).pop(),
"visibility": "authors"
})
authors.remove(user.slug)
authors.remove(auth.user_id)
if authors:
chat = create_chat(None, info, new_shout.title, members=authors)
# create a cooperative chatroom
MessagesStorage.register_chat(chat)
await MessagesStorage.register_chat(chat)
# now we should create a collab
new_collab = Collab.create({
"shout": new_shout.id,
"authors": [user.slug, ],
"authors": [auth.user_id, ],
"invites": authors
})
session.add(new_collab)
# NOTE: shout made by one first author
sa = ShoutAuthor.create(shout=new_shout.id, user=user.id)
sa = ShoutAuthor.create(shout=new_shout.id, user=auth.user_id)
session.add(sa)
reactions_follow(user, new_shout.slug, True)
reactions_follow(auth.user_id, new_shout.slug, True)
if "mainTopic" in inp:
topic_slugs.append(inp["mainTopic"])
@ -65,11 +66,11 @@ async def create_shout(_, info, inp):
st = ShoutTopic.create(shout=new_shout.id, topic=topic.id)
session.add(st)
tf = session.query(TopicFollower).where(
and_(TopicFollower.follower == user.id, TopicFollower.topic == topic.id)
and_(TopicFollower.follower == auth.user_id, TopicFollower.topic == topic.id)
)
if not tf:
tf = TopicFollower.create(follower=user.id, topic=topic.id, auto=True)
tf = TopicFollower.create(follower=auth.user_id, topic=topic.id, auto=True)
session.add(tf)
new_shout.topic_slugs = topic_slugs
@ -77,7 +78,8 @@ async def create_shout(_, info, inp):
session.commit()
GitTask(inp, user.username, user.email, "new shout %s" % new_shout.slug)
# TODO
# GitTask(inp, user.username, user.email, "new shout %s" % new_shout.slug)
return {"shout": new_shout}
@ -85,18 +87,17 @@ async def create_shout(_, info, inp):
@mutation.field("updateShout")
@login_required
async def update_shout(_, info, inp):
auth = info.context["request"].auth
user_id = auth.user_id
auth: AuthCredentials = info.context["request"].auth
slug = inp["slug"]
with local_session() as session:
user = session.query(User).filter(User.id == user_id).first()
user = session.query(User).filter(User.id == auth.user_id).first()
shout = session.query(Shout).filter(Shout.slug == slug).first()
if not shout:
return {"error": "shout not found"}
authors = [author.id for author in shout.authors]
if user_id not in authors:
if auth.user_id not in authors:
scopes = auth.scopes
print(scopes)
if Resource.shout not in scopes:
@ -115,7 +116,7 @@ async def update_shout(_, info, inp):
ShoutTopic.create(shout=slug, topic=topic)
session.commit()
GitTask(inp, user.username, user.email, "update shout %s" % (slug))
GitTask(inp, user.username, user.email, "update shout %s" % slug)
return {"shout": shout}
@ -123,18 +124,17 @@ async def update_shout(_, info, inp):
@mutation.field("deleteShout")
@login_required
async def delete_shout(_, info, slug):
auth = info.context["request"].auth
user_id = auth.user_id
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
shout = session.query(Shout).filter(Shout.slug == slug).first()
authors = [a.id for a in shout.authors]
if not shout:
return {"error": "invalid shout slug"}
if user_id not in authors:
if auth.user_id not in authors:
return {"error": "access denied"}
for a in authors:
reactions_unfollow(a.slug, slug, True)
reactions_unfollow(a.id, slug)
shout.deletedAt = datetime.now(tz=timezone.utc)
session.add(shout)
session.commit()

View File

@ -3,6 +3,7 @@ import uuid
from datetime import datetime, timezone
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.redis import redis
from base.resolvers import mutation
@ -18,7 +19,7 @@ async def update_chat(_, info, chat_new: dict):
:param chat_new: dict with chat data
:return: Result { error chat }
"""
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
chat_id = chat_new["id"]
chat = await redis.execute("GET", f"chats/{chat_id}")
if not chat:
@ -26,7 +27,9 @@ async def update_chat(_, info, chat_new: dict):
"error": "chat not exist"
}
chat = dict(json.loads(chat))
if user.slug in chat["admins"]:
# TODO
if auth.user_id in chat["admins"]:
chat.update({
"title": chat_new.get("title", chat["title"]),
"description": chat_new.get("description", chat["description"]),
@ -46,10 +49,11 @@ async def update_chat(_, info, chat_new: dict):
@mutation.field("createChat")
@login_required
async def create_chat(_, info, title="", members=[]):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
chat = {}
if user.slug not in members:
members.append(user.slug)
if auth.user_id not in members:
members.append(auth.user_id)
# reuse chat craeted before if exists
if len(members) == 2 and title == "":
@ -73,7 +77,7 @@ async def create_chat(_, info, title="", members=[]):
"id": chat_id,
"users": members,
"title": title,
"createdBy": user.slug,
"createdBy": auth.user_id,
"createdAt": int(datetime.now(tz=timezone.utc).timestamp()),
"updatedAt": int(datetime.now(tz=timezone.utc).timestamp()),
"admins": []
@ -93,13 +97,14 @@ async def create_chat(_, info, title="", members=[]):
@mutation.field("deleteChat")
@login_required
async def delete_chat(_, info, chat_id: str):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
chat = await redis.execute("GET", f"/chats/{chat_id}")
if chat:
chat = dict(json.loads(chat))
if user.slug in chat['admins']:
if auth.user_id in chat['admins']:
await redis.execute("DEL", f"chats/{chat_id}")
await redis.execute("SREM", "chats_by_user/" + user, chat_id)
await redis.execute("SREM", "chats_by_user/" + str(auth.user_id), chat_id)
await redis.execute("COMMIT")
else:
return {

View File

@ -2,6 +2,7 @@ import json
from datetime import datetime, timedelta, timezone
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.redis import redis
from base.orm import local_session
from base.resolvers import query
@ -30,12 +31,9 @@ async def load_messages(chat_id: str, limit: int, offset: int):
@login_required
async def load_chats(_, info, limit: int = 50, offset: int = 0):
""" load :limit chats of current user with :offset """
user = info.context["request"].user
if user:
print('[inbox] load user\'s chats %s' % user.slug)
else:
raise Unauthorized("Please login to load chats")
cids = await redis.execute("SMEMBERS", "chats_by_user/" + user.slug)
auth: AuthCredentials = info.context["request"].auth
cids = await redis.execute("SMEMBERS", "chats_by_user/" + str(auth.user_id))
if cids:
cids = list(cids)[offset:offset + limit]
if not cids:
@ -47,7 +45,7 @@ async def load_chats(_, info, limit: int = 50, offset: int = 0):
if c:
c = dict(json.loads(c))
c['messages'] = await load_messages(cid, 5, 0)
c['unread'] = await get_unread_counter(cid, user.slug)
c['unread'] = await get_unread_counter(cid, auth.user_id)
with local_session() as session:
c['members'] = []
for userslug in c["users"]:
@ -65,11 +63,11 @@ async def load_chats(_, info, limit: int = 50, offset: int = 0):
}
async def search_user_chats(by, messages: set, slug: str, limit, offset):
async def search_user_chats(by, messages: set, user_id: int, limit, offset):
cids = set([])
by_author = by.get('author')
body_like = by.get('body')
cids.unioin(set(await redis.execute("SMEMBERS", "chats_by_user/" + slug)))
cids.unioin(set(await redis.execute("SMEMBERS", "chats_by_user/" + str(user_id))))
if by_author:
# all author's messages
cids.union(set(await redis.execute("SMEMBERS", f"chats_by_user/{by_author}")))
@ -104,9 +102,11 @@ async def load_messages_by(_, info, by, limit: int = 10, offset: int = 0):
# everyone's messages in filtered chat
messages.union(set(await load_messages(by_chat, limit, offset)))
user = info.context["request"].user
if user and len(messages) == 0:
messages.union(search_user_chats(by, messages, user.slug, limit, offset))
auth: AuthCredentials = info.context["request"].auth
if len(messages) == 0:
# FIXME
messages.union(search_user_chats(by, messages, auth.user_id, limit, offset))
days = by.get("days")
if days:
@ -126,9 +126,10 @@ async def load_messages_by(_, info, by, limit: int = 10, offset: int = 0):
@query.field("loadRecipients")
async def load_recipients(_, info, limit=50, offset=0):
chat_users = []
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
try:
chat_users += await followed_authors(user.slug)
chat_users += await followed_authors(auth.user_id)
limit = limit - len(chat_users)
except Exception:
pass

View File

@ -3,6 +3,7 @@ import json
from datetime import datetime, timezone
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.redis import redis
from base.resolvers import mutation, subscription
from services.inbox import ChatFollowing, MessageResult, MessagesStorage
@ -12,7 +13,8 @@ from services.inbox import ChatFollowing, MessageResult, MessagesStorage
@login_required
async def create_message(_, info, chat: str, body: str, replyTo=None):
""" create message with :body for :chat_id replying to :replyTo optionally """
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
chat = await redis.execute("GET", f"chats/{chat}")
if not chat:
return {
@ -25,7 +27,7 @@ async def create_message(_, info, chat: str, body: str, replyTo=None):
new_message = {
"chatId": chat['id'],
"id": message_id,
"author": user.slug,
"author": auth.user_id,
"body": body,
"replyTo": replyTo,
"createdAt": int(datetime.now(tz=timezone.utc).timestamp()),
@ -55,7 +57,7 @@ async def create_message(_, info, chat: str, body: str, replyTo=None):
@mutation.field("updateMessage")
@login_required
async def update_message(_, info, chat_id: str, message_id: int, body: str):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
chat = await redis.execute("GET", f"chats/{chat_id}")
if not chat:
@ -66,7 +68,7 @@ async def update_message(_, info, chat_id: str, message_id: int, body: str):
return {"error": "message not exist"}
message = json.loads(message)
if message["author"] != user.slug:
if message["author"] != auth.user_id:
return {"error": "access denied"}
message["body"] = body
@ -86,7 +88,7 @@ async def update_message(_, info, chat_id: str, message_id: int, body: str):
@mutation.field("deleteMessage")
@login_required
async def delete_message(_, info, chat_id: str, message_id: int):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
chat = await redis.execute("GET", f"chats/{chat_id}")
if not chat:
@ -97,15 +99,15 @@ async def delete_message(_, info, chat_id: str, message_id: int):
if not message:
return {"error": "message not exist"}
message = json.loads(message)
if message["author"] != user.slug:
if message["author"] != auth.user_id:
return {"error": "access denied"}
await redis.execute("LREM", f"chats/{chat_id}/message_ids", 0, str(message_id))
await redis.execute("DEL", f"chats/{chat_id}/messages/{str(message_id)}")
users = chat["users"]
for user_slug in users:
await redis.execute("LREM", f"chats/{chat_id}/unread/{user_slug}", 0, str(message_id))
for user_id in users:
await redis.execute("LREM", f"chats/{chat_id}/unread/{user_id}", 0, str(message_id))
result = MessageResult("DELETED", message)
await MessagesStorage.put(result)
@ -116,7 +118,7 @@ async def delete_message(_, info, chat_id: str, message_id: int):
@mutation.field("markAsRead")
@login_required
async def mark_as_read(_, info, chat_id: str, messages: [int]):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
chat = await redis.execute("GET", f"chats/{chat_id}")
if not chat:
@ -124,11 +126,11 @@ async def mark_as_read(_, info, chat_id: str, messages: [int]):
chat = json.loads(chat)
users = set(chat["users"])
if user.slug not in users:
if auth.user_id not in users:
return {"error": "access denied"}
for message_id in messages:
await redis.execute("LREM", f"chats/{chat_id}/unread/{user.slug}", 0, str(message_id))
await redis.execute("LREM", f"chats/{chat_id}/unread/{auth.user_id}", 0, str(message_id))
return {
"error": None
@ -139,8 +141,9 @@ async def mark_as_read(_, info, chat_id: str, messages: [int]):
@login_required
async def message_generator(obj, info):
try:
user = info.context["request"].user
user_following_chats = await redis.execute("GET", f"chats_by_user/{user.slug}")
auth: AuthCredentials = info.context["request"].auth
user_following_chats = await redis.execute("GET", f"chats_by_user/{auth.user_id}")
if user_following_chats:
user_following_chats = list(json.loads(user_following_chats)) # chat ids
else:

View File

@ -1,6 +1,7 @@
import json
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.redis import redis
from base.resolvers import query
from base.orm import local_session
@ -12,8 +13,8 @@ from orm.user import AuthorFollower, User
async def search_recipients(_, info, query: str, limit: int = 50, offset: int = 0):
result = []
# TODO: maybe redis scan?
user = info.context["request"].user
talk_before = await redis.execute("GET", f"/chats_by_user/{user.slug}")
auth: AuthCredentials = info.context["request"].auth
talk_before = await redis.execute("GET", f"/chats_by_user/{auth.user_id}")
if talk_before:
talk_before = list(json.loads(talk_before))[offset:offset + limit]
for chat_id in talk_before:
@ -24,7 +25,6 @@ async def search_recipients(_, info, query: str, limit: int = 50, offset: int =
if member.startswith(query):
if member not in result:
result.append(member)
user = info.context["request"].user
more_amount = limit - len(result)

View File

@ -1,4 +1,5 @@
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.resolvers import mutation
# from resolvers.community import community_follow, community_unfollow
from resolvers.zine.profile import author_follow, author_unfollow
@ -9,17 +10,18 @@ from resolvers.zine.topics import topic_follow, topic_unfollow
@mutation.field("follow")
@login_required
async def follow(_, info, what, slug):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
try:
if what == "AUTHOR":
author_follow(user, slug)
author_follow(auth.user_id, slug)
elif what == "TOPIC":
topic_follow(user, slug)
topic_follow(auth.user_id, slug)
elif what == "COMMUNITY":
# community_follow(user, slug)
pass
elif what == "REACTIONS":
reactions_follow(user, slug)
reactions_follow(auth.user_id, slug)
except Exception as e:
return {"error": str(e)}
@ -29,18 +31,18 @@ async def follow(_, info, what, slug):
@mutation.field("unfollow")
@login_required
async def unfollow(_, info, what, slug):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
try:
if what == "AUTHOR":
author_unfollow(user, slug)
author_unfollow(auth.user_id, slug)
elif what == "TOPIC":
topic_unfollow(user, slug)
topic_unfollow(auth.user_id, slug)
elif what == "COMMUNITY":
# community_unfollow(user, slug)
pass
elif what == "REACTIONS":
reactions_unfollow(user, slug)
reactions_unfollow(auth.user_id, slug)
except Exception as e:
return {"error": str(e)}

View File

@ -1,6 +1,8 @@
from datetime import datetime, timedelta, timezone
from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc, asc, select, func
from auth.credentials import AuthCredentials
from base.orm import local_session
from base.resolvers import query
from orm import ViewedEntry
@ -15,10 +17,10 @@ def add_stat_columns(q):
return add_common_stat_columns(q)
def apply_filters(q, filters, user=None):
def apply_filters(q, filters, user_id=None):
if filters.get("reacted") and user:
q.join(Reaction, Reaction.createdBy == user.id)
if filters.get("reacted") and user_id:
q.join(Reaction, Reaction.createdBy == user_id)
v = filters.get("visibility")
if v == "public":
@ -105,17 +107,15 @@ async def load_shouts_by(_, info, options):
q = add_stat_columns(q)
user = info.context["request"].user
q = apply_filters(q, options.get("filters", {}), user)
auth: AuthCredentials = info.context["request"].auth
q = apply_filters(q, options.get("filters", {}), auth.user_id)
order_by = options.get("order_by", Shout.createdAt)
if order_by == 'reacted':
aliased_reaction = aliased(Reaction)
q.outerjoin(aliased_reaction).add_columns(func.max(aliased_reaction.createdAt).label('reacted'))
order_by_desc = options.get('order_by_desc', True)
query_order_by = desc(order_by) if order_by_desc else asc(order_by)
query_order_by = desc(order_by) if options.get('order_by_desc', True) else asc(order_by)
offset = options.get("offset", 0)
limit = options.get("limit", 10)

View File

@ -4,6 +4,7 @@ from sqlalchemy import and_, func, distinct, select, literal
from sqlalchemy.orm import aliased, joinedload
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.orm import local_session
from base.resolvers import mutation, query
from orm.reaction import Reaction
@ -40,11 +41,10 @@ def add_author_stat_columns(q):
# func.sum(user_rating_aliased.value).label('rating_stat')
# )
# q = q.add_columns(literal(0).label('commented_stat'))
q = q.outerjoin(Reaction, and_(Reaction.createdBy == User.id, Reaction.body.is_not(None))).add_columns(
func.count(distinct(Reaction.id)).label('commented_stat')
)
q = q.add_columns(literal(0).label('commented_stat'))
# q = q.outerjoin(Reaction, and_(Reaction.createdBy == User.id, Reaction.body.is_not(None))).add_columns(
# func.count(distinct(Reaction.id)).label('commented_stat')
# )
q = q.group_by(User.id)
@ -74,25 +74,25 @@ def get_authors_from_query(q):
return authors
async def user_subscriptions(slug: str):
async def user_subscriptions(user_id: int):
return {
"unread": await get_total_unread_counter(slug), # unread inbox messages counter
"topics": [t.slug for t in await followed_topics(slug)], # followed topics slugs
"authors": [a.slug for a in await followed_authors(slug)], # followed authors slugs
"reactions": await followed_reactions(slug)
"unread": await get_total_unread_counter(user_id), # unread inbox messages counter
"topics": [t.slug for t in await followed_topics(user_id)], # followed topics slugs
"authors": [a.slug for a in await followed_authors(user_id)], # followed authors slugs
"reactions": await followed_reactions(user_id)
# "communities": [c.slug for c in followed_communities(slug)], # communities
}
# @query.field("userFollowedDiscussions")
@login_required
async def followed_discussions(_, info, slug) -> List[Topic]:
return await followed_reactions(slug)
# @login_required
async def followed_discussions(_, info, user_id) -> List[Topic]:
return await followed_reactions(user_id)
async def followed_reactions(slug):
async def followed_reactions(user_id):
with local_session() as session:
user = session.query(User).where(User.slug == slug).first()
user = session.query(User).where(User.id == user_id).first()
return session.query(
Reaction.shout
).where(
@ -104,31 +104,26 @@ async def followed_reactions(slug):
@query.field("userFollowedTopics")
@login_required
async def get_followed_topics(_, info, slug) -> List[Topic]:
return await followed_topics(slug)
async def get_followed_topics(_, info, user_id) -> List[Topic]:
return await followed_topics(user_id)
async def followed_topics(slug):
return followed_by_user(slug)
async def followed_topics(user_id):
return followed_by_user(user_id)
@query.field("userFollowedAuthors")
async def get_followed_authors(_, _info, slug) -> List[User]:
return await followed_authors(slug)
async def get_followed_authors(_, _info, user_id: int) -> List[User]:
return await followed_authors(user_id)
async def followed_authors(slug):
with local_session() as session:
user = session.query(User).where(User.slug == slug).first()
q = select(User)
q = add_author_stat_columns(q)
aliased_user = aliased(User)
q = q.join(AuthorFollower, AuthorFollower.author == user.id).join(
aliased_user, aliased_user.id == AuthorFollower.follower
).where(
aliased_user.slug == slug
)
return get_authors_from_query(q)
async def followed_authors(user_id):
q = select(User)
q = add_author_stat_columns(q)
q = q.join(AuthorFollower, AuthorFollower.author == User.id).where(
AuthorFollower.follower == user_id
)
return get_authors_from_query(q)
@query.field("userFollowers")
@ -157,25 +152,16 @@ async def get_user_roles(slug):
.all()
)
return roles
return [] # roles
@mutation.field("updateProfile")
@login_required
async def update_profile(_, info, profile):
print('[zine] update_profile')
print(profile)
auth = info.context["request"].auth
user_id = auth.user_id
with local_session() as session:
session.query(User).filter(User.id == user_id).update({
"name": profile['name'],
"slug": profile['slug'],
"bio": profile['bio'],
"userpic": profile['userpic'],
"about": profile['about'],
"links": profile['links']
})
session.query(User).filter(User.id == user_id).update(profile)
session.commit()
return {}
@ -183,11 +169,12 @@ async def update_profile(_, info, profile):
@mutation.field("rateUser")
@login_required
async def rate_user(_, info, rated_userslug, value):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
rating = (
session.query(UserRating)
.filter(and_(UserRating.rater == user.slug, UserRating.user == rated_userslug))
.filter(and_(UserRating.rater == auth.user_id, UserRating.user == rated_userslug))
.first()
)
if rating:
@ -195,30 +182,30 @@ async def rate_user(_, info, rated_userslug, value):
session.commit()
return {}
try:
UserRating.create(rater=user.slug, user=rated_userslug, value=value)
UserRating.create(rater=auth.user_id, user=rated_userslug, value=value)
except Exception as err:
return {"error": err}
return {}
# for mutation.field("follow")
def author_follow(user, slug):
def author_follow(user_id, slug):
with local_session() as session:
author = session.query(User).where(User.slug == slug).one()
af = AuthorFollower.create(follower=user.id, author=author.id)
af = AuthorFollower.create(follower=user_id, author=author.id)
session.add(af)
session.commit()
# for mutation.field("unfollow")
def author_unfollow(user, slug):
def author_unfollow(user_id, slug):
with local_session() as session:
flw = (
session.query(
AuthorFollower
).join(User, User.id == AuthorFollower.author).filter(
and_(
AuthorFollower.follower == user.id, User.slug == slug
AuthorFollower.follower == user_id, User.slug == slug
)
).first()
)

View File

@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import and_, asc, desc, select, text, func
from sqlalchemy.orm import aliased
from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.orm import local_session
from base.resolvers import mutation, query
from orm.reaction import Reaction, ReactionKind
@ -14,20 +15,20 @@ def add_reaction_stat_columns(q):
return add_common_stat_columns(q)
def reactions_follow(user: User, slug: str, auto=False):
def reactions_follow(user_id, slug: str, auto=False):
with local_session() as session:
shout = session.query(Shout).where(Shout.slug == slug).one()
following = (
session.query(ShoutReactionsFollower).where(and_(
ShoutReactionsFollower.follower == user.id,
ShoutReactionsFollower.follower == user_id,
ShoutReactionsFollower.shout == shout.id,
)).first()
)
if not following:
following = ShoutReactionsFollower.create(
follower=user.id,
follower=user_id,
shout=shout.id,
auto=auto
)
@ -35,13 +36,13 @@ def reactions_follow(user: User, slug: str, auto=False):
session.commit()
def reactions_unfollow(user, slug):
def reactions_unfollow(user_id, slug):
with local_session() as session:
shout = session.query(Shout).where(Shout.slug == slug).one()
following = (
session.query(ShoutReactionsFollower).where(and_(
ShoutReactionsFollower.follower == user.id,
ShoutReactionsFollower.follower == user_id,
ShoutReactionsFollower.shout == shout.id
)).first()
)
@ -51,12 +52,12 @@ def reactions_unfollow(user, slug):
session.commit()
def is_published_author(session, userslug):
def is_published_author(session, user_id):
''' checks if user has at least one publication '''
return session.query(
Shout
).where(
Shout.authors.contains(userslug)
Shout.authors.contains(user_id)
).filter(
and_(
Shout.publishedAt.is_not(None),
@ -65,17 +66,17 @@ def is_published_author(session, userslug):
).count() > 0
def check_to_publish(session, user, reaction):
def check_to_publish(session, user_id, reaction):
''' set shout to public if publicated approvers amount > 4 '''
if not reaction.replyTo and reaction.kind in [
ReactionKind.ACCEPT,
ReactionKind.LIKE,
ReactionKind.PROOF
]:
if is_published_author(user):
if is_published_author(user_id):
# now count how many approvers are voted already
approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
approvers = [user.slug, ]
approvers = [user_id, ]
for ar in approvers_reactions:
a = ar.createdBy
if is_published_author(session, a):
@ -85,7 +86,7 @@ def check_to_publish(session, user, reaction):
return False
def check_to_hide(session, user, reaction):
def check_to_hide(session, user_id, reaction):
''' hides any shout if 20% of reactions are negative '''
if not reaction.replyTo and reaction.kind in [
ReactionKind.DECLINE,
@ -107,8 +108,8 @@ def check_to_hide(session, user, reaction):
return False
def set_published(session, slug, publisher):
s = session.query(Shout).where(Shout.slug == slug).first()
def set_published(session, shout_id, publisher):
s = session.query(Shout).where(Shout.id == shout_id).first()
s.publishedAt = datetime.now(tz=timezone.utc)
s.publishedBy = publisher
s.visibility = text('public')
@ -116,8 +117,8 @@ def set_published(session, slug, publisher):
session.commit()
def set_hidden(session, slug):
s = session.query(Shout).where(Shout.slug == slug).first()
def set_hidden(session, shout_id):
s = session.query(Shout).where(Shout.id == shout_id).first()
s.visibility = text('authors')
s.publishedAt = None # TODO: discuss
s.publishedBy = None # TODO: store changes history in git
@ -128,7 +129,7 @@ def set_hidden(session, slug):
@mutation.field("createReaction")
@login_required
async def create_reaction(_, info, inp):
user = info.context["request"].user
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
reaction = Reaction.create(**inp)
@ -137,13 +138,13 @@ async def create_reaction(_, info, inp):
# self-regulation mechanics
if check_to_hide(session, user, reaction):
if check_to_hide(session, auth.user_id, reaction):
set_hidden(session, reaction.shout)
elif check_to_publish(session, user, reaction):
elif check_to_publish(session, auth.user_id, reaction):
set_published(session, reaction.shout, reaction.createdBy)
try:
reactions_follow(user, inp["shout"], True)
reactions_follow(auth.user_id, inp["shout"], True)
except Exception as e:
print(f"[resolvers.reactions] error on reactions autofollowing: {e}")
@ -158,11 +159,10 @@ async def create_reaction(_, info, inp):
@mutation.field("updateReaction")
@login_required
async def update_reaction(_, info, inp):
auth = info.context["request"].auth
user_id = auth.user_id
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
user = session.query(User).where(User.id == user_id).first()
user = session.query(User).where(User.id == auth.user_id).first()
q = select(Reaction).filter(Reaction.id == inp.id)
q = add_reaction_stat_columns(q)
@ -193,10 +193,10 @@ async def update_reaction(_, info, inp):
@mutation.field("deleteReaction")
@login_required
async def delete_reaction(_, info, rid):
auth = info.context["request"].auth
user_id = auth.user_id
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
user = session.query(User).where(User.id == user_id).first()
user = session.query(User).where(User.id == auth.user_id).first()
reaction = session.query(Reaction).filter(Reaction.id == rid).first()
if not reaction:
return {"error": "invalid reaction id"}

View File

@ -1,4 +1,6 @@
from sqlalchemy import and_, select, distinct, func
from sqlalchemy.orm import aliased
from auth.authenticate import login_required
from base.orm import local_session
from base.resolvers import mutation, query
@ -8,16 +10,20 @@ from orm import Shout, User
def add_topic_stat_columns(q):
q = q.outerjoin(ShoutTopic, Topic.id == ShoutTopic.topic).add_columns(
func.count(distinct(ShoutTopic.shout)).label('shouts_stat')
).outerjoin(ShoutAuthor, ShoutTopic.shout == ShoutAuthor.shout).add_columns(
func.count(distinct(ShoutAuthor.user)).label('authors_stat')
).outerjoin(TopicFollower,
aliased_shout_topic = aliased(ShoutTopic)
aliased_shout_author = aliased(ShoutAuthor)
aliased_topic_follower = aliased(TopicFollower)
q = q.outerjoin(aliased_shout_topic, Topic.id == aliased_shout_topic.topic).add_columns(
func.count(distinct(aliased_shout_topic.shout)).label('shouts_stat')
).outerjoin(aliased_shout_author, aliased_shout_topic.shout == aliased_shout_author.shout).add_columns(
func.count(distinct(aliased_shout_author.user)).label('authors_stat')
).outerjoin(aliased_topic_follower,
and_(
TopicFollower.topic == Topic.id,
TopicFollower.follower == ShoutAuthor.id
aliased_topic_follower.topic == Topic.id,
aliased_topic_follower.follower == aliased_shout_author.id
)).add_columns(
func.count(distinct(TopicFollower.follower)).label('followers_stat')
func.count(distinct(aliased_topic_follower.follower)).label('followers_stat')
)
q = q.group_by(Topic.id)
@ -46,10 +52,10 @@ def get_topics_from_query(q):
return topics
def followed_by_user(user_slug):
def followed_by_user(user_id):
q = select(Topic)
q = add_topic_stat_columns(q)
q = q.join(User).where(User.slug == user_slug)
q = q.join(TopicFollower).where(TopicFollower.follower == user_id)
return get_topics_from_query(q)
@ -115,21 +121,21 @@ async def update_topic(_, _info, inp):
return {"topic": topic}
async def topic_follow(user, slug):
def topic_follow(user_id, slug):
with local_session() as session:
topic = session.query(Topic).where(Topic.slug == slug).one()
following = TopicFollower.create(topic=topic.id, follower=user.id)
following = TopicFollower.create(topic=topic.id, follower=user_id)
session.add(following)
session.commit()
async def topic_unfollow(user, slug):
def topic_unfollow(user_id, slug):
with local_session() as session:
sub = (
session.query(TopicFollower).join(Topic).filter(
and_(
TopicFollower.follower == user.id,
TopicFollower.follower == user_id,
Topic.slug == slug
)
).first()
@ -145,7 +151,7 @@ async def topic_unfollow(user, slug):
async def topics_random(_, info, amount=12):
q = select(Topic)
q = add_topic_stat_columns(q)
q = q.join(Shout, ShoutTopic.shout == Shout.id).group_by(Topic.id).having(func.count(Shout.id) > 2)
q = q.join(ShoutTopic).join(Shout, ShoutTopic.shout == Shout.id).group_by(Topic.id).having(func.count(Shout.id) > 2)
q = q.order_by(func.random()).limit(amount)
return get_topics_from_query(q)