Merge branch 'following' into 'main'

following-manager

See merge request discoursio/discoursio-api!10
This commit is contained in:
Igor 2023-02-20 21:11:16 +00:00
commit 761d17e02e
20 changed files with 425 additions and 218 deletions

View File

@ -12,7 +12,7 @@ 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
from base.exceptions import InvalidToken, OperationNotAllowed from base.exceptions import OperationNotAllowed
class JWTAuthenticate(AuthenticationBackend): class JWTAuthenticate(AuthenticationBackend):
@ -30,44 +30,34 @@ class JWTAuthenticate(AuthenticationBackend):
user_id=None user_id=None
) )
try: if len(token.split('.')) > 1:
if len(token.split('.')) > 1: payload = await SessionToken.verify(token)
payload = await SessionToken.verify(token) user = None
if payload is None: with local_session() as session:
return AuthCredentials(scopes=[]), AuthUser(user_id=None) try:
user = None user = (
with local_session() as session: session.query(User).options(
try: joinedload(User.roles).options(joinedload(Role.permissions)),
user = ( joinedload(User.ratings)
session.query(User).options( ).filter(
joinedload(User.roles).options(joinedload(Role.permissions)), User.id == payload.user_id
joinedload(User.ratings) ).one()
).filter( )
User.id == payload.user_id
).one()
)
except exc.NoResultFound:
user = None
if not user: scopes = {} # TODO: integrate await user.get_permission()
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
scopes = {} # await user.get_permission() return (
AuthCredentials(
user_id=payload.user_id,
scopes=scopes,
logged_in=True
),
AuthUser(user_id=user.id),
)
except exc.NoResultFound:
pass
return ( return AuthCredentials(scopes=[], error_message=str('Invalid token')), AuthUser(user_id=None)
AuthCredentials(
user_id=payload.user_id,
scopes=scopes,
logged_in=True
),
AuthUser(user_id=user.id),
)
else:
InvalidToken("please try again")
except Exception as e:
print("[auth.authenticate] session token verify error")
print(e)
return AuthCredentials(scopes=[], error_message=str(e)), AuthUser(user_id=None)
def login_required(func): def login_required(func):

View File

