This commit is contained in:
@@ -82,35 +82,6 @@ async def update_profile(_, info, profile):
|
||||
return {'error': None, 'author': author}
|
||||
|
||||
|
||||
# for mutation.field("follow")
|
||||
def author_follow(follower_id, slug):
|
||||
try:
|
||||
with local_session() as session:
|
||||
author = session.query(Author).where(Author.slug == slug).one()
|
||||
af = AuthorFollower(follower=follower_id, author=author.id)
|
||||
session.add(af)
|
||||
session.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# for mutation.field("unfollow")
|
||||
def author_unfollow(follower_id, slug):
|
||||
with local_session() as session:
|
||||
flw = (
|
||||
session.query(AuthorFollower)
|
||||
.join(Author, Author.id == AuthorFollower.author)
|
||||
.filter(and_(AuthorFollower.follower == follower_id, Author.slug == slug))
|
||||
.first()
|
||||
)
|
||||
if flw:
|
||||
session.delete(flw)
|
||||
session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# TODO: caching query
|
||||
@query.field('get_authors_all')
|
||||
async def get_authors_all(_, _info):
|
||||
|
@@ -4,11 +4,14 @@ from sqlalchemy import and_, select
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from orm.author import Author
|
||||
from orm.reaction import Reaction, ReactionKind
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility
|
||||
from orm.topic import Topic
|
||||
from resolvers.reaction import reactions_follow, reactions_unfollow
|
||||
from resolvers.follower import reactions_follow, reactions_unfollow
|
||||
from resolvers.rater import is_negative, is_positive
|
||||
from services.auth import login_required
|
||||
from services.db import local_session
|
||||
from services.diff import apply_diff, get_diff
|
||||
from services.notify import notify_shout
|
||||
from services.schema import mutation, query
|
||||
from services.search import search_service
|
||||
@@ -187,7 +190,8 @@ async def update_shout( # noqa: C901
|
||||
|
||||
if not publish:
|
||||
await notify_shout(shout_dict, 'update')
|
||||
if shout.visibility is ShoutVisibility.COMMUNITY.value or shout.visibility is ShoutVisibility.PUBLIC.value:
|
||||
else:
|
||||
await notify_shout(shout_dict, 'published')
|
||||
# search service indexing
|
||||
search_service.index(shout)
|
||||
|
||||
@@ -217,3 +221,30 @@ async def delete_shout(_, info, shout_id):
|
||||
session.commit()
|
||||
await notify_shout(shout_dict, 'delete')
|
||||
return {}
|
||||
|
||||
|
||||
def handle_proposing(session, r, shout):
|
||||
if is_positive(r.kind):
|
||||
# Proposal accepting logic
|
||||
replied_reaction = session.query(Reaction).filter(Reaction.id == r.reply_to).first()
|
||||
if replied_reaction and replied_reaction.kind is ReactionKind.PROPOSE.value and replied_reaction.quote:
|
||||
# patch all the proposals' quotes
|
||||
proposals = session.query(Reaction).filter(and_(Reaction.shout == r.shout, Reaction.kind == ReactionKind.PROPOSE.value)).all()
|
||||
for proposal in proposals:
|
||||
if proposal.quote:
|
||||
proposal_diff = get_diff(shout.body, proposal.quote)
|
||||
proposal_dict = proposal.dict()
|
||||
proposal_dict['quote'] = apply_diff(replied_reaction.quote, proposal_diff)
|
||||
Reaction.update(proposal, proposal_dict)
|
||||
session.add(proposal)
|
||||
|
||||
# patch shout's body
|
||||
shout_dict = shout.dict()
|
||||
shout_dict['body'] = replied_reaction.quote
|
||||
Shout.update(shout, shout_dict)
|
||||
session.add(shout)
|
||||
session.commit()
|
||||
|
||||
if is_negative(r.kind):
|
||||
# TODO: rejection logic
|
||||
pass
|
||||
|
@@ -2,15 +2,14 @@ import logging
|
||||
from typing import List
|
||||
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy.sql import and_
|
||||
|
||||
from orm.author import Author, AuthorFollower
|
||||
from orm.community import Community
|
||||
from orm.reaction import Reaction
|
||||
from orm.shout import Shout
|
||||
from orm.shout import Shout, ShoutReactionsFollower
|
||||
from orm.topic import Topic, TopicFollower
|
||||
from resolvers.author import author_follow, author_unfollow
|
||||
from resolvers.community import community_follow, community_unfollow
|
||||
from resolvers.reaction import reactions_follow, reactions_unfollow
|
||||
from resolvers.topic import topic_follow, topic_unfollow
|
||||
from services.auth import login_required
|
||||
from services.db import local_session
|
||||
@@ -140,3 +139,83 @@ def get_shout_followers(_, _info, slug: str = '', shout_id: int | None = None) -
|
||||
followers.append(r.created_by)
|
||||
|
||||
return followers
|
||||
|
||||
|
||||
|
||||
def reactions_follow(author_id, shout_id, auto=False):
|
||||
try:
|
||||
with local_session() as session:
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).one()
|
||||
|
||||
following = (
|
||||
session.query(ShoutReactionsFollower)
|
||||
.where(
|
||||
and_(
|
||||
ShoutReactionsFollower.follower == author_id,
|
||||
ShoutReactionsFollower.shout == shout.id,
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not following:
|
||||
following = ShoutReactionsFollower(follower=author_id, shout=shout.id, auto=auto)
|
||||
session.add(following)
|
||||
session.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def reactions_unfollow(author_id, shout_id: int):
|
||||
try:
|
||||
with local_session() as session:
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).one()
|
||||
|
||||
following = (
|
||||
session.query(ShoutReactionsFollower)
|
||||
.where(
|
||||
and_(
|
||||
ShoutReactionsFollower.follower == author_id,
|
||||
ShoutReactionsFollower.shout == shout.id,
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if following:
|
||||
session.delete(following)
|
||||
session.commit()
|
||||
return True
|
||||
except Exception as ex:
|
||||
logger.debug(ex)
|
||||
return False
|
||||
|
||||
|
||||
# for mutation.field("follow")
|
||||
def author_follow(follower_id, slug):
|
||||
try:
|
||||
with local_session() as session:
|
||||
author = session.query(Author).where(Author.slug == slug).one()
|
||||
af = AuthorFollower(follower=follower_id, author=author.id)
|
||||
session.add(af)
|
||||
session.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# for mutation.field("unfollow")
|
||||
def author_unfollow(follower_id, slug):
|
||||
with local_session() as session:
|
||||
flw = (
|
||||
session.query(AuthorFollower)
|
||||
.join(Author, Author.id == AuthorFollower.author)
|
||||
.filter(and_(AuthorFollower.follower == follower_id, Author.slug == slug))
|
||||
.first()
|
||||
)
|
||||
if flw:
|
||||
session.delete(flw)
|
||||
session.commit()
|
||||
return True
|
||||
return False
|
||||
|
28
resolvers/rater.py
Normal file
28
resolvers/rater.py
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
from orm.reaction import ReactionKind
|
||||
|
||||
|
||||
RATING_REACTIONS = [
|
||||
ReactionKind.LIKE.value,
|
||||
ReactionKind.ACCEPT.value,
|
||||
ReactionKind.AGREE.value,
|
||||
ReactionKind.DISLIKE.value,
|
||||
ReactionKind.REJECT.value,
|
||||
ReactionKind.DISAGREE.value]
|
||||
|
||||
|
||||
|
||||
def is_negative(x):
|
||||
return x in [
|
||||
ReactionKind.ACCEPT.value,
|
||||
ReactionKind.LIKE.value,
|
||||
ReactionKind.PROOF.value,
|
||||
]
|
||||
|
||||
|
||||
def is_positive(x):
|
||||
return x in [
|
||||
ReactionKind.ACCEPT.value,
|
||||
ReactionKind.LIKE.value,
|
||||
ReactionKind.PROOF.value,
|
||||
]
|
@@ -8,7 +8,10 @@ from sqlalchemy.sql import union
|
||||
|
||||
from orm.author import Author
|
||||
from orm.reaction import Reaction, ReactionKind
|
||||
from orm.shout import Shout, ShoutReactionsFollower, ShoutVisibility
|
||||
from orm.shout import Shout, ShoutVisibility
|
||||
from resolvers.editor import handle_proposing
|
||||
from resolvers.follower import reactions_follow
|
||||
from resolvers.rater import RATING_REACTIONS, is_negative, is_positive
|
||||
from services.auth import add_user_role, login_required
|
||||
from services.db import local_session
|
||||
from services.notify import notify_reaction
|
||||
@@ -37,120 +40,52 @@ def add_stat_columns(q, aliased_reaction):
|
||||
return q
|
||||
|
||||
|
||||
def reactions_follow(author_id, shout_id, auto=False):
|
||||
try:
|
||||
with local_session() as session:
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).one()
|
||||
|
||||
following = (
|
||||
session.query(ShoutReactionsFollower)
|
||||
.where(
|
||||
and_(
|
||||
ShoutReactionsFollower.follower == author_id,
|
||||
ShoutReactionsFollower.shout == shout.id,
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not following:
|
||||
following = ShoutReactionsFollower(follower=author_id, shout=shout.id, auto=auto)
|
||||
session.add(following)
|
||||
session.commit()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def reactions_unfollow(author_id, shout_id: int):
|
||||
try:
|
||||
with local_session() as session:
|
||||
shout = session.query(Shout).where(Shout.id == shout_id).one()
|
||||
|
||||
following = (
|
||||
session.query(ShoutReactionsFollower)
|
||||
.where(
|
||||
and_(
|
||||
ShoutReactionsFollower.follower == author_id,
|
||||
ShoutReactionsFollower.shout == shout.id,
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if following:
|
||||
session.delete(following)
|
||||
session.commit()
|
||||
return True
|
||||
except Exception as ex:
|
||||
logger.debug(ex)
|
||||
return False
|
||||
|
||||
|
||||
def is_published_author(session, author_id):
|
||||
"""checks if author has at least one publication"""
|
||||
def is_featured_author(session, author_id):
|
||||
"""checks if author has at least one featured publication"""
|
||||
return (
|
||||
session.query(Shout)
|
||||
.where(Shout.authors.any(id=author_id))
|
||||
.filter(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
.filter(and_(Shout.featured_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
.count()
|
||||
> 0
|
||||
)
|
||||
|
||||
|
||||
def is_negative(x):
|
||||
return x in [
|
||||
ReactionKind.ACCEPT.value,
|
||||
ReactionKind.LIKE.value,
|
||||
ReactionKind.PROOF.value,
|
||||
]
|
||||
|
||||
|
||||
def is_positive(x):
|
||||
return x in [
|
||||
ReactionKind.ACCEPT.value,
|
||||
ReactionKind.LIKE.value,
|
||||
ReactionKind.PROOF.value,
|
||||
]
|
||||
|
||||
|
||||
def check_to_publish(session, approver_id, reaction):
|
||||
def check_to_feature(session, approver_id, reaction):
|
||||
"""set shout to public if publicated approvers amount > 4"""
|
||||
if not reaction.reply_to and is_positive(reaction.kind):
|
||||
if is_published_author(session, approver_id):
|
||||
if is_featured_author(session, approver_id):
|
||||
approvers = []
|
||||
approvers.append(approver_id)
|
||||
# now count how many approvers are voted already
|
||||
approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
|
||||
approvers = [
|
||||
approver_id,
|
||||
]
|
||||
for ar in approvers_reactions:
|
||||
a = ar.created_by
|
||||
if is_published_author(session, a):
|
||||
approvers.append(a)
|
||||
reacted_readers = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
|
||||
for reacted_reader in reacted_readers:
|
||||
if is_featured_author(session, reacted_reader.id):
|
||||
approvers.append(reacted_reader.id)
|
||||
if len(approvers) > 4:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_to_hide(session, reaction):
|
||||
"""hides any shout if 20% of reactions are negative"""
|
||||
def check_to_unfeature(session, rejecter_id, reaction):
|
||||
"""unfeature any shout if 20% of reactions are negative"""
|
||||
if not reaction.reply_to and is_negative(reaction.kind):
|
||||
# if is_published_author(author_id):
|
||||
approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
|
||||
rejects = 0
|
||||
for r in approvers_reactions:
|
||||
if is_negative(r.kind):
|
||||
rejects += 1
|
||||
if len(approvers_reactions) / rejects < 5:
|
||||
return True
|
||||
if is_featured_author(session, rejecter_id):
|
||||
reactions = session.query(Reaction).where(and_(Reaction.shout == reaction.shout, Reaction.kind.in_(RATING_REACTIONS))).all()
|
||||
rejects = 0
|
||||
for r in reactions:
|
||||
approver = session.query(Author).filter(Author.id == r.created_by).first()
|
||||
if is_featured_author(session, approver):
|
||||
if is_negative(r.kind):
|
||||
rejects += 1
|
||||
if len(reactions) / rejects < 5:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def set_published(session, shout_id, approver_id):
|
||||
async def set_featured(session, shout_id):
|
||||
s = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
s.published_at = int(time.time())
|
||||
s.published_by = approver_id
|
||||
Shout.update(s, {'visibility': ShoutVisibility.PUBLIC.value})
|
||||
s.featured_at = int(time.time())
|
||||
Shout.update(s, {'visibility': ShoutVisibility.FEATURED.value})
|
||||
author = session.query(Author).filter(Author.id == s.created_by).first()
|
||||
if author:
|
||||
await add_user_role(str(author.user))
|
||||
@@ -158,7 +93,7 @@ async def set_published(session, shout_id, approver_id):
|
||||
session.commit()
|
||||
|
||||
|
||||
def set_hidden(session, shout_id):
|
||||
def set_unfeatured(session, shout_id):
|
||||
s = session.query(Shout).where(Shout.id == shout_id).first()
|
||||
Shout.update(s, {'visibility': ShoutVisibility.COMMUNITY.value})
|
||||
session.add(s)
|
||||
@@ -171,38 +106,24 @@ async def _create_reaction(session, shout, author, reaction):
|
||||
session.commit()
|
||||
rdict = r.dict()
|
||||
|
||||
# Proposal accepting logic
|
||||
if rdict.get('reply_to'):
|
||||
if r.kind in ['LIKE', 'APPROVE'] and author.id in shout.authors:
|
||||
replied_reaction = session.query(Reaction).filter(Reaction.id == r.reply_to).first()
|
||||
if replied_reaction:
|
||||
if replied_reaction.kind is ReactionKind.PROPOSE.value:
|
||||
if replied_reaction.range:
|
||||
old_body = shout.body
|
||||
start, end = replied_reaction.range.split(':')
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
new_body = old_body[:start] + replied_reaction.body + old_body[end:]
|
||||
shout_dict = shout.dict()
|
||||
shout_dict['body'] = new_body
|
||||
Shout.update(shout, shout_dict)
|
||||
session.add(shout)
|
||||
session.commit()
|
||||
# collaborative editing
|
||||
if rdict.get('reply_to') and r.kind in RATING_REACTIONS and author.id in shout.authors:
|
||||
handle_proposing(session, r, shout)
|
||||
|
||||
# Self-regulation mechanics
|
||||
if check_to_hide(session, r):
|
||||
set_hidden(session, shout.id)
|
||||
elif check_to_publish(session, author.id, r):
|
||||
await set_published(session, shout.id, author.id)
|
||||
# self-regultaion mechanics
|
||||
if check_to_unfeature(session, author.id, r):
|
||||
set_unfeatured(session, shout.id)
|
||||
elif check_to_feature(session, author.id, r):
|
||||
await set_featured(session, shout.id)
|
||||
|
||||
# Reactions auto-following
|
||||
# reactions auto-following
|
||||
reactions_follow(author.id, reaction['shout'], True)
|
||||
|
||||
rdict['shout'] = shout.dict()
|
||||
rdict['created_by'] = author.dict()
|
||||
rdict['stat'] = {'commented': 0, 'reacted': 0, 'rating': 0}
|
||||
|
||||
# Notifications call
|
||||
# notifications call
|
||||
await notify_reaction(rdict, 'create')
|
||||
|
||||
return rdict
|
||||
@@ -220,14 +141,14 @@ async def create_reaction(_, info, reaction):
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
shout = session.query(Shout).filter(Shout.id == shout_id).one()
|
||||
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
|
||||
kind = reaction.get('kind')
|
||||
shout_id = shout.id
|
||||
|
||||
if not kind and reaction.get('body'):
|
||||
if not kind and isinstance(reaction.get('body'), str):
|
||||
kind = ReactionKind.COMMENT.value
|
||||
|
||||
if not kind:
|
||||
|
@@ -26,9 +26,9 @@ def apply_filters(q, filters, author_id=None):
|
||||
if filters.get('reacted') and author_id:
|
||||
q.join(Reaction, Reaction.created_by == author_id)
|
||||
|
||||
by_published = filters.get('published')
|
||||
if by_published:
|
||||
q = q.filter(Shout.visibility == ShoutVisibility.PUBLIC.value)
|
||||
by_featured = filters.get('featured')
|
||||
if by_featured:
|
||||
q = q.filter(Shout.visibility == ShoutVisibility.FEATURED.value)
|
||||
by_layouts = filters.get('layouts')
|
||||
if by_layouts:
|
||||
q = q.filter(Shout.layout.in_(by_layouts))
|
||||
@@ -114,7 +114,7 @@ async def load_shouts_by(_, _info, options):
|
||||
filters: {
|
||||
layouts: ['audio', 'video', ..],
|
||||
reacted: True,
|
||||
published: True, // filter published-only
|
||||
featured: True, // filter featured-only
|
||||
author: 'discours',
|
||||
topic: 'culture',
|
||||
after: 1234567 // unixtime
|
||||
@@ -143,13 +143,14 @@ async def load_shouts_by(_, _info, options):
|
||||
q = add_stat_columns(q, aliased_reaction)
|
||||
|
||||
# filters
|
||||
q = apply_filters(q, options.get('filters', {}))
|
||||
filters = options.get('filters', {})
|
||||
q = apply_filters(q, filters)
|
||||
|
||||
# group
|
||||
q = q.group_by(Shout.id)
|
||||
|
||||
# order
|
||||
order_by = options.get('order_by', Shout.published_at)
|
||||
order_by = options.get('order_by', Shout.featured_at if filters.get('featured') else Shout.published_at)
|
||||
query_order_by = desc(order_by) if options.get('order_by_desc', True) else asc(order_by)
|
||||
q = q.order_by(nulls_last(query_order_by))
|
||||
|
||||
@@ -274,9 +275,10 @@ async def load_shouts_feed(_, info, options):
|
||||
|
||||
aliased_reaction = aliased(Reaction)
|
||||
q = add_stat_columns(q, aliased_reaction)
|
||||
q = apply_filters(q, options.get('filters', {}), reader.id)
|
||||
filters = options.get('filters', {})
|
||||
q = apply_filters(q, filters, reader.id)
|
||||
|
||||
order_by = options.get('order_by', Shout.published_at)
|
||||
order_by = options.get('order_by', Shout.featured_at if filters.get('featured') else Shout.published_at)
|
||||
|
||||
query_order_by = desc(order_by) if options.get('order_by_desc', True) else asc(order_by)
|
||||
offset = options.get('offset', 0)
|
||||
@@ -464,7 +466,7 @@ async def load_shouts_random_topic(_, info, limit: int = 10):
|
||||
.filter(
|
||||
and_(
|
||||
Shout.deleted_at.is_(None),
|
||||
Shout.visibility == ShoutVisibility.PUBLIC.value,
|
||||
Shout.visibility == ShoutVisibility.FEATURED.value,
|
||||
Shout.topics.any(slug=topic.slug),
|
||||
)
|
||||
)
|
||||
|
@@ -1,36 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from starlette.endpoints import HTTPEndpoint
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from orm.author import Author
|
||||
from resolvers.author import create_author
|
||||
from services.db import local_session
|
||||
|
||||
|
||||
class WebhookEndpoint(HTTPEndpoint):
|
||||
async def post(self, request: Request) -> JSONResponse:
|
||||
try:
|
||||
data = await request.json()
|
||||
if data:
|
||||
auth = request.headers.get('Authorization')
|
||||
if auth:
|
||||
if auth == os.environ.get('WEBHOOK_SECRET'):
|
||||
user_id: str = data['user']['id']
|
||||
name: str = data['user']['given_name']
|
||||
slug: str = data['user']['email'].split('@')[0]
|
||||
slug: str = re.sub('[^0-9a-z]+', '-', slug.lower())
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.slug == slug).first()
|
||||
if author:
|
||||
slug = slug + '-' + user_id.split('-').pop()
|
||||
await create_author(user_id, slug, name)
|
||||
|
||||
return JSONResponse({'status': 'success'})
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return JSONResponse({'status': 'error', 'message': str(e)}, status_code=500)
|
Reference in New Issue
Block a user