refactored-author-on-login-required
All checks were successful
Deploy on push / deploy (push) Successful in 23s

This commit is contained in:
Untone 2024-04-19 18:22:07 +03:00
parent 0ca6676474
commit b7d82d9cc5
18 changed files with 316 additions and 346 deletions

View File

@ -2,8 +2,8 @@ import time
from enum import Enum as Enumeration
from sqlalchemy import JSON, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy.exc import ProgrammingError
from sqlalchemy.orm import relationship
from orm.author import Author
from services.db import Base, engine

View File

@ -1,6 +1,5 @@
from orm.reaction import ReactionKind
PROPOSAL_REACTIONS = [
ReactionKind.ACCEPT.value,
ReactionKind.REJECT.value,

View File

@ -1,52 +1,24 @@
from resolvers.author import (
get_author,
get_author_followers,
get_author_follows,
get_author_follows_authors,
get_author_follows_topics,
get_author_id,
get_authors_all,
load_authors_by,
search_authors,
update_author,
)
from resolvers.author import (get_author, get_author_followers,
get_author_follows, get_author_follows_authors,
get_author_follows_topics, get_author_id,
get_authors_all, load_authors_by, search_authors,
update_author)
from resolvers.community import get_communities_all, get_community
from resolvers.editor import create_shout, delete_shout, update_shout
from resolvers.follower import (
follow,
get_shout_followers,
get_topic_followers,
unfollow,
)
from resolvers.notifier import (
load_notifications,
notification_mark_seen,
notifications_seen_after,
notifications_seen_thread,
)
from resolvers.follower import (follow, get_shout_followers,
get_topic_followers, unfollow)
from resolvers.notifier import (load_notifications, notification_mark_seen,
notifications_seen_after,
notifications_seen_thread)
from resolvers.rating import rate_author
from resolvers.reaction import (
create_reaction,
delete_reaction,
load_reactions_by,
load_shouts_followed,
update_reaction,
)
from resolvers.reader import (
get_shout,
load_shouts_by,
load_shouts_feed,
load_shouts_random_top,
load_shouts_random_topic,
load_shouts_search,
load_shouts_unrated,
)
from resolvers.topic import (
get_topic,
get_topics_all,
get_topics_by_author,
get_topics_by_community,
)
from resolvers.reaction import (create_reaction, delete_reaction,
load_reactions_by, load_shouts_followed,
update_reaction)
from resolvers.reader import (get_shout, load_shouts_by, load_shouts_feed,
load_shouts_random_top, load_shouts_random_topic,
load_shouts_search, load_shouts_unrated)
from resolvers.topic import (get_topic, get_topics_all, get_topics_by_author,
get_topics_by_community)
from services.triggers import events_register
events_register()

View File

@ -8,7 +8,8 @@ from sqlalchemy_searchable import search
from orm.author import Author, AuthorFollower
from orm.shout import ShoutAuthor, ShoutTopic
from orm.topic import Topic
from resolvers.stat import author_follows_authors, author_follows_topics, get_with_stat
from resolvers.stat import (author_follows_authors, author_follows_topics,
get_with_stat)
from services.auth import login_required
from services.cache import cache_author, cache_follower
from services.db import local_session

View File

@ -9,50 +9,57 @@ from services.schema import mutation
@mutation.field("accept_invite")
@login_required
async def accept_invite(_, info, invite_id: int):
user_id = info.context["user_id"]
# Check if the user exists
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if author:
info.context["user_id"]
author_dict = info.context["author"]
author_id = author_dict.get("id")
if author_id:
author_id = int(author_id)
# Check if the user exists
with local_session() as session:
# Check if the invite exists
invite = session.query(Invite).filter(Invite.id == invite_id).first()
if (
invite
and invite.author_d is author.id
and invite.author_id is author_id
and invite.status is InviteStatus.PENDING.value
):
# Add the user to the shout authors
shout = session.query(Shout).filter(Shout.id == invite.shout_id).first()
if shout:
if author not in shout.authors:
shout.authors.append(author)
session.delete(invite)
session.add(shout)
session.commit()
if author_id not in shout.authors:
author = (
session.query(Author).filter(Author.id == author_id).first()
)
if author:
shout.authors.append(author)
session.add(shout)
session.delete(invite)
session.commit()
return {"success": True, "message": "Invite accepted"}
else:
return {"error": "Shout not found"}
else:
return {"error": "Invalid invite or already accepted/rejected"}
else:
return {"error": "User not found"}
else:
return {"error": "Unauthorized"}
@mutation.field("reject_invite")
@login_required
async def reject_invite(_, info, invite_id: int):
user_id = info.context["user_id"]
info.context["user_id"]
author_dict = info.context["author"]
author_id = author_dict.get("id")
# Check if the user exists
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if author:
if author_id:
# Check if the user exists
with local_session() as session:
author_id = int(author_id)
# Check if the invite exists
invite = session.query(Invite).filter(Invite.id == invite_id).first()
if (
invite
and invite.author_id is author.id
and invite.author_id is author_id
and invite.status is InviteStatus.PENDING.value
):
# Delete the invite
@ -61,23 +68,21 @@ async def reject_invite(_, info, invite_id: int):
return {"success": True, "message": "Invite rejected"}
else:
return {"error": "Invalid invite or already accepted/rejected"}
else:
return {"error": "User not found"}
return {"error": "User not found"}
@mutation.field("create_invite")
@login_required
async def create_invite(_, info, slug: str = "", author_id: int = 0):
user_id = info.context["user_id"]
# Check if the inviter is the owner of the shout
with local_session() as session:
shout = session.query(Shout).filter(Shout.slug == slug).first()
inviter = session.query(Author).filter(Author.user == user_id).first()
if inviter and shout and shout.authors and inviter.id is shout.created_by:
# Check if the author is a valid author
author = session.query(Author).filter(Author.id == author_id).first()
if author:
author_dict = info.context["author"]
author_id = author_dict.get("id")
if author_id:
# Check if the inviter is the owner of the shout
with local_session() as session:
shout = session.query(Shout).filter(Shout.slug == slug).first()
inviter = session.query(Author).filter(Author.user == user_id).first()
if inviter and shout and shout.authors and inviter.id is shout.created_by:
# Check if an invite already exists
existing_invite = (
session.query(Invite)
@ -105,8 +110,8 @@ async def create_invite(_, info, slug: str = "", author_id: int = 0):
return {"error": None, "invite": new_invite}
else:
return {"error": "Invalid author"}
else:
return {"error": "Access denied"}
else:
return {"error": "Access denied"}
@mutation.field("remove_author")
@ -130,18 +135,20 @@ async def remove_author(_, info, slug: str = "", author_id: int = 0):
@mutation.field("remove_invite")
@login_required
async def remove_invite(_, info, invite_id: int):
user_id = info.context["user_id"]
info.context["user_id"]
# Check if the user exists
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if author:
author_dict = info.context["author"]
author_id = author_dict.get("id")
if author_id:
# Check if the user exists
with local_session() as session:
author_id == int(author_id)
# Check if the invite exists
invite = session.query(Invite).filter(Invite.id == invite_id).first()
if isinstance(invite, Invite):
shout = session.query(Shout).filter(Shout.id == invite.shout_id).first()
if shout and shout.deleted_at is None and invite:
if invite.inviter_id is author.id or author.id is shout.created_by:
if invite.inviter_id is author_id or author_id == shout.created_by:
if invite.status is InviteStatus.PENDING.value:
# Delete the invite
session.delete(invite)
@ -149,5 +156,5 @@ async def remove_invite(_, info, invite_id: int):
return {}
else:
return {"error": "Invalid invite or already accepted/rejected"}
else:
return {"error": "Author not found"}
else:
return {"error": "Author not found"}

View File

@ -4,7 +4,6 @@ from sqlalchemy import and_, desc, select
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.functions import coalesce
from orm.author import Author
from orm.rating import is_negative, is_positive
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic
@ -24,6 +23,8 @@ from services.search import search_service
async def get_my_shout(_, info, shout_id: int):
with local_session() as session:
user_id = info.context.get("user_id", "")
author_dict = info.context["author"]
author_id = author_dict.get("id")
if not user_id:
return {"error": "unauthorized", "shout": None}
shout = (
@ -36,12 +37,11 @@ async def get_my_shout(_, info, shout_id: int):
if not shout:
return {"error": "no shout found", "shout": None}
if not bool(shout.published_at):
author = session.query(Author).filter(Author.user == user_id).first()
if not author:
if not author_id:
return {"error": "no author found", "shout": None}
roles = info.context.get("roles", [])
if "editor" not in roles and not filter(
lambda x: x.id == author.id, [x for x in shout.authors]
lambda x: x.id == int(author_id), [x for x in shout.authors]
):
return {"error": "forbidden", "shout": None}
return {"error": None, "shout": shout}
@ -50,15 +50,18 @@ async def get_my_shout(_, info, shout_id: int):
@query.field("get_shouts_drafts")
@login_required
async def get_shouts_drafts(_, info):
user_id = info.context.get("user_id")
info.context.get("user_id")
author_dict = info.context["author"]
author_id = author_dict.get("id")
shouts = []
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if author:
if author_id:
q = (
select(Shout)
.options(joinedload(Shout.authors), joinedload(Shout.topics))
.filter(and_(Shout.deleted_at.is_(None), Shout.created_by == author.id))
.filter(
and_(Shout.deleted_at.is_(None), Shout.created_by == int(author_id))
)
.filter(Shout.published_at.is_(None))
.order_by(desc(coalesce(Shout.updated_at, Shout.created_at)))
.group_by(Shout.id)
@ -71,67 +74,68 @@ async def get_shouts_drafts(_, info):
@login_required
async def create_shout(_, info, inp):
user_id = info.context.get("user_id")
if user_id:
author_dict = info.context["author"]
author_id = author_dict.get("id")
if user_id and author_id:
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if isinstance(author, Author):
current_time = int(time.time())
slug = inp.get("slug") or f"draft-{current_time}"
shout_dict = {
"title": inp.get("title", ""),
"subtitle": inp.get("subtitle", ""),
"lead": inp.get("lead", ""),
"description": inp.get("description", ""),
"body": inp.get("body", ""),
"layout": inp.get("layout", "article"),
"created_by": author.id,
"authors": [],
"slug": slug,
"topics": inp.get("topics", []),
"published_at": None,
"created_at": current_time, # Set created_at as Unix timestamp
}
author_id = int(author_id)
current_time = int(time.time())
slug = inp.get("slug") or f"draft-{current_time}"
shout_dict = {
"title": inp.get("title", ""),
"subtitle": inp.get("subtitle", ""),
"lead": inp.get("lead", ""),
"description": inp.get("description", ""),
"body": inp.get("body", ""),
"layout": inp.get("layout", "article"),
"created_by": author_id,
"authors": [],
"slug": slug,
"topics": inp.get("topics", []),
"published_at": None,
"created_at": current_time, # Set created_at as Unix timestamp
}
same_slug_shout = (
session.query(Shout)
.filter(Shout.slug == shout_dict.get("slug"))
.first()
)
c = 1
while same_slug_shout is not None:
same_slug_shout = (
session.query(Shout)
.filter(Shout.slug == shout_dict.get("slug"))
.first()
)
c = 1
while same_slug_shout is not None:
same_slug_shout = (
session.query(Shout)
.filter(Shout.slug == shout_dict.get("slug"))
.first()
)
c += 1
shout_dict["slug"] += f"-{c}"
new_shout = Shout(**shout_dict)
session.add(new_shout)
c += 1
shout_dict["slug"] += f"-{c}"
new_shout = Shout(**shout_dict)
session.add(new_shout)
session.commit()
# NOTE: requesting new shout back
shout = session.query(Shout).where(Shout.slug == slug).first()
if shout:
sa = ShoutAuthor(shout=shout.id, author=author_id)
session.add(sa)
topics = (
session.query(Topic)
.filter(Topic.slug.in_(inp.get("topics", [])))
.all()
)
for topic in topics:
t = ShoutTopic(topic=topic.id, shout=shout.id)
session.add(t)
session.commit()
# NOTE: requesting new shout back
shout = session.query(Shout).where(Shout.slug == slug).first()
if shout:
sa = ShoutAuthor(shout=shout.id, author=author.id)
session.add(sa)
reactions_follow(author_id, shout.id, True)
topics = (
session.query(Topic)
.filter(Topic.slug.in_(inp.get("topics", [])))
.all()
)
for topic in topics:
t = ShoutTopic(topic=topic.id, shout=shout.id)
session.add(t)
# notifier
# await notify_shout(shout_dict, 'create')
session.commit()
reactions_follow(author.id, shout.id, True)
# notifier
# await notify_shout(shout_dict, 'create')
return {"shout": shout}
return {"shout": shout}
return {"error": "cant create shout" if user_id else "unauthorized"}
@ -220,6 +224,8 @@ def patch_topics(session, shout, topics_input):
async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
user_id = info.context.get("user_id")
roles = info.context.get("roles", [])
author_dict = info.context["author"]
author_id = author_dict.get("id")
shout_input = shout_input or {}
current_time = int(time.time())
shout_id = shout_id or shout_input.get("id", shout_id)
@ -228,9 +234,8 @@ async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
return {"error": "unauthorized"}
try:
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
if author:
logger.info(f"author for shout#{shout_id} detected {author.dict()}")
if author_id:
logger.info(f"author for shout#{shout_id} detected author #{author_id}")
shout_by_id = session.query(Shout).filter(Shout.id == shout_id).first()
if not shout_by_id:
return {"error": "shout not found"}
@ -249,7 +254,7 @@ async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
if (
filter(
lambda x: x.id == author.id, [x for x in shout_by_id.authors]
lambda x: x.id == author_id, [x for x in shout_by_id.authors]
)
or "editor" in roles
):
@ -297,28 +302,29 @@ async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
@login_required
async def delete_shout(_, info, shout_id: int):
user_id = info.context.get("user_id")
roles = info.context.get("roles")
if user_id:
roles = info.context.get("roles", [])
author_dict = info.context["author"]
author_id = author_dict.get("id")
if user_id and author_id:
author_id = int(author_id)
with local_session() as session:
author = session.query(Author).filter(Author.user == user_id).first()
shout = session.query(Shout).filter(Shout.id == shout_id).first()
if not shout:
if not isinstance(shout, Shout):
return {"error": "invalid shout id"}
if author and shout:
# NOTE: only owner and editor can mark the shout as deleted
if shout.created_by == author.id or "editor" in roles:
for author_id in shout.authors:
reactions_unfollow(author_id, shout_id)
shout_dict = shout.dict()
# NOTE: only owner and editor can mark the shout as deleted
if shout_dict["created_by"] == author_id or "editor" in roles:
for author_id in shout.authors:
reactions_unfollow(author_id, shout_id)
shout_dict = shout.dict()
shout_dict["deleted_at"] = int(time.time())
Shout.update(shout, shout_dict)
session.add(shout)
session.commit()
await notify_shout(shout_dict, "delete")
return {"error": None}
else:
return {"error": "access denied"}
shout_dict["deleted_at"] = int(time.time())
Shout.update(shout, shout_dict)
session.add(shout)
session.commit()
await notify_shout(shout_dict, "delete")
return {"error": None}
else:
return {"error": "access denied"}
def handle_proposing(session, r, shout):

View File

@ -11,7 +11,8 @@ from orm.community import Community
from orm.reaction import Reaction
from orm.shout import Shout, ShoutReactionsFollower
from orm.topic import Topic, TopicFollower
from resolvers.stat import author_follows_authors, author_follows_topics, get_with_stat
from resolvers.stat import (author_follows_authors, author_follows_topics,
get_with_stat)
from services.auth import login_required
from services.cache import DEFAULT_FOLLOWS, cache_follower
from services.db import local_session
@ -26,43 +27,42 @@ from services.schema import mutation, query
async def follow(_, info, what, slug):
error = None
user_id = info.context.get("user_id")
if not user_id:
follower_dict = info.context["author"]
follower_id = follower_dict.get("id")
if not user_id or not follower_id:
return {"error": "unauthorized"}
follower = local_session().query(Author).filter(Author.user == user_id).first()
if not follower:
return {"error": "cant find follower account"}
entity = what.lower()
follows = []
follows_str = await redis.execute("GET", f"author:{follower.id}:follows-{entity}s")
follows_str = await redis.execute("GET", f"author:{follower_id}:follows-{entity}s")
if isinstance(follows_str, str):
follows = json.loads(follows_str)
if not follower:
return {"error": "cant find follower"}
if not follows:
return {"error": "cant find following cache"}
if what == "AUTHOR":
error = author_follow(follower.id, slug)
follower_id = int(follower_id)
error = author_follow(follower_id, slug)
if not error:
result = get_with_stat(select(Author).where(Author.slug == slug))
if result:
[author] = result
if author:
await cache_follower(follower, author)
await notify_follower(follower.dict(), author.id, "follow")
await cache_follower(follower_dict, author.dict())
await notify_follower(follower_dict, author.id, "follow")
if not any(a["id"] == author.id for a in follows):
follows.append(author.dict())
elif what == "TOPIC":
error = topic_follow(follower.id, slug)
error = topic_follow(follower_id, slug)
elif what == "COMMUNITY":
# FIXME: when more communities
follows = local_session().execute(select(Community))
elif what == "SHOUT":
error = reactions_follow(follower.id, slug)
error = reactions_follow(follower_id, slug)
if error:
return {"error": error}
@ -76,38 +76,39 @@ async def unfollow(_, info, what, slug):
follows = []
error = None
user_id = info.context.get("user_id")
follower_dict = info.context["author"]
follower_id = follower_dict.get("id")
if not user_id:
return {"error": "unauthorized"}
follower = local_session().query(Author).filter(Author.user == user_id).first()
if not follower:
if not follower_id:
return {"error": "cant find follower account"}
if what == "AUTHOR":
error = author_unfollow(follower.id, slug)
error = author_unfollow(follower_id, slug)
# NOTE: after triggers should update cached stats
if not error:
logger.info(f"@{follower.slug} unfollowed @{slug}")
logger.info(f"@{follower_dict.get('slug')} unfollowed @{slug}")
author = local_session().query(Author).where(Author.slug == slug).first()
if isinstance(author, Author):
await cache_follower(follower, author, False)
await notify_follower(follower.dict(), author.id, "unfollow")
await cache_follower(follower_dict, author.dict(), False)
await notify_follower(follower_dict, author.id, "unfollow")
for idx, item in enumerate(follows):
if item["id"] == author.id:
follows.pop(idx) # Remove the author_dict from the follows list
break
elif what == "TOPIC":
error = topic_unfollow(follower.id, slug)
error = topic_unfollow(follower_id, slug)
elif what == "COMMUNITY":
follows = local_session().execute(select(Community))
elif what == "SHOUT":
error = reactions_unfollow(follower.id, slug)
error = reactions_unfollow(follower_id, slug)
entity = what.lower()
follows_str = await redis.execute("GET", f"author:{follower.id}:follows-{entity}s")
follows_str = await redis.execute("GET", f"author:{follower_id}:follows-{entity}s")
if isinstance(follows_str, str):
follows = json.loads(follows_str)
return {"error": error, f"{entity}s": follows}

View File

@ -8,12 +8,8 @@ from sqlalchemy.orm import aliased
from sqlalchemy.sql import not_
from orm.author import Author
from orm.notification import (
Notification,
NotificationAction,
NotificationEntity,
NotificationSeen,
)
from orm.notification import (Notification, NotificationAction,
NotificationEntity, NotificationSeen)
from orm.shout import Shout
from services.auth import login_required
from services.db import local_session
@ -213,7 +209,8 @@ def get_notifications_grouped(
@query.field("load_notifications")
@login_required
async def load_notifications(_, info, after: int, limit: int = 50, offset=0):
author_id = info.context.get("author_id")
author_dict = info.context.get("author")
author_id = author_dict.get("id")
error = None
total = 0
unread = 0
@ -238,7 +235,7 @@ async def load_notifications(_, info, after: int, limit: int = 50, offset=0):
@mutation.field("notification_mark_seen")
@login_required
async def notification_mark_seen(_, info, notification_id: int):
author_id = info.context.get("author_id")
author_id = info.context.get("author", {}).get("id")
if author_id:
with local_session() as session:
try:
@ -258,7 +255,7 @@ async def notifications_seen_after(_, info, after: int):
# TODO: use latest loaded notification_id as input offset parameter
error = None
try:
author_id = info.context.get("author_id")
author_id = info.context.get("author", {}).get("id")
if author_id:
with local_session() as session:
nnn = (
@ -283,7 +280,7 @@ async def notifications_seen_after(_, info, after: int):
@login_required
async def notifications_seen_thread(_, info, thread: str, after: int):
error = None
author_id = info.context.get("author_id")
author_id = info.context.get("author", {}).get("id")
if author_id:
[shout_id, reply_to_id] = thread.split(":")
with local_session() as session:

View File

@ -12,17 +12,17 @@ from services.schema import mutation
@mutation.field("rate_author")
@login_required
async def rate_author(_, info, rated_slug, value):
user_id = info.context["user_id"]
info.context["user_id"]
rater_id = info.context.get("author", {}).get("id")
with local_session() as session:
rater_id = int(rater_id)
rated_author = session.query(Author).filter(Author.slug == rated_slug).first()
rater = session.query(Author).filter(Author.slug == user_id).first()
if rater and rated_author:
if rater_id and rated_author:
rating: AuthorRating = (
session.query(AuthorRating)
.filter(
and_(
AuthorRating.rater == rater.id,
AuthorRating.rater == rater_id,
AuthorRating.author == rated_author.id,
)
)
@ -36,7 +36,7 @@ async def rate_author(_, info, rated_slug, value):
else:
try:
rating = AuthorRating(
rater=rater.id, author=rated_author.id, plus=value > 0
rater=rater_id, author=rated_author.id, plus=value > 0
)
session.add(rating)
session.commit()

View File

@ -1,17 +1,18 @@
import time
from typing import List
from resolvers.stat import update_author_stat
from sqlalchemy import and_, asc, case, desc, func, select, text
from sqlalchemy.orm import aliased, joinedload
from sqlalchemy.sql import union
from orm.author import Author
from orm.rating import PROPOSAL_REACTIONS, RATING_REACTIONS, is_negative, is_positive
from orm.rating import (PROPOSAL_REACTIONS, RATING_REACTIONS, is_negative,
is_positive)
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout
from resolvers.editor import handle_proposing
from resolvers.follower import reactions_follow
from resolvers.stat import update_author_stat
from services.auth import add_user_role, login_required
from services.db import local_session
from services.logger import root_logger as logger
@ -116,7 +117,7 @@ def set_unfeatured(session, shout_id):
session.commit()
async def _create_reaction(session, shout, author, reaction):
async def _create_reaction(session, shout, author_id: int, reaction):
r = Reaction(**reaction)
session.add(r)
session.commit()
@ -124,38 +125,38 @@ async def _create_reaction(session, shout, author, reaction):
# пересчет счетчика комментариев
if str(r.kind) == ReactionKind.COMMENT.value:
await update_author_stat(author)
await update_author_stat(author_id)
# collaborative editing
if (
rdict.get("reply_to")
and r.kind in PROPOSAL_REACTIONS
and author.id in shout.authors
and author_id in shout.authors
):
handle_proposing(session, r, shout)
# рейтинг и саморегуляция
if r.kind in RATING_REACTIONS:
# self-regultaion mechanics
if check_to_unfeature(session, author.id, r):
if check_to_unfeature(session, author_id, r):
set_unfeatured(session, shout.id)
elif check_to_feature(session, author.id, r):
elif check_to_feature(session, author_id, r):
await set_featured(session, shout.id)
# follow if liked
if r.kind == ReactionKind.LIKE.value:
try:
# reactions auto-following
reactions_follow(author.id, reaction["shout"], True)
reactions_follow(author_id, reaction["shout"], True)
except Exception:
pass
# обновление счетчика комментариев в кеше
if str(r.kind) == ReactionKind.COMMENT.value:
await update_author_stat(author)
await update_author_stat(author_id)
rdict["shout"] = shout.dict()
rdict["created_by"] = author.id
rdict["created_by"] = author_id
rdict["stat"] = {"commented": 0, "reacted": 0, "rating": 0}
# notifications call
@ -164,7 +165,7 @@ async def _create_reaction(session, shout, author, reaction):
return rdict
def prepare_new_rating(reaction: dict, shout_id: int, session, author: Author):
def prepare_new_rating(reaction: dict, shout_id: int, session, author_id: int):
kind = reaction.get("kind")
opposite_kind = (
ReactionKind.DISLIKE.value if is_positive(kind) else ReactionKind.LIKE.value
@ -173,7 +174,7 @@ def prepare_new_rating(reaction: dict, shout_id: int, session, author: Author):
q = select(Reaction).filter(
and_(
Reaction.shout == shout_id,
Reaction.created_by == author.id,
Reaction.created_by == author_id,
Reaction.kind.in_(RATING_REACTIONS),
)
)
@ -182,18 +183,18 @@ def prepare_new_rating(reaction: dict, shout_id: int, session, author: Author):
q = q.filter(Reaction.reply_to == reply_to)
rating_reactions = session.execute(q).all()
same_rating = filter(
lambda r: r.created_by == author.id and r.kind == opposite_kind,
lambda r: r.created_by == author_id and r.kind == opposite_kind,
rating_reactions,
)
opposite_rating = filter(
lambda r: r.created_by == author.id and r.kind == opposite_kind,
lambda r: r.created_by == author_id and r.kind == opposite_kind,
rating_reactions,
)
if same_rating:
return {"error": "You can't rate the same thing twice"}
elif opposite_rating:
return {"error": "Remove opposite vote first"}
elif filter(lambda r: r.created_by == author.id, rating_reactions):
elif filter(lambda r: r.created_by == author_id, rating_reactions):
return {"error": "You can't rate your own thing"}
return
@ -202,7 +203,8 @@ def prepare_new_rating(reaction: dict, shout_id: int, session, author: Author):
@login_required
async def create_reaction(_, info, reaction):
logger.debug(f"{info.context} for {reaction}")
user_id = info.context.get("user_id")
info.context.get("user_id")
author_id = info.context.get("author", {}).get("id")
shout_id = reaction.get("shout")
if not shout_id:
@ -211,9 +213,8 @@ async def create_reaction(_, info, reaction):
try:
with local_session() as session:
shout = session.query(Shout).filter(Shout.id == shout_id).first()
author = session.query(Author).filter(Author.user == user_id).first()
if shout and author:
reaction["created_by"] = author.id
if shout and author_id:
reaction["created_by"] = int(author_id)
kind = reaction.get("kind")
if not kind and isinstance(reaction.get("body"), str):
@ -224,12 +225,12 @@ async def create_reaction(_, info, reaction):
if kind in RATING_REACTIONS:
error_result = prepare_new_rating(
reaction, shout_id, session, author
reaction, shout_id, session, author_id
)
if error_result:
return error_result
rdict = await _create_reaction(session, shout, author, reaction)
rdict = await _create_reaction(session, shout, author_id, reaction)
# TODO: call recount ratings periodically
@ -315,13 +316,14 @@ async def update_reaction(_, info, reaction):
async def delete_reaction(_, info, reaction_id: int):
logger.debug(f"{info.context} for {reaction_id}")
user_id = info.context.get("user_id")
author_id = info.context.get("author", {}).get("id")
roles = info.context.get("roles", [])
if user_id:
with local_session() as session:
try:
author = session.query(Author).filter(Author.user == user_id).one()
r = session.query(Reaction).filter(Reaction.id == reaction_id).one()
if r.created_by != author.id and "editor" not in roles:
if r.created_by != author_id and "editor" not in roles:
return {"error": "access denied"}
logger.debug(f"{user_id} user removing his #{reaction_id} reaction")

View File

@ -1,6 +1,7 @@
from sqlalchemy import bindparam, distinct, or_, text
from sqlalchemy.orm import aliased, joinedload
from sqlalchemy.sql.expression import and_, asc, case, desc, func, nulls_last, select
from sqlalchemy.sql.expression import (and_, asc, case, desc, func, nulls_last,
select)
from orm.author import Author, AuthorFollower
from orm.reaction import Reaction, ReactionKind
@ -25,31 +26,26 @@ def query_shouts():
def filter_my(info, session, q):
reader_id = None
user_id = None
if isinstance(info.context, dict):
user_id = info.context.get("user_id")
if user_id:
reader = session.query(Author).filter(Author.user == user_id).first()
if reader:
reader_followed_authors = select(AuthorFollower.author).where(
AuthorFollower.follower == reader.id
)
reader_followed_topics = select(TopicFollower.topic).where(
TopicFollower.follower == reader.id
)
user_id = info.context.get("user_id")
reader_id = info.context.get("author", {}).get("id")
if user_id and reader_id:
reader_followed_authors = select(AuthorFollower.author).where(
AuthorFollower.follower == reader_id
)
reader_followed_topics = select(TopicFollower.topic).where(
TopicFollower.follower == reader_id
)
subquery = (
select(Shout.id)
.where(Shout.id == ShoutAuthor.shout)
.where(Shout.id == ShoutTopic.shout)
.where(
(ShoutAuthor.author.in_(reader_followed_authors))
| (ShoutTopic.topic.in_(reader_followed_topics))
)
subquery = (
select(Shout.id)
.where(Shout.id == ShoutAuthor.shout)
.where(Shout.id == ShoutTopic.shout)
.where(
(ShoutAuthor.author.in_(reader_followed_authors))
| (ShoutTopic.topic.in_(reader_followed_topics))
)
q = q.filter(Shout.id.in_(subquery))
reader_id = reader.id
)
q = q.filter(Shout.id.in_(subquery))
return q, reader_id

View File

@ -5,8 +5,8 @@ from orm.author import Author, AuthorFollower
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic, TopicFollower
from services.db import local_session
from services.cache import cache_author
from services.db import local_session
from services.logger import root_logger as logger
@ -167,8 +167,8 @@ def author_follows_topics(author_id: int):
return get_with_stat(q)
async def update_author_stat(author: Author):
author_with_stat = get_with_stat(select(Author).where(Author.id == author.id))
async def update_author_stat(author_id: int):
author_with_stat = get_with_stat(select(Author).where(Author.id == author_id))
if isinstance(author_with_stat, Author):
author_dict = author_with_stat.dict()
await cache_author(author_dict)

View File

@ -1,12 +1,24 @@
import json
from functools import wraps
import httpx
from starlette.exceptions import HTTPException
from services.logger import root_logger as logger
from services.rediscache import redis
from settings import ADMIN_SECRET, AUTH_URL
async def get_author_by_user(user: str):
author = None
redis_key = f"user:{user}"
result = await redis.execute("GET", redis_key)
if isinstance(result, str):
author = json.loads(result)
return author
async def request_data(gql, headers=None):
if headers is None:
headers = {"Content-Type": "application/json"}
@ -78,32 +90,13 @@ def login_required(f):
async def decorated_function(*args, **kwargs):
info = args[1]
req = info.context.get("request")
authorized = await check_auth(req)
if authorized:
logger.info(authorized)
user_id, user_roles = authorized
if user_id and user_roles:
logger.info(f" got {user_id} roles: {user_roles}")
info.context["user_id"] = user_id.strip()
info.context["roles"] = user_roles
user_id, user_roles = await check_auth(req)
if user_id and user_roles:
logger.info(f" got {user_id} roles: {user_roles}")
info.context["user_id"] = user_id.strip()
info.context["roles"] = user_roles
author = await get_author_by_user(user_id)
info.context["author"] = author
return await f(*args, **kwargs)
return decorated_function
def auth_request(f):
@wraps(f)
async def decorated_function(*args, **kwargs):
req = args[0]
authorized = await check_auth(req)
if authorized:
user_id, user_roles = authorized
if user_id and user_roles:
logger.info(f" got {user_id} roles: {user_roles}")
req["user_id"] = user_id.strip()
req["roles"] = user_roles
return await f(*args, **kwargs)
else:
raise HTTPException(status_code=401, detail="Unauthorized")
return decorated_function

View File

@ -1,8 +1,5 @@
import json
from orm.author import Author
from orm.topic import Topic
from services.encoders import CustomJSONEncoder
from services.rediscache import redis
@ -67,55 +64,57 @@ async def cache_author(author: dict):
followed_author_followers.append(author)
async def cache_follows(follower: Author, entity_type: str, entity, is_insert=True):
async def cache_follows(follower: dict, entity_type: str, entity: dict, is_insert=True):
# prepare
follows = []
redis_key = f"author:{follower.id}:follows-{entity_type}s"
follows_str = await redis.execute("GET", redis_key)
if isinstance(follows_str, str):
follows = json.loads(follows_str)
if is_insert:
follows.append(entity)
else:
entity_id = entity.get("id")
if not entity_id:
raise Exception("wrong entity")
# Remove the entity from follows
follows = [e for e in follows if e["id"] != entity_id]
follower_id = follower.get("id")
if follower_id:
redis_key = f"author:{follower_id}:follows-{entity_type}s"
follows_str = await redis.execute("GET", redis_key)
if isinstance(follows_str, str):
follows = json.loads(follows_str)
if is_insert:
follows.append(entity)
else:
entity_id = entity.get("id")
if not entity_id:
raise Exception("wrong entity")
# Remove the entity from follows
follows = [e for e in follows if e["id"] != entity_id]
# update follows cache
updated_data = [t.dict() if isinstance(t, Topic) else t for t in follows]
payload = json.dumps(updated_data, cls=CustomJSONEncoder)
await redis.execute("SET", redis_key, payload)
# update follows cache
payload = json.dumps(follows, cls=CustomJSONEncoder)
await redis.execute("SET", redis_key, payload)
# update follower's stats everywhere
author_str = await redis.execute("GET", f"author:{follower.id}")
if isinstance(author_str, str):
author = json.loads(author_str)
author["stat"][f"{entity_type}s"] = len(updated_data)
await cache_author(author)
# update follower's stats everywhere
author_str = await redis.execute("GET", f"author:{follower_id}")
if isinstance(author_str, str):
author = json.loads(author_str)
author["stat"][f"{entity_type}s"] = len(follows)
await cache_author(author)
return follows
async def cache_follower(follower: Author, author: Author, is_insert=True):
redis_key = f"author:{author.id}:followers"
followers_str = await redis.execute("GET", redis_key)
async def cache_follower(follower: dict, author: dict, is_insert=True):
author_id = author.get("id")
follower_id = follower.get("id")
followers = []
if isinstance(followers_str, str):
followers = json.loads(followers_str)
if is_insert:
# Remove the entity from followers
followers = [e for e in followers if e["id"] != author.id]
else:
followers.append(follower)
updated_followers = [
f.dict() if isinstance(f, Author) else f for f in followers
]
payload = json.dumps(updated_followers, cls=CustomJSONEncoder)
await redis.execute("SET", redis_key, payload)
author_str = await redis.execute("GET", f"author:{follower.id}")
if isinstance(author_str, str):
author = json.loads(author_str)
author["stat"]["followers"] = len(updated_followers)
await cache_author(author)
if author_id and follower_id:
redis_key = f"author:{author_id}:followers"
followers_str = await redis.execute("GET", redis_key)
followers = []
if isinstance(followers_str, str):
followers = json.loads(followers_str)
if is_insert:
# Remove the entity from followers
followers = [e for e in followers if e["id"] != author_id]
else:
followers.append(follower)
payload = json.dumps(followers, cls=CustomJSONEncoder)
await redis.execute("SET", redis_key, payload)
author_str = await redis.execute("GET", f"author:{follower_id}")
if isinstance(author_str, str):
author = json.loads(author_str)
author["stat"]["followers"] = len(followers)
await cache_author(author)
return followers

View File

@ -5,7 +5,8 @@ import traceback
import warnings
from typing import Any, Callable, Dict, TypeVar
from sqlalchemy import JSON, Column, Engine, Integer, create_engine, event, exc, inspect
from sqlalchemy import (JSON, Column, Engine, Integer, create_engine, event,
exc, inspect)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session, configure_mappers
from sqlalchemy.sql.schema import Table

View File

@ -2,8 +2,8 @@ import json
from orm.notification import Notification
from services.db import local_session
from services.rediscache import redis
from services.logger import root_logger as logger
from services.rediscache import redis
def save_notification(action: str, entity: str, payload):

View File

@ -6,12 +6,12 @@ from sqlalchemy import event, select
from orm.author import Author, AuthorFollower
from orm.reaction import Reaction
from orm.shout import Shout, ShoutAuthor
from orm.topic import TopicFollower, Topic
from orm.topic import Topic, TopicFollower
from resolvers.stat import get_with_stat
from services.cache import cache_author, cache_follower, cache_follows
from services.encoders import CustomJSONEncoder
from services.rediscache import redis
from services.logger import root_logger as logger
from services.cache import cache_author, cache_follows, cache_follower
from services.rediscache import redis
DEFAULT_FOLLOWS = {
"topics": [],
@ -31,8 +31,8 @@ async def handle_author_follower_change(
if follower and author:
await cache_author(author.dict())
await cache_author(follower.dict())
await cache_follows(follower, "author", author.dict(), is_insert)
await cache_follower(follower, author, is_insert)
await cache_follows(follower.dict(), "author", author.dict(), is_insert)
await cache_follower(follower.dict(), author.dict(), is_insert)
async def handle_topic_follower_change(
@ -48,7 +48,7 @@ async def handle_topic_follower_change(
await redis.execute(
"SET", f"topic:{topic.id}", json.dumps(topic.dict(), cls=CustomJSONEncoder)
)
await cache_follows(follower, "topic", topic.dict(), is_insert)
await cache_follows(follower.dict(), "topic", topic.dict(), is_insert)
# handle_author_follow and handle_topic_follow -> cache_author, cache_follows, cache_followers

View File

@ -7,12 +7,8 @@ from typing import Dict
# ga
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
DateRange,
Dimension,
Metric,
RunReportRequest,
)
from google.analytics.data_v1beta.types import (DateRange, Dimension, Metric,
RunReportRequest)
from orm.author import Author
from orm.shout import Shout, ShoutAuthor, ShoutTopic