@ -23,6 +23,7 @@ class JWTCodec:
@staticmethod @staticmethod
def decode(token: str, verify_exp: bool = True) -> TokenPayload: def decode(token: str, verify_exp: bool = True) -> TokenPayload:
r = None r = None
payload = None
try: try:
payload = jwt.decode( payload = jwt.decode(
token, token,

View File

@ -13,6 +13,7 @@ class DraftTopic(Base):
id = None # type: ignore id = None # type: ignore
collab = Column(ForeignKey("draft_collab.id"), primary_key=True) collab = Column(ForeignKey("draft_collab.id"), primary_key=True)
topic = Column(ForeignKey("topic.id"), primary_key=True) topic = Column(ForeignKey("topic.id"), primary_key=True)
main = Column(Boolean, default=False)
class DraftAuthor(Base): class DraftAuthor(Base):

View File

@ -19,8 +19,8 @@ class ReactionKind(Enumeration):
REJECT = 0 # -1 REJECT = 0 # -1
LIKE = 11 # +1 LIKE = 11 # +1
DISLIKE = 12 # -1 DISLIKE = 12 # -1
REMARK = 13 # 0 REMARK = 13 # 0
FOOTNOTE = 14 # 0 FOOTNOTE = 14 # 0
# TYPE = <reaction index> # rating diff # TYPE = <reaction index> # rating diff

View File

@ -15,6 +15,7 @@ class ShoutTopic(Base):
id = None # type: ignore id = None # type: ignore
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
topic = Column(ForeignKey("topic.id"), primary_key=True, index=True) topic = Column(ForeignKey("topic.id"), primary_key=True, index=True)
main = Column(Boolean, default=False)
class ShoutReactionsFollower(Base): class ShoutReactionsFollower(Base):
@ -42,28 +43,33 @@ class ShoutAuthor(Base):
class Shout(Base): class Shout(Base):
__tablename__ = "shout" __tablename__ = "shout"
slug = Column(String, unique=True) # timestamps
community = Column(ForeignKey("community.id"), default=1)
lang = Column(String, nullable=False, default='ru', comment="Language")
body = Column(String, nullable=False, comment="Body")
title = Column(String, nullable=True)
subtitle = Column(String, nullable=True)
layout = Column(String, nullable=True)
mainTopic = Column(ForeignKey("topic.slug"), nullable=True)
cover = Column(String, nullable=True, comment="Cover")
authors = relationship(lambda: User, secondary=ShoutAuthor.__tablename__)
topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__)
reactions = relationship(lambda: Reaction)
visibility = Column(String, nullable=True) # owner authors community public
versionOf = Column(ForeignKey("shout.id"), nullable=True)
oid = Column(String, nullable=True)
media = Column(JSON, nullable=True)
createdAt = Column(DateTime, nullable=False, default=datetime.now, comment="Created at") createdAt = Column(DateTime, nullable=False, default=datetime.now, comment="Created at")
updatedAt = Column(DateTime, nullable=True, comment="Updated at") updatedAt = Column(DateTime, nullable=True, comment="Updated at")
publishedAt = Column(DateTime, nullable=True) publishedAt = Column(DateTime, nullable=True)
deletedAt = Column(DateTime, nullable=True) deletedAt = Column(DateTime, nullable=True)
# same with Draft
slug = Column(String, unique=True)
cover = Column(String, nullable=True, comment="Cover")
body = Column(String, nullable=False, comment="Body")
title = Column(String, nullable=True)
subtitle = Column(String, nullable=True)
layout = Column(String, nullable=True)
media = Column(JSON, nullable=True)
authors = relationship(lambda: User, secondary=ShoutAuthor.__tablename__)
topics = relationship(lambda: Topic, secondary=ShoutTopic.__tablename__)
reactions = relationship(lambda: Reaction)
# TODO: these field should be used or modified
community = Column(ForeignKey("community.id"), default=1)
lang = Column(String, nullable=False, default='ru', comment="Language")
mainTopic = Column(ForeignKey("topic.slug"), nullable=True)
visibility = Column(String, nullable=True) # owner authors community public
versionOf = Column(ForeignKey("shout.id"), nullable=True)
oid = Column(String, nullable=True)
@staticmethod @staticmethod
def init_table(): def init_table():
with local_session() as session: with local_session() as session:

View File

@ -8,8 +8,8 @@ from resolvers.auth import (
get_current_user, get_current_user,
) )
from resolvers.create.collab import load_drafts, create_draft, update_draft, delete_draft,\ from resolvers.create.drafts import load_drafts, create_draft, update_draft, delete_draft,\
accept_coauthor, invite_coauthor accept_coauthor, invite_coauthor, draft_to_shout
from resolvers.create.migrate import markdown_body from resolvers.create.migrate import markdown_body
from resolvers.create.editor import create_shout, delete_shout, update_shout from resolvers.create.editor import create_shout, delete_shout, update_shout
@ -87,19 +87,18 @@ __all__ = [
# zine.following # zine.following
"follow", "follow",
"unfollow", "unfollow",
# create.editor # create
"create_shout", "create_shout",
"update_shout", "update_shout",
"delete_shout", "delete_shout",
# create.migrate
"markdown_body", "markdown_body",
# create.collab
"load_drafts", "load_drafts",
"create_draft", "create_draft",
"update_draft", "update_draft",
"delete_draft", "delete_draft",
"invite_coauthor", "invite_coauthor",
"accept_coauthor", "accept_coauthor",
"draft_to_shout",
# zine.topics # zine.topics
"topics_all", "topics_all",
"topics_by_community", "topics_by_community",

View File

@ -6,7 +6,7 @@ from urllib.parse import quote_plus
from graphql.type import GraphQLResolveInfo from graphql.type import GraphQLResolveInfo
from starlette.responses import RedirectResponse from starlette.responses import RedirectResponse
from transliterate import translit from transliterate import translit
import re
from auth.authenticate import login_required from auth.authenticate import login_required
from auth.credentials import AuthCredentials from auth.credentials import AuthCredentials
from auth.email import send_auth_email from auth.email import send_auth_email
@ -92,6 +92,7 @@ def create_user(user_dict):
def generate_unique_slug(src): def generate_unique_slug(src):
print('[resolvers.auth] generating slug from: ' + src) print('[resolvers.auth] generating slug from: ' + src)
slug = translit(src, "ru", reversed=True).replace(".", "-").lower() slug = translit(src, "ru", reversed=True).replace(".", "-").lower()
slug = re.sub('[^0-9a-zA-Z]+', '-', slug)
if slug != src: if slug != src:
print('[resolvers.auth] translited name: ' + slug) print('[resolvers.auth] translited name: ' + slug)
c = 1 c = 1

View File

@ -2,10 +2,13 @@ from auth.authenticate import login_required
from auth.credentials import AuthCredentials 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 orm.draft import DraftCollab, DraftAuthor
from orm.draft import DraftCollab, DraftAuthor, DraftTopic
from orm.shout import Shout from orm.shout import Shout
from orm.topic import Topic
from orm.user import User from orm.user import User
from datetime import datetime, timezone
from transliterate import translit
import re
@query.field("loadDrafts") @query.field("loadDrafts")
@ -18,11 +21,11 @@ async def load_drafts(_, info):
return drafts return drafts
@mutation.field("createDraft") # TODO @mutation.field("createDraft") # TODO
@login_required @login_required
async def create_draft(_, info, draft_input): async def create_draft(_, info, draft_input):
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
draft_input['createdBy'] = auth.user_id
with local_session() as session: with local_session() as session:
collab = DraftCollab.create(**draft_input) collab = DraftCollab.create(**draft_input)
session.add(collab) session.add(collab)
@ -32,52 +35,56 @@ async def create_draft(_, info, draft_input):
return {} return {}
@mutation.field("deleteDraft") # TODO @mutation.field("deleteDraft")
@login_required @login_required
async def delete_draft(_, info, draft: int = 0): async def delete_draft(_, info, draft: int = 0):
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
with local_session() as session: with local_session() as session:
collab = session.query(DraftCollab).where(DraftCollab.id == draft_input.id).one() d = session.query(DraftCollab).where(DraftCollab.id == draft).one()
if auth.user_id not in s.authors: if auth.user_id not in d.authors:
# raise BaseHttpException("only owner can remove coauthors") # raise BaseHttpException("only owner can remove coauthors")
return { return {
"error": "Only authors can update a draft" "error": "Only authors can update a draft"
} }
elif not collab: elif not d:
return { return {
"error": "There is no draft with this id" "error": "There is no draft with this id"
} }
else: else:
session.delete(collab) session.delete(d)
session.commit() session.commit()
return {} return {}
@mutation.field("updateDraft") # TODO: draft input type @mutation.field("updateDraft") # TODO: draft input type
@login_required @login_required
async def update_draft(_, info, draft_input): async def update_draft(_, info, draft_input):
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
with local_session() as session: with local_session() as session:
collab = session.query(DraftCollab).where(DraftCollab.id == draft_input.id).one() # raises Error when not found d = session.query(
if auth.user_id not in s.authors: DraftCollab
).where(
DraftCollab.id == draft_input.id
).one() # raises Error when not found
if auth.user_id not in d.authors:
# raise BaseHttpException("only owner can remove coauthors") # raise BaseHttpException("only owner can remove coauthors")
return { return {
"error": "Only authors can update draft" "error": "Only authors can update draft"
} }
elif not s: elif not d:
return { return {
"error": "There is no draft with this id" "error": "There is no draft with this id"
} }
else: else:
draft_input["updatedAt"] = datetime.now(tz=timezone.utc) draft_input["updatedAt"] = datetime.now(tz=timezone.utc)
collab.update(draft_input) d.update(draft_input)
session.commit() session.commit()
# TODO: email notify # TODO: email notify
return {} return {}
@mutation.field("inviteAuthor") @mutation.field("inviteAuthor")
@login_required @login_required
async def invite_coauthor(_, info, author: int = 0, draft: int = 0): async def invite_coauthor(_, info, author: int = 0, draft: int = 0):
@ -108,25 +115,75 @@ async def invite_coauthor(_, info, author: int = 0, draft: int = 0):
return {} return {}
def get_slug(src):
slug = translit(src, "ru", reversed=True).replace(".", "-").lower()
slug = re.sub('[^0-9a-zA-Z]+', '-', slug)
return slug
@mutation.field("inviteAccept") @mutation.field("inviteAccept")
@login_required @login_required
async def accept_coauthor(_, info, draft: int): async def accept_coauthor(_, info, draft: int):
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
with local_session() as session: with local_session() as session:
# c = session.query(DraftCollab).where(DraftCollab.id == draft).one() d = session.query(DraftCollab).where(DraftCollab.id == draft).one()
a = session.query(DraftAuthor).where(DraftAuthor.collab == draft).filter(DraftAuthor.author == auth.user_id).one() if not d:
if not a.accepted:
a.accepted = True
session.commit()
# TODO: email notify
return {}
elif a.accepted == True:
return { return {
"error": "You have accepted invite before" "error": "Draft id was not found"
} }
else: else:
# raise BaseHttpException("only invited can accept") a = session.query(DraftAuthor).where(DraftAuthor.collab == draft).filter(
DraftAuthor.author == auth.user_id).one()
if not a.accepted:
a.accepted = True
session.commit()
# TODO: email notify
return {}
elif a.accepted:
return {
"error": "You have accepted invite before"
}
else:
# raise BaseHttpException("only invited can accept")
return {
"error": "You don't have an invitation yet"
}
@mutation.field("draftToShout")
@login_required
async def draft_to_shout(_, info, draft: int = 0):
auth: AuthCredentials = info.context["request"].auth
with local_session() as session:
d = session.query(DraftCollab).where(DraftCollab.id == draft).one()
if auth.user_id not in d.authors:
# raise BaseHttpException("you are not in authors list")
return { return {
"error": "You don't have an invitation yet" "error": "You are not in authors list"
} }
elif d.id:
draft_authors = [a.author for a in d.authors]
draft_topics = [t.topic for t in d.topics]
authors = session.query(User).where(User.id._in(draft_authors)).all()
topics = session.query(Topic).where(Topic.id._in(draft_topics)).all()
new_shout = Shout.create({
"authors": authors,
"body": d.body,
"title": d.title,
"subtitle": d.subtitle or "",
"topics": topics,
"media": d.media,
"slug": d.slug or get_slug(d.title),
"layout": d.layout or "article"
})
session.add(new_shout)
session.commit()
else:
return {
"error": "Draft is not found"
}
# TODO: email notify
return {}

