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 enum import Enum as Enumeration
from sqlalchemy import JSON, Column, ForeignKey, Integer, String from sqlalchemy import JSON, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy.exc import ProgrammingError from sqlalchemy.exc import ProgrammingError
from sqlalchemy.orm import relationship
from orm.author import Author from orm.author import Author
from services.db import Base, engine from services.db import Base, engine

View File

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

View File

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

View File

@ -8,7 +8,8 @@ from sqlalchemy_searchable import search
from orm.author import Author, AuthorFollower from orm.author import Author, AuthorFollower
from orm.shout import ShoutAuthor, ShoutTopic from orm.shout import ShoutAuthor, ShoutTopic
from orm.topic import Topic 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.auth import login_required
from services.cache import cache_author, cache_follower from services.cache import cache_author, cache_follower
from services.db import local_session from services.db import local_session

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,24 @@
import json
from functools import wraps from functools import wraps
import httpx import httpx
from starlette.exceptions import HTTPException
from services.logger import root_logger as logger from services.logger import root_logger as logger
from services.rediscache import redis
from settings import ADMIN_SECRET, AUTH_URL 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): async def request_data(gql, headers=None):
if headers is None: if headers is None:
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
@ -78,32 +90,13 @@ def login_required(f):
async def decorated_function(*args, **kwargs): async def decorated_function(*args, **kwargs):
info = args[1] info = args[1]
req = info.context.get("request") req = info.context.get("request")
authorized = await check_auth(req) user_id, user_roles = await check_auth(req)
if authorized: if user_id and user_roles:
logger.info(authorized) logger.info(f" got {user_id} roles: {user_roles}")
user_id, user_roles = authorized info.context["user_id"] = user_id.strip()
if user_id and user_roles: info.context["roles"] = user_roles
logger.info(f" got {user_id} roles: {user_roles}") author = await get_author_by_user(user_id)
info.context["user_id"] = user_id.strip() info.context["author"] = author
info.context["roles"] = user_roles
return await f(*args, **kwargs) return await f(*args, **kwargs)
return decorated_function 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 import json
from orm.author import Author
from orm.topic import Topic
from services.encoders import CustomJSONEncoder from services.encoders import CustomJSONEncoder
from services.rediscache import redis from services.rediscache import redis
@ -67,55 +64,57 @@ async def cache_author(author: dict):
followed_author_followers.append(author) 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 # prepare
follows = [] follows = []
redis_key = f"author:{follower.id}:follows-{entity_type}s" follower_id = follower.get("id")
follows_str = await redis.execute("GET", redis_key) if follower_id:
if isinstance(follows_str, str): redis_key = f"author:{follower_id}:follows-{entity_type}s"
follows = json.loads(follows_str) follows_str = await redis.execute("GET", redis_key)
if is_insert: if isinstance(follows_str, str):
follows.append(entity) follows = json.loads(follows_str)
else: if is_insert:
entity_id = entity.get("id") follows.append(entity)
if not entity_id: else:
raise Exception("wrong entity") entity_id = entity.get("id")
# Remove the entity from follows if not entity_id:
follows = [e for e in follows if e["id"] != 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 # update follows cache
updated_data = [t.dict() if isinstance(t, Topic) else t for t in follows] payload = json.dumps(follows, cls=CustomJSONEncoder)
payload = json.dumps(updated_data, cls=CustomJSONEncoder) await redis.execute("SET", redis_key, payload)
await redis.execute("SET", redis_key, payload)
# update follower's stats everywhere # update follower's stats everywhere
author_str = await redis.execute("GET", f"author:{follower.id}") author_str = await redis.execute("GET", f"author:{follower_id}")
if isinstance(author_str, str): if isinstance(author_str, str):
author = json.loads(author_str) author = json.loads(author_str)
author["stat"][f"{entity_type}s"] = len(updated_data) author["stat"][f"{entity_type}s"] = len(follows)
await cache_author(author) await cache_author(author)
return follows return follows
async def cache_follower(follower: Author, author: Author, is_insert=True): async def cache_follower(follower: dict, author: dict, is_insert=True):
redis_key = f"author:{author.id}:followers" author_id = author.get("id")
followers_str = await redis.execute("GET", redis_key) follower_id = follower.get("id")
followers = [] followers = []
if isinstance(followers_str, str): if author_id and follower_id:
followers = json.loads(followers_str) redis_key = f"author:{author_id}:followers"
if is_insert: followers_str = await redis.execute("GET", redis_key)
# Remove the entity from followers followers = []
followers = [e for e in followers if e["id"] != author.id] if isinstance(followers_str, str):
else: followers = json.loads(followers_str)
followers.append(follower) if is_insert:
updated_followers = [ # Remove the entity from followers
f.dict() if isinstance(f, Author) else f for f in followers followers = [e for e in followers if e["id"] != author_id]
] else:
payload = json.dumps(updated_followers, cls=CustomJSONEncoder) followers.append(follower)
await redis.execute("SET", redis_key, payload) payload = json.dumps(followers, cls=CustomJSONEncoder)
author_str = await redis.execute("GET", f"author:{follower.id}") await redis.execute("SET", redis_key, payload)
if isinstance(author_str, str): author_str = await redis.execute("GET", f"author:{follower_id}")
author = json.loads(author_str) if isinstance(author_str, str):
author["stat"]["followers"] = len(updated_followers) author = json.loads(author_str)
await cache_author(author) author["stat"]["followers"] = len(followers)
await cache_author(author)
return followers return followers

View File

@ -5,7 +5,8 @@ import traceback
import warnings import warnings
from typing import Any, Callable, Dict, TypeVar 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.ext.declarative import declarative_base
from sqlalchemy.orm import Session, configure_mappers from sqlalchemy.orm import Session, configure_mappers
from sqlalchemy.sql.schema import Table from sqlalchemy.sql.schema import Table

View File

@ -2,8 +2,8 @@ import json
from orm.notification import Notification from orm.notification import Notification
from services.db import local_session from services.db import local_session
from services.rediscache import redis
from services.logger import root_logger as logger from services.logger import root_logger as logger
from services.rediscache import redis
def save_notification(action: str, entity: str, payload): 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.author import Author, AuthorFollower
from orm.reaction import Reaction from orm.reaction import Reaction
from orm.shout import Shout, ShoutAuthor 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 resolvers.stat import get_with_stat
from services.cache import cache_author, cache_follower, cache_follows
from services.encoders import CustomJSONEncoder from services.encoders import CustomJSONEncoder
from services.rediscache import redis
from services.logger import root_logger as logger 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 = { DEFAULT_FOLLOWS = {
"topics": [], "topics": [],
@ -31,8 +31,8 @@ async def handle_author_follower_change(
if follower and author: if follower and author:
await cache_author(author.dict()) await cache_author(author.dict())
await cache_author(follower.dict()) await cache_author(follower.dict())
await cache_follows(follower, "author", author.dict(), is_insert) await cache_follows(follower.dict(), "author", author.dict(), is_insert)
await cache_follower(follower, author, is_insert) await cache_follower(follower.dict(), author.dict(), is_insert)
async def handle_topic_follower_change( async def handle_topic_follower_change(
@ -48,7 +48,7 @@ async def handle_topic_follower_change(
await redis.execute( await redis.execute(
"SET", f"topic:{topic.id}", json.dumps(topic.dict(), cls=CustomJSONEncoder) "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 # handle_author_follow and handle_topic_follow -> cache_author, cache_follows, cache_followers

View File

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