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 auth.credentials import AuthCredentials, AuthUser
from base.orm import local_session 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 settings import SESSION_TOKEN_HEADER
from auth.tokenstorage import SessionToken from auth.tokenstorage import SessionToken
@ -39,12 +39,14 @@ class JWTAuthenticate(AuthenticationBackend):
user = None user = None
with local_session() as session: with local_session() as session:
try: try:
q = select( user = (
User session.query(User).options(
).filter( joinedload(User.roles).options(joinedload(Role.permissions)),
User.id == payload.user_id joinedload(User.ratings)
).select_from(User) ).filter(
user = session.execute(q).unique().one() User.id == payload.user_id
).one()
)
except exc.NoResultFound: except exc.NoResultFound:
user = None user = None
@ -59,7 +61,7 @@ class JWTAuthenticate(AuthenticationBackend):
scopes=scopes, scopes=scopes,
logged_in=True logged_in=True
), ),
user, AuthUser(user_id=user.id),
) )
else: else:
InvalidToken("please try again") 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: try:
to = "%s <%s>" % (user.name, user.email) to = "%s <%s>" % (user.name, user.email)
if lang not in ['ru', 'en']: if lang not in ['ru', 'en']:

View File

@ -41,6 +41,7 @@ async def start_up():
async def dev_start_up(): async def dev_start_up():
if exists(DEV_SERVER_STATUS_FILE_NAME): if exists(DEV_SERVER_STATUS_FILE_NAME):
await redis.connect()
return return
else: else:
with open(DEV_SERVER_STATUS_FILE_NAME, 'w', encoding='utf-8') as f: 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( dev_app = app = Starlette(
debug=True, debug=True,
on_startup=[dev_start_up], on_startup=[dev_start_up],
on_shutdown=[shutdown],
middleware=middleware, middleware=middleware,
routes=routes, routes=routes,
) )

View File

@ -9,7 +9,6 @@ from orm.shout import ShoutReactionsFollower
from orm.topic import TopicFollower from orm.topic import TopicFollower
from orm.user import User from orm.user import User
from orm.shout import Shout from orm.shout import Shout
# from services.stat.reacted import ReactedStorage
ts = datetime.now(tz=timezone.utc) ts = datetime.now(tz=timezone.utc)
@ -84,7 +83,6 @@ def migrate_ratings(session, entry, reaction_dict):
) )
session.add(following2) session.add(following2)
session.add(rr) session.add(rr)
# await ReactedStorage.react(rr)
except Exception as e: except Exception as e:
print("[migration] comment rating error: %r" % re_reaction_dict) 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.shout import Shout, ShoutTopic, ShoutReactionsFollower
from orm.user import User from orm.user import User
from orm.topic import TopicFollower, Topic from orm.topic import TopicFollower, Topic
# from services.stat.reacted import ReactedStorage
from services.stat.viewed import ViewedStorage from services.stat.viewed import ViewedStorage
import re import re
@ -365,7 +364,6 @@ async def content_ratings_to_reactions(entry, slug):
else: else:
rea = Reaction.create(**reaction_dict) rea = Reaction.create(**reaction_dict)
session.add(rea) session.add(rea)
# await ReactedStorage.react(rea)
# shout_dict['ratings'].append(reaction_dict) # shout_dict['ratings'].append(reaction_dict)
session.commit() session.commit()

View File

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

View File