View File

@ -12,9 +12,9 @@ from orm.topic import TopicFollower, Topic
from orm.user import User from orm.user import User
from resolvers.zine.reactions import reactions_follow, reactions_unfollow from resolvers.zine.reactions import reactions_follow, reactions_unfollow
from services.zine.gittask import GitTask from services.zine.gittask import GitTask
from resolvers.inbox.chats import create_chat # from resolvers.inbox.chats import create_chat
from services.inbox.storage import MessagesStorage # from services.inbox.storage import MessagesStorage
from orm.draft import DraftCollab # from orm.draft import DraftCollab
@mutation.field("createShout") @mutation.field("createShout")

View File

@ -50,12 +50,14 @@ async def load_chats(_, info, limit: int = 50, offset: int = 0):
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
cids = await redis.execute("SMEMBERS", "chats_by_user/" + str(auth.user_id)) cids = await redis.execute("SMEMBERS", "chats_by_user/" + str(auth.user_id))
onliners = await redis.execute("SMEMBERS", "users-online")
if cids: if cids:
cids = list(cids)[offset:offset + limit] cids = list(cids)[offset:offset + limit]
if not cids: if not cids:
print('[inbox.load] no chats were found') print('[inbox.load] no chats were found')
cids = [] cids = []
onliners = await redis.execute("SMEMBERS", "users-online")
if not onliners:
onliners = []
chats = [] chats = []
for cid in cids: for cid in cids:
cid = cid.decode("utf-8") cid = cid.decode("utf-8")
@ -124,8 +126,10 @@ async def load_messages_by(_, info, by, limit: int = 10, offset: int = 0):
async def load_recipients(_, info, limit=50, offset=0): async def load_recipients(_, info, limit=50, offset=0):
chat_users = [] chat_users = []
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
onliners = await redis.execute("SMEMBERS", "users-online")
if not onliners:
onliners = []
try: try:
onliners = await redis.execute("SMEMBERS", "users-online")
chat_users += await followed_authors(auth.user_id) chat_users += await followed_authors(auth.user_id)
limit = limit - len(chat_users) limit = limit - len(chat_users)
except Exception: except Exception:

View File

@ -7,8 +7,7 @@ from auth.authenticate import login_required
from auth.credentials import AuthCredentials 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.helpers import ChatFollowing, MessageResult from services.following import FollowingManager, FollowingResult, Following
from services.inbox.storage import MessagesStorage
from validations.inbox import Message from validations.inbox import Message
@ -51,8 +50,8 @@ async def create_message(_, info, chat: str, body: str, replyTo=None):
"LPUSH", f"chats/{chat['id']}/unread/{user_slug}", str(message_id) "LPUSH", f"chats/{chat['id']}/unread/{user_slug}", str(message_id)
) )
result = MessageResult("NEW", new_message) result = FollowingResult("NEW", 'chat', new_message)
await MessagesStorage.put(result) await FollowingManager.push('chat', result)
return { return {
"message": new_message, "message": new_message,
@ -82,8 +81,8 @@ async def update_message(_, info, chat_id: str, message_id: int, body: str):
await redis.execute("SET", f"chats/{chat_id}/messages/{message_id}", json.dumps(message)) await redis.execute("SET", f"chats/{chat_id}/messages/{message_id}", json.dumps(message))
result = MessageResult("UPDATED", message) result = FollowingResult("UPDATED", 'chat', message)
await MessagesStorage.put(result) await FollowingManager.push('chat', result)
return { return {
"message": message, "message": message,
@ -115,8 +114,8 @@ async def delete_message(_, info, chat_id: str, message_id: int):
for user_id in users: for user_id in users:
await redis.execute("LREM", f"chats/{chat_id}/unread/{user_id}", 0, str(message_id)) await redis.execute("LREM", f"chats/{chat_id}/unread/{user_id}", 0, str(message_id))
result = MessageResult("DELETED", message) result = FollowingResult("DELETED", 'chat', message)
await MessagesStorage.put(result) await FollowingManager.push(result)
return {} return {}
@ -162,8 +161,8 @@ async def message_generator(_, info: GraphQLResolveInfo):
user_following_chats_sorted = sorted(user_following_chats, key=lambda x: updated[x], reverse=True) user_following_chats_sorted = sorted(user_following_chats, key=lambda x: updated[x], reverse=True)
for chat_id in user_following_chats_sorted: for chat_id in user_following_chats_sorted:
following_chat = ChatFollowing(chat_id) following_chat = Following('chat', chat_id)
await MessagesStorage.register_chat(following_chat) await FollowingManager.register('chat', following_chat)
chat_task = following_chat.queue.get() chat_task = following_chat.queue.get()
tasks.append(chat_task) tasks.append(chat_task)
@ -171,7 +170,7 @@ async def message_generator(_, info: GraphQLResolveInfo):
msg = await asyncio.gather(*tasks) msg = await asyncio.gather(*tasks)
yield msg yield msg
finally: finally:
await MessagesStorage.remove_chat(following_chat) await FollowingManager.remove('chat', following_chat)
@subscription.field("newMessage") @subscription.field("newMessage")

View File

@ -1,11 +1,16 @@
import asyncio
from base.orm import local_session
from base.resolvers import mutation, subscription
from auth.authenticate import login_required from auth.authenticate import login_required
from auth.credentials import AuthCredentials from auth.credentials import AuthCredentials
from base.resolvers import mutation, subscription
# from resolvers.community import community_follow, community_unfollow # from resolvers.community import community_follow, community_unfollow
from orm.user import AuthorFollower
from orm.topic import TopicFollower
from orm.shout import ShoutReactionsFollower
from resolvers.zine.profile import author_follow, author_unfollow from resolvers.zine.profile import author_follow, author_unfollow
from resolvers.zine.reactions import reactions_follow, reactions_unfollow from resolvers.zine.reactions import reactions_follow, reactions_unfollow
from resolvers.zine.topics import topic_follow, topic_unfollow from resolvers.zine.topics import topic_follow, topic_unfollow
import asyncio from services.following import Following, FollowingManager, FollowingResult
from graphql.type import GraphQLResolveInfo from graphql.type import GraphQLResolveInfo
@ -16,15 +21,23 @@ async def follow(_, info, what, slug):
try: try:
if what == "AUTHOR": if what == "AUTHOR":
author_follow(auth.user_id, slug) if author_follow(auth.user_id, slug):
result = FollowingResult("NEW", 'author', slug)
await FollowingManager.push('author', result)
elif what == "TOPIC": elif what == "TOPIC":
topic_follow(auth.user_id, slug) if topic_follow(auth.user_id, slug):
result = FollowingResult("NEW", 'topic', slug)
await FollowingManager.push('topic', result)
elif what == "COMMUNITY": elif what == "COMMUNITY":
# community_follow(user, slug) if False: # TODO: use community_follow(auth.user_id, slug):
pass result = FollowingResult("NEW", 'community', slug)
await FollowingManager.push('community', result)
elif what == "REACTIONS": elif what == "REACTIONS":
reactions_follow(auth.user_id, slug) if reactions_follow(auth.user_id, slug):
result = FollowingResult("NEW", 'shout', slug)
await FollowingManager.push('shout', result)
except Exception as e: except Exception as e:
print(Exception(e))
return {"error": str(e)} return {"error": str(e)}
return {} return {}
@ -37,20 +50,28 @@ async def unfollow(_, info, what, slug):
try: try:
if what == "AUTHOR": if what == "AUTHOR":
author_unfollow(auth.user_id, slug) if author_unfollow(auth.user_id, slug):
result = FollowingResult("DELETED", 'author', slug)
await FollowingManager.push('author', result)
elif what == "TOPIC": elif what == "TOPIC":
topic_unfollow(auth.user_id, slug) if topic_unfollow(auth.user_id, slug):
result = FollowingResult("DELETED", 'topic', slug)
await FollowingManager.push('topic', result)
elif what == "COMMUNITY": elif what == "COMMUNITY":
# community_unfollow(user, slug) if False: # TODO: use community_unfollow(auth.user_id, slug):
pass result = FollowingResult("DELETED", 'community', slug)
await FollowingManager.push('community', result)
elif what == "REACTIONS": elif what == "REACTIONS":
reactions_unfollow(auth.user_id, slug) if reactions_unfollow(auth.user_id, slug):
result = FollowingResult("DELETED", 'shout', slug)
await FollowingManager.push('shout', result)
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}
return {} return {}
# by author and by topic
@subscription.source("newShout") @subscription.source("newShout")
@login_required @login_required
async def shout_generator(_, info: GraphQLResolveInfo): async def shout_generator(_, info: GraphQLResolveInfo):
@ -60,7 +81,37 @@ async def shout_generator(_, info: GraphQLResolveInfo):
try: try:
tasks = [] tasks = []
# TODO: implement when noticing new shout with local_session() as session:
# notify new shout by followed authors
following_topics = session.query(TopicFollower).where(TopicFollower.follower == user_id).all()
for topic_id in following_topics:
following_topic = Following('topic', topic_id)
await FollowingManager.register('topic', following_topic)
following_topic_task = following_topic.queue.get()
tasks.append(following_topic_task)
# by followed topics
following_authors = session.query(AuthorFollower).where(
AuthorFollower.follower == user_id).all()
for author_id in following_authors:
following_author = Following('author', author_id)
await FollowingManager.register('author', following_author)
following_author_task = following_author.queue.get()
tasks.append(following_author_task)
# TODO: use communities
# by followed communities
# following_communities = session.query(CommunityFollower).where(
# CommunityFollower.follower == user_id).all()
# for community_id in following_communities:
# following_community = Following('community', author_id)
# await FollowingManager.register('community', following_community)
# following_community_task = following_community.queue.get()
# tasks.append(following_community_task)
while True: while True:
shout = await asyncio.gather(*tasks) shout = await asyncio.gather(*tasks)
@ -76,12 +127,21 @@ async def reaction_generator(_, info):
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
user_id = auth.user_id user_id = auth.user_id
try: try:
tasks = [] with local_session() as session:
followings = session.query(ShoutReactionsFollower.shout).where(
ShoutReactionsFollower.follower == user_id).unique()
# TODO: implement when noticing new reaction # notify new reaction
while True: tasks = []
reaction = await asyncio.gather(*tasks) for shout_id in followings:
yield reaction following_shout = Following('shout', shout_id)
await FollowingManager.register('shout', following_shout)
following_author_task = following_shout.queue.get()
tasks.append(following_author_task)
while True:
reaction = await asyncio.gather(*tasks)
yield reaction
finally: finally:
pass pass

View File

@ -3,13 +3,15 @@ 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, case from sqlalchemy.sql.expression import desc, asc, select, func, case
from auth.authenticate import login_required
from auth.credentials import AuthCredentials from auth.credentials import AuthCredentials
from base.exceptions import ObjectNotExist from base.exceptions import ObjectNotExist
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, TopicFollower
from orm.reaction import Reaction, ReactionKind from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.user import AuthorFollower
def add_stat_columns(q): def add_stat_columns(q):
@ -193,3 +195,56 @@ async def load_shouts_by(_, info, options):
shouts_map[shout_id].stat['viewed'] = viewed_stat shouts_map[shout_id].stat['viewed'] = viewed_stat
return shouts return shouts
@query.field("myFeed")
@login_required
async def get_my_feed(_, info, options):
auth: AuthCredentials = info.context["request"].auth
user_id = auth.user_id
q = select(Shout).options(
joinedload(Shout.authors),
joinedload(Shout.topics),
).where(
Shout.deletedAt.is_(None)
)
q = q.join(
ShoutAuthor
).join(
AuthorFollower
).where(
AuthorFollower.follower == user_id
).join(
ShoutTopic
).join(
TopicFollower
).where(TopicFollower.follower == user_id)
q = add_stat_columns(q)
q = apply_filters(q, options.get("filters", {}), 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'))
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)
q = q.group_by(Shout.id).order_by(query_order_by).limit(limit).offset(offset)
shouts = []
with local_session() as session:
for [shout, reacted_stat, commented_stat, rating_stat] in session.execute(q).unique():
shouts.append(shout)
shout.stat = {
"viewed": 0,
"reacted": reacted_stat,
"commented": commented_stat,
"rating": rating_stat
}
return shouts

View File

@ -198,11 +198,15 @@ async def rate_user(_, info, rated_userslug, value):
# for mutation.field("follow") # for mutation.field("follow")
def author_follow(user_id, slug): def author_follow(user_id, slug):
with local_session() as session: try:
author = session.query(User).where(User.slug == slug).one() with local_session() as session:
af = AuthorFollower.create(follower=user_id, author=author.id) author = session.query(User).where(User.slug == slug).one()
session.add(af) af = AuthorFollower.create(follower=user_id, author=author.id)
session.commit() session.add(af)
session.commit()
return True
except:
return False
# for mutation.field("unfollow") # for mutation.field("unfollow")
@ -217,14 +221,11 @@ def author_unfollow(user_id, slug):
) )
).first() ).first()
) )
if not flw: if flw:
return {
"error": "Follower is not exist, cant unfollow"
}
else:
session.delete(flw) session.delete(flw)
session.commit() session.commit()
return {} return True
return False
@query.field("authorsAll") @query.field("authorsAll")