@ -56,7 +56,8 @@ class User(Base):
email = Column(String, unique=True, nullable=False, comment="Email") email = Column(String, unique=True, nullable=False, comment="Email")
username = Column(String, nullable=False, comment="Login") username = Column(String, nullable=False, comment="Login")
password = Column(String, nullable=True, comment="Password") 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") userpic = Column(String, nullable=True, comment="Userpic")
name = Column(String, nullable=True, comment="Display name") name = Column(String, nullable=True, comment="Display name")
slug = Column(String, unique=True, comment="User's slug") slug = Column(String, unique=True, comment="User's slug")
@ -100,7 +101,7 @@ class User(Base):
session.commit() session.commit()
User.default_user = default User.default_user = default
async def get_permission(self): def get_permission(self):
scope = {} scope = {}
for role in self.roles: for role in self.roles:
for p in role.permissions: for p in role.permissions:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy.orm import joinedload, aliased from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc, asc, select, func from sqlalchemy.sql.expression import desc, asc, select, func
from auth.credentials import AuthCredentials
from base.orm import local_session from base.orm import local_session
from base.resolvers import query from base.resolvers import query
from orm import ViewedEntry from orm import ViewedEntry
@ -15,10 +17,10 @@ def add_stat_columns(q):
return add_common_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: if filters.get("reacted") and user_id:
q.join(Reaction, Reaction.createdBy == user.id) q.join(Reaction, Reaction.createdBy == user_id)
v = filters.get("visibility") v = filters.get("visibility")
if v == "public": if v == "public":
@ -105,17 +107,15 @@ async def load_shouts_by(_, info, options):
q = add_stat_columns(q) q = add_stat_columns(q)
user = info.context["request"].user auth: AuthCredentials = info.context["request"].auth
q = apply_filters(q, options.get("filters", {}), user) q = apply_filters(q, options.get("filters", {}), auth.user_id)
order_by = options.get("order_by", Shout.createdAt) order_by = options.get("order_by", Shout.createdAt)
if order_by == 'reacted': if order_by == 'reacted':
aliased_reaction = aliased(Reaction) aliased_reaction = aliased(Reaction)
q.outerjoin(aliased_reaction).add_columns(func.max(aliased_reaction.createdAt).label('reacted')) 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 options.get('order_by_desc', True) else asc(order_by)
query_order_by = desc(order_by) if order_by_desc else asc(order_by)
offset = options.get("offset", 0) offset = options.get("offset", 0)
limit = options.get("limit", 10) 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 sqlalchemy.orm import aliased, joinedload
from auth.authenticate import login_required from auth.authenticate import login_required
from auth.credentials import AuthCredentials
from base.orm import local_session from base.orm import local_session
from base.resolvers import mutation, query from base.resolvers import mutation, query
from orm.reaction import Reaction from orm.reaction import Reaction
@ -40,11 +41,10 @@ def add_author_stat_columns(q):
# func.sum(user_rating_aliased.value).label('rating_stat') # func.sum(user_rating_aliased.value).label('rating_stat')
# ) # )
# q = q.add_columns(literal(0).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(
q = q.outerjoin(Reaction, and_(Reaction.createdBy == User.id, Reaction.body.is_not(None))).add_columns( # func.count(distinct(Reaction.id)).label('commented_stat')
func.count(distinct(Reaction.id)).label('commented_stat') # )
)
q = q.group_by(User.id) q = q.group_by(User.id)
@ -74,25 +74,25 @@ def get_authors_from_query(q):
return authors return authors
async def user_subscriptions(slug: str): async def user_subscriptions(user_id: int):
return { return {
"unread": await get_total_unread_counter(slug), # unread inbox messages counter "unread": await get_total_unread_counter(user_id), # unread inbox messages counter
"topics": [t.slug for t in await followed_topics(slug)], # followed topics slugs "topics": [t.slug for t in await followed_topics(user_id)], # followed topics slugs
"authors": [a.slug for a in await followed_authors(slug)], # followed authors slugs "authors": [a.slug for a in await followed_authors(user_id)], # followed authors slugs
"reactions": await followed_reactions(slug) "reactions": await followed_reactions(user_id)
# "communities": [c.slug for c in followed_communities(slug)], # communities # "communities": [c.slug for c in followed_communities(slug)], # communities
} }
# @query.field("userFollowedDiscussions") # @query.field("userFollowedDiscussions")
@login_required # @login_required
async def followed_discussions(_, info, slug) -> List[Topic]: async def followed_discussions(_, info, user_id) -> List[Topic]:
return await followed_reactions(slug) return await followed_reactions(user_id)
async def followed_reactions(slug): async def followed_reactions(user_id):
with local_session() as session: 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( return session.query(
Reaction.shout Reaction.shout
).where( ).where(
@ -104,31 +104,26 @@ async def followed_reactions(slug):
@query.field("userFollowedTopics") @query.field("userFollowedTopics")
@login_required @login_required
async def get_followed_topics(_, info, slug) -> List[Topic]: async def get_followed_topics(_, info, user_id) -> List[Topic]:
return await followed_topics(slug) return await followed_topics(user_id)
async def followed_topics(slug): async def followed_topics(user_id):
return followed_by_user(slug) return followed_by_user(user_id)
@query.field("userFollowedAuthors") @query.field("userFollowedAuthors")
async def get_followed_authors(_, _info, slug) -> List[User]: async def get_followed_authors(_, _info, user_id: int) -> List[User]:
return await followed_authors(slug) return await followed_authors(user_id)
async def followed_authors(slug): async def followed_authors(user_id):
with local_session() as session: q = select(User)
user = session.query(User).where(User.slug == slug).first() q = add_author_stat_columns(q)
q = select(User) q = q.join(AuthorFollower, AuthorFollower.author == User.id).where(
q = add_author_stat_columns(q) AuthorFollower.follower == user_id
aliased_user = aliased(User) )
q = q.join(AuthorFollower, AuthorFollower.author == user.id).join( return get_authors_from_query(q)
aliased_user, aliased_user.id == AuthorFollower.follower
).where(
aliased_user.slug == slug
)
return get_authors_from_query(q)
@query.field("userFollowers") @query.field("userFollowers")
@ -157,25 +152,16 @@ async def get_user_roles(slug):
.all() .all()
) )
return roles return [] # roles
@mutation.field("updateProfile") @mutation.field("updateProfile")
@login_required @login_required
async def update_profile(_, info, profile): async def update_profile(_, info, profile):
print('[zine] update_profile')
print(profile)
auth = info.context["request"].auth auth = info.context["request"].auth
user_id = auth.user_id user_id = auth.user_id
with local_session() as session: with local_session() as session:
session.query(User).filter(User.id == user_id).update({ session.query(User).filter(User.id == user_id).update(profile)
"name": profile['name'],
"slug": profile['slug'],
"bio": profile['bio'],
"userpic": profile['userpic'],
"about": profile['about'],
"links": profile['links']
})
session.commit() session.commit()
return {} return {}
@ -183,11 +169,12 @@ async def update_profile(_, info, profile):
@mutation.field("rateUser") @mutation.field("rateUser")
@login_required @login_required
async def rate_user(_, info, rated_userslug, value): async def rate_user(_, info, rated_userslug, value):
user = info.context["request"].user auth: AuthCredentials = info.context["request"].auth
with local_session() as session: with local_session() as session:
rating = ( rating = (
session.query(UserRating) 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() .first()
) )
if rating: if rating:
@ -195,30 +182,30 @@ async def rate_user(_, info, rated_userslug, value):
session.commit() session.commit()
return {} return {}
try: 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: except Exception as err:
return {"error": err} return {"error": err}
return {} return {}
# for mutation.field("follow") # for mutation.field("follow")
def author_follow(user, slug): def author_follow(user_id, slug):
with local_session() as session: with local_session() as session:
author = session.query(User).where(User.slug == slug).one() 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.add(af)
session.commit() session.commit()
# for mutation.field("unfollow") # for mutation.field("unfollow")
def author_unfollow(user, slug): def author_unfollow(user_id, slug):
with local_session() as session: with local_session() as session:
flw = ( flw = (
session.query( session.query(
AuthorFollower AuthorFollower
).join(User, User.id == AuthorFollower.author).filter( ).join(User, User.id == AuthorFollower.author).filter(
and_( and_(
AuthorFollower.follower == user.id, User.slug == slug AuthorFollower.follower == user_id, User.slug == slug
) )
).first() ).first()
) )

View File

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

View File

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