View File

@ -40,40 +40,49 @@ def add_reaction_stat_columns(q):
def reactions_follow(user_id, shout_id: int, auto=False): def reactions_follow(user_id, shout_id: int, auto=False):
with local_session() as session: try:
shout = session.query(Shout).where(Shout.id == shout_id).one() with local_session() as session:
shout = session.query(Shout).where(Shout.id == shout_id).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:
following = ShoutReactionsFollower.create(
follower=user_id,
shout=shout.id,
auto=auto
) )
session.add(following)
session.commit() if not following:
following = ShoutReactionsFollower.create(
follower=user_id,
shout=shout.id,
auto=auto
)
session.add(following)
session.commit()
return True
except:
return False
def reactions_unfollow(user_id: int, shout_id: int): def reactions_unfollow(user_id: int, shout_id: int):
with local_session() as session: try:
shout = session.query(Shout).where(Shout.id == shout_id).one() with local_session() as session:
shout = session.query(Shout).where(Shout.id == shout_id).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 following: if following:
session.delete(following) session.delete(following)
session.commit() session.commit()
return True
except:
pass
return False
def is_published_author(session, user_id): def is_published_author(session, user_id):

View File

@ -117,29 +117,36 @@ async def update_topic(_, _info, inp):
def topic_follow(user_id, slug): def topic_follow(user_id, slug):
with local_session() as session: try:
topic = session.query(Topic).where(Topic.slug == slug).one() 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.add(following)
session.commit() session.commit()
return True
except:
return False
def topic_unfollow(user_id, slug): def topic_unfollow(user_id, slug):
with local_session() as session: try:
sub = ( with local_session() as session:
session.query(TopicFollower).join(Topic).filter( sub = (
and_( session.query(TopicFollower).join(Topic).filter(
TopicFollower.follower == user_id, and_(
Topic.slug == slug TopicFollower.follower == user_id,
) Topic.slug == slug
).first() )
) ).first()
if not sub: )
raise Exception("[resolvers.topics] follower not exist") if sub:
else: session.delete(sub)
session.delete(sub) session.commit()
session.commit() return True
except:
pass
return False
@query.field("topicsRandom") @query.field("topicsRandom")

View File

@ -69,7 +69,6 @@ type Result {
members: [ChatMember] members: [ChatMember]
shout: Shout shout: Shout
shouts: [Shout] shouts: [Shout]
drafts: [DraftCollab]
author: Author author: Author
authors: [Author] authors: [Author]
reaction: Reaction reaction: Reaction
@ -78,6 +77,8 @@ type Result {
topics: [Topic] topics: [Topic]
community: Community community: Community
communities: [Community] communities: [Community]
draft: DraftCollab
drafts: [DraftCollab]
} }
enum ReactionStatus { enum ReactionStatus {
@ -207,6 +208,7 @@ type Mutation {
deleteDraft(draft: Int!): Result! deleteDraft(draft: Int!): Result!
inviteAccept(draft: Int!): Result! inviteAccept(draft: Int!): Result!
inviteAuthor(draft: Int!, author: Int!): Result! inviteAuthor(draft: Int!, author: Int!): Result!
draftToShout(draft: Int!): Result!
# following # following
follow(what: FollowingEntity!, slug: String!): Result! follow(what: FollowingEntity!, slug: String!): Result!
@ -302,6 +304,7 @@ type Query {
userFollowedTopics(slug: String!): [Topic]! userFollowedTopics(slug: String!): [Topic]!
authorsAll: [Author]! authorsAll: [Author]!
getAuthor(slug: String!): User getAuthor(slug: String!): User
myFeed(options: LoadShoutsOptions): [Shout]
# draft/collab # draft/collab
loadDrafts: [DraftCollab]! loadDrafts: [DraftCollab]!

51
services/following.py Normal file
View File

@ -0,0 +1,51 @@
import asyncio
class FollowingResult:
def __init__(self, event, kind, payload):
self.event = event
self.kind = kind
self.payload = payload
class Following:
queue = asyncio.Queue()
def __init__(self, kind, uid):
self.kind = kind # author topic shout chat
self.uid = uid
class FollowingManager:
lock = asyncio.Lock()
data = {
'author': [],
'topic': [],
'shout': [],
'chat': []
}
@staticmethod
async def register(kind, uid):
async with FollowingManager.lock:
FollowingManager[kind].append(uid)
@staticmethod
async def remove(kind, uid):
async with FollowingManager.lock:
FollowingManager[kind].remove(uid)
@staticmethod
async def push(kind, payload):
try:
async with FollowingManager.lock:
if kind == 'chat':
for chat in FollowingManager['chat']:
if payload.message["chatId"] == chat.uid:
chat.queue.put_nowait(payload)
else:
for entity in FollowingManager[kind]:
if payload.shout['createdBy'] == entity.uid:
entity.queue.put_nowait(payload)
except Exception as e:
print(Exception(e))

View File

@ -1,14 +0,0 @@
import asyncio
class MessageResult:
def __init__(self, status, message):
self.seen = status
self.message = message
class ChatFollowing:
queue = asyncio.Queue()
def __init__(self, chat_id):
self.chat_id = chat_id

View File

@ -1,23 +0,0 @@
import asyncio
class MessagesStorage:
lock = asyncio.Lock()
chats = []
@staticmethod
async def register_chat(chat):
async with MessagesStorage.lock:
MessagesStorage.chats.append(chat)
@staticmethod
async def remove_chat(chat):
async with MessagesStorage.lock:
MessagesStorage.chats.remove(chat)
@staticmethod
async def put(message_result):
async with MessagesStorage.lock:
for chat in MessagesStorage.chats:
if message_result.message["chatId"] == chat.chat_id:
chat.queue.put_nowait(message_result)