This commit is contained in:
parent
bd5f910f8c
commit
c00361b2ec
|
@ -1,3 +1,9 @@
|
||||||
|
[0.3.0]
|
||||||
|
- Shout.featured_at timestamp of the frontpage featuring event
|
||||||
|
- added proposal accepting logics
|
||||||
|
- schema modulized
|
||||||
|
-
|
||||||
|
|
||||||
[0.2.22]
|
[0.2.22]
|
||||||
- added precommit hook
|
- added precommit hook
|
||||||
- fmt
|
- fmt
|
||||||
|
|
5
main.py
5
main.py
|
@ -13,16 +13,17 @@ from sentry_sdk.integrations.starlette import StarletteIntegration
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
|
|
||||||
from resolvers.webhook import WebhookEndpoint
|
|
||||||
from services.rediscache import redis
|
from services.rediscache import redis
|
||||||
from services.schema import resolvers
|
from services.schema import resolvers
|
||||||
from services.search import search_service
|
from services.search import search_service
|
||||||
from services.viewed import ViewedStorage
|
from services.viewed import ViewedStorage
|
||||||
|
from services.webhook import WebhookEndpoint
|
||||||
from settings import DEV_SERVER_PID_FILE_NAME, MODE, SENTRY_DSN
|
from settings import DEV_SERVER_PID_FILE_NAME, MODE, SENTRY_DSN
|
||||||
|
|
||||||
|
|
||||||
import_module('resolvers')
|
import_module('resolvers')
|
||||||
schema = make_executable_schema(load_schema_from_path('schemas/core.graphql'), resolvers) # type: ignore
|
|
||||||
|
schema = make_executable_schema(load_schema_from_path('schema/'), resolvers)
|
||||||
|
|
||||||
|
|
||||||
async def start_up():
|
async def start_up():
|
||||||
|
|
|
@ -51,7 +51,7 @@ class ShoutCommunity(Base):
|
||||||
class ShoutVisibility(Enumeration):
|
class ShoutVisibility(Enumeration):
|
||||||
AUTHORS = 'AUTHORS'
|
AUTHORS = 'AUTHORS'
|
||||||
COMMUNITY = 'COMMUNITY'
|
COMMUNITY = 'COMMUNITY'
|
||||||
PUBLIC = 'PUBLIC'
|
FEATURED = 'FEATURED'
|
||||||
|
|
||||||
|
|
||||||
class Shout(Base):
|
class Shout(Base):
|
||||||
|
@ -60,6 +60,7 @@ class Shout(Base):
|
||||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||||
updated_at = Column(Integer, nullable=True)
|
updated_at = Column(Integer, nullable=True)
|
||||||
published_at = Column(Integer, nullable=True)
|
published_at = Column(Integer, nullable=True)
|
||||||
|
featured_at = Column(Integer, nullable=True)
|
||||||
deleted_at = Column(Integer, nullable=True)
|
deleted_at = Column(Integer, nullable=True)
|
||||||
|
|
||||||
created_by = Column(ForeignKey('author.id'), nullable=False)
|
created_by = Column(ForeignKey('author.id'), nullable=False)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "discoursio-core"
|
name = "discoursio-core"
|
||||||
version = "0.2.22"
|
version = "0.3.0"
|
||||||
description = "core module for discours.io"
|
description = "core module for discours.io"
|
||||||
authors = ["discoursio devteam"]
|
authors = ["discoursio devteam"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -26,14 +26,17 @@ setuptools = "^69.0.2"
|
||||||
pyright = "^1.1.341"
|
pyright = "^1.1.341"
|
||||||
pytest = "^7.4.2"
|
pytest = "^7.4.2"
|
||||||
black = { version = "^23.12.0", python = ">=3.12" }
|
black = { version = "^23.12.0", python = ">=3.12" }
|
||||||
ruff = { version = "^0.1.8", python = ">=3.12" }
|
ruff = { version = "^0.1.15", python = ">=3.12" }
|
||||||
isort = "^5.13.2"
|
isort = "^5.13.2"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "core.__version__"}
|
||||||
|
readme = {file = "README.md"}
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
extend-select = [
|
extend-select = [
|
||||||
|
|
|
@ -82,35 +82,6 @@ async def update_profile(_, info, profile):
|
||||||
return {'error': None, 'author': author}
|
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
|
# TODO: caching query
|
||||||
@query.field('get_authors_all')
|
@query.field('get_authors_all')
|
||||||
async def get_authors_all(_, _info):
|
async def get_authors_all(_, _info):
|
||||||
|
|
|
@ -4,11 +4,14 @@ from sqlalchemy import and_, select
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
from orm.author import Author
|
from orm.author import Author
|
||||||
|
from orm.reaction import Reaction, ReactionKind
|
||||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility
|
from orm.shout import Shout, ShoutAuthor, ShoutTopic, ShoutVisibility
|
||||||
from orm.topic import Topic
|
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.auth import login_required
|
||||||
from services.db import local_session
|
from services.db import local_session
|
||||||
|
from services.diff import apply_diff, get_diff
|
||||||
from services.notify import notify_shout
|
from services.notify import notify_shout
|
||||||
from services.schema import mutation, query
|
from services.schema import mutation, query
|
||||||
from services.search import search_service
|
from services.search import search_service
|
||||||
|
@ -187,7 +190,8 @@ async def update_shout( # noqa: C901
|
||||||
|
|
||||||
if not publish:
|
if not publish:
|
||||||
await notify_shout(shout_dict, 'update')
|
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 indexing
|
||||||
search_service.index(shout)
|
search_service.index(shout)
|
||||||
|
|
||||||
|
@ -217,3 +221,30 @@ async def delete_shout(_, info, shout_id):
|
||||||
session.commit()
|
session.commit()
|
||||||
await notify_shout(shout_dict, 'delete')
|
await notify_shout(shout_dict, 'delete')
|
||||||
return {}
|
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 typing import List
|
||||||
|
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
|
from sqlalchemy.sql import and_
|
||||||
|
|
||||||
from orm.author import Author, AuthorFollower
|
from orm.author import Author, AuthorFollower
|
||||||
from orm.community import Community
|
from orm.community import Community
|
||||||
from orm.reaction import Reaction
|
from orm.reaction import Reaction
|
||||||
from orm.shout import Shout
|
from orm.shout import Shout, ShoutReactionsFollower
|
||||||
from orm.topic import Topic, TopicFollower
|
from orm.topic import Topic, TopicFollower
|
||||||
from resolvers.author import author_follow, author_unfollow
|
|
||||||
from resolvers.community import community_follow, community_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 resolvers.topic import topic_follow, topic_unfollow
|
||||||
from services.auth import login_required
|
from services.auth import login_required
|
||||||
from services.db import local_session
|
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)
|
followers.append(r.created_by)
|
||||||
|
|
||||||
return followers
|
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.author import Author
|
||||||
from orm.reaction import Reaction, ReactionKind
|
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.auth import add_user_role, login_required
|
||||||
from services.db import local_session
|
from services.db import local_session
|
||||||
from services.notify import notify_reaction
|
from services.notify import notify_reaction
|
||||||
|
@ -37,120 +40,52 @@ def add_stat_columns(q, aliased_reaction):
|
||||||
return q
|
return q
|
||||||
|
|
||||||
|
|
||||||
def reactions_follow(author_id, shout_id, auto=False):
|
def is_featured_author(session, author_id):
|
||||||
try:
|
"""checks if author has at least one featured publication"""
|
||||||
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"""
|
|
||||||
return (
|
return (
|
||||||
session.query(Shout)
|
session.query(Shout)
|
||||||
.where(Shout.authors.any(id=author_id))
|
.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()
|
.count()
|
||||||
> 0
|
> 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def check_to_feature(session, approver_id, reaction):
|
||||||
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):
|
|
||||||
"""set shout to public if publicated approvers amount > 4"""
|
"""set shout to public if publicated approvers amount > 4"""
|
||||||
if not reaction.reply_to and is_positive(reaction.kind):
|
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
|
# now count how many approvers are voted already
|
||||||
approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
|
reacted_readers = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
|
||||||
approvers = [
|
for reacted_reader in reacted_readers:
|
||||||
approver_id,
|
if is_featured_author(session, reacted_reader.id):
|
||||||
]
|
approvers.append(reacted_reader.id)
|
||||||
for ar in approvers_reactions:
|
|
||||||
a = ar.created_by
|
|
||||||
if is_published_author(session, a):
|
|
||||||
approvers.append(a)
|
|
||||||
if len(approvers) > 4:
|
if len(approvers) > 4:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_to_hide(session, reaction):
|
def check_to_unfeature(session, rejecter_id, reaction):
|
||||||
"""hides any shout if 20% of reactions are negative"""
|
"""unfeature any shout if 20% of reactions are negative"""
|
||||||
if not reaction.reply_to and is_negative(reaction.kind):
|
if not reaction.reply_to and is_negative(reaction.kind):
|
||||||
# if is_published_author(author_id):
|
if is_featured_author(session, rejecter_id):
|
||||||
approvers_reactions = session.query(Reaction).where(Reaction.shout == reaction.shout).all()
|
reactions = session.query(Reaction).where(and_(Reaction.shout == reaction.shout, Reaction.kind.in_(RATING_REACTIONS))).all()
|
||||||
rejects = 0
|
rejects = 0
|
||||||
for r in approvers_reactions:
|
for r in reactions:
|
||||||
if is_negative(r.kind):
|
approver = session.query(Author).filter(Author.id == r.created_by).first()
|
||||||
rejects += 1
|
if is_featured_author(session, approver):
|
||||||
if len(approvers_reactions) / rejects < 5:
|
if is_negative(r.kind):
|
||||||
return True
|
rejects += 1
|
||||||
|
if len(reactions) / rejects < 5:
|
||||||
|
return True
|
||||||
return False
|
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 = session.query(Shout).where(Shout.id == shout_id).first()
|
||||||
s.published_at = int(time.time())
|
s.featured_at = int(time.time())
|
||||||
s.published_by = approver_id
|
Shout.update(s, {'visibility': ShoutVisibility.FEATURED.value})
|
||||||
Shout.update(s, {'visibility': ShoutVisibility.PUBLIC.value})
|
|
||||||
author = session.query(Author).filter(Author.id == s.created_by).first()
|
author = session.query(Author).filter(Author.id == s.created_by).first()
|
||||||
if author:
|
if author:
|
||||||
await add_user_role(str(author.user))
|
await add_user_role(str(author.user))
|
||||||
|
@ -158,7 +93,7 @@ async def set_published(session, shout_id, approver_id):
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
def set_hidden(session, shout_id):
|
def set_unfeatured(session, shout_id):
|
||||||
s = session.query(Shout).where(Shout.id == shout_id).first()
|
s = session.query(Shout).where(Shout.id == shout_id).first()
|
||||||
Shout.update(s, {'visibility': ShoutVisibility.COMMUNITY.value})
|
Shout.update(s, {'visibility': ShoutVisibility.COMMUNITY.value})
|
||||||
session.add(s)
|
session.add(s)
|
||||||
|
@ -171,38 +106,24 @@ async def _create_reaction(session, shout, author, reaction):
|
||||||
session.commit()
|
session.commit()
|
||||||
rdict = r.dict()
|
rdict = r.dict()
|
||||||
|
|
||||||
# Proposal accepting logic
|
# collaborative editing
|
||||||
if rdict.get('reply_to'):
|
if rdict.get('reply_to') and r.kind in RATING_REACTIONS and author.id in shout.authors:
|
||||||
if r.kind in ['LIKE', 'APPROVE'] and author.id in shout.authors:
|
handle_proposing(session, r, shout)
|
||||||
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()
|
|
||||||
|
|
||||||
# Self-regulation mechanics
|
# self-regultaion mechanics
|
||||||
if check_to_hide(session, r):
|
if check_to_unfeature(session, author.id, r):
|
||||||
set_hidden(session, shout.id)
|
set_unfeatured(session, shout.id)
|
||||||
elif check_to_publish(session, author.id, r):
|
elif check_to_feature(session, author.id, r):
|
||||||
await set_published(session, shout.id, author.id)
|
await set_featured(session, shout.id)
|
||||||
|
|
||||||
# Reactions auto-following
|
# reactions auto-following
|
||||||
reactions_follow(author.id, reaction['shout'], True)
|
reactions_follow(author.id, reaction['shout'], True)
|
||||||
|
|
||||||
rdict['shout'] = shout.dict()
|
rdict['shout'] = shout.dict()
|
||||||
rdict['created_by'] = author.dict()
|
rdict['created_by'] = author.dict()
|
||||||
rdict['stat'] = {'commented': 0, 'reacted': 0, 'rating': 0}
|
rdict['stat'] = {'commented': 0, 'reacted': 0, 'rating': 0}
|
||||||
|
|
||||||
# Notifications call
|
# notifications call
|
||||||
await notify_reaction(rdict, 'create')
|
await notify_reaction(rdict, 'create')
|
||||||
|
|
||||||
return rdict
|
return rdict
|
||||||
|
@ -220,14 +141,14 @@ 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).one()
|
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||||
author = session.query(Author).filter(Author.user == user_id).first()
|
author = session.query(Author).filter(Author.user == user_id).first()
|
||||||
if shout and author:
|
if shout and author:
|
||||||
reaction['created_by'] = author.id
|
reaction['created_by'] = author.id
|
||||||
kind = reaction.get('kind')
|
kind = reaction.get('kind')
|
||||||
shout_id = shout.id
|
shout_id = shout.id
|
||||||
|
|
||||||
if not kind and reaction.get('body'):
|
if not kind and isinstance(reaction.get('body'), str):
|
||||||
kind = ReactionKind.COMMENT.value
|
kind = ReactionKind.COMMENT.value
|
||||||
|
|
||||||
if not kind:
|
if not kind:
|
||||||
|
|
|
@ -26,9 +26,9 @@ def apply_filters(q, filters, author_id=None):
|
||||||
if filters.get('reacted') and author_id:
|
if filters.get('reacted') and author_id:
|
||||||
q.join(Reaction, Reaction.created_by == author_id)
|
q.join(Reaction, Reaction.created_by == author_id)
|
||||||
|
|
||||||
by_published = filters.get('published')
|
by_featured = filters.get('featured')
|
||||||
if by_published:
|
if by_featured:
|
||||||
q = q.filter(Shout.visibility == ShoutVisibility.PUBLIC.value)
|
q = q.filter(Shout.visibility == ShoutVisibility.FEATURED.value)
|
||||||
by_layouts = filters.get('layouts')
|
by_layouts = filters.get('layouts')
|
||||||
if by_layouts:
|
if by_layouts:
|
||||||
q = q.filter(Shout.layout.in_(by_layouts))
|
q = q.filter(Shout.layout.in_(by_layouts))
|
||||||
|
@ -114,7 +114,7 @@ async def load_shouts_by(_, _info, options):
|
||||||
filters: {
|
filters: {
|
||||||
layouts: ['audio', 'video', ..],
|
layouts: ['audio', 'video', ..],
|
||||||
reacted: True,
|
reacted: True,
|
||||||
published: True, // filter published-only
|
featured: True, // filter featured-only
|
||||||
author: 'discours',
|
author: 'discours',
|
||||||
topic: 'culture',
|
topic: 'culture',
|
||||||
after: 1234567 // unixtime
|
after: 1234567 // unixtime
|
||||||
|
@ -143,13 +143,14 @@ async def load_shouts_by(_, _info, options):
|
||||||
q = add_stat_columns(q, aliased_reaction)
|
q = add_stat_columns(q, aliased_reaction)
|
||||||
|
|
||||||
# filters
|
# filters
|
||||||
q = apply_filters(q, options.get('filters', {}))
|
filters = options.get('filters', {})
|
||||||
|
q = apply_filters(q, filters)
|
||||||
|
|
||||||
# group
|
# group
|
||||||
q = q.group_by(Shout.id)
|
q = q.group_by(Shout.id)
|
||||||
|
|
||||||
# order
|
# 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)
|
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))
|
q = q.order_by(nulls_last(query_order_by))
|
||||||
|
|
||||||
|
@ -274,9 +275,10 @@ async def load_shouts_feed(_, info, options):
|
||||||
|
|
||||||
aliased_reaction = aliased(Reaction)
|
aliased_reaction = aliased(Reaction)
|
||||||
q = add_stat_columns(q, 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)
|
query_order_by = desc(order_by) if options.get('order_by_desc', True) else asc(order_by)
|
||||||
offset = options.get('offset', 0)
|
offset = options.get('offset', 0)
|
||||||
|
@ -464,7 +466,7 @@ async def load_shouts_random_topic(_, info, limit: int = 10):
|
||||||
.filter(
|
.filter(
|
||||||
and_(
|
and_(
|
||||||
Shout.deleted_at.is_(None),
|
Shout.deleted_at.is_(None),
|
||||||
Shout.visibility == ShoutVisibility.PUBLIC.value,
|
Shout.visibility == ShoutVisibility.FEATURED.value,
|
||||||
Shout.topics.any(slug=topic.slug),
|
Shout.topics.any(slug=topic.slug),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
46
schema/enum.graphql
Normal file
46
schema/enum.graphql
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
enum ShoutVisibility {
|
||||||
|
AUTHORS
|
||||||
|
COMMUNITY
|
||||||
|
FEATURED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReactionStatus {
|
||||||
|
NEW
|
||||||
|
UPDATED
|
||||||
|
CHANGED
|
||||||
|
EXPLAINED
|
||||||
|
DELETED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReactionKind {
|
||||||
|
|
||||||
|
# collabs
|
||||||
|
AGREE
|
||||||
|
DISAGREE
|
||||||
|
ASK
|
||||||
|
PROPOSE
|
||||||
|
PROOF
|
||||||
|
DISPROOF
|
||||||
|
ACCEPT
|
||||||
|
REJECT
|
||||||
|
|
||||||
|
# public feed
|
||||||
|
QUOTE
|
||||||
|
COMMENT
|
||||||
|
LIKE
|
||||||
|
DISLIKE
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FollowingEntity {
|
||||||
|
TOPIC
|
||||||
|
AUTHOR
|
||||||
|
COMMUNITY
|
||||||
|
REACTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InviteStatus {
|
||||||
|
PENDING
|
||||||
|
ACCEPTED
|
||||||
|
REJECTED
|
||||||
|
}
|
80
schema/input.graphql
Normal file
80
schema/input.graphql
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
input ShoutInput {
|
||||||
|
slug: String
|
||||||
|
title: String
|
||||||
|
body: String
|
||||||
|
lead: String
|
||||||
|
description: String
|
||||||
|
layout: String
|
||||||
|
media: String
|
||||||
|
authors: [String]
|
||||||
|
topics: [TopicInput]
|
||||||
|
community: Int
|
||||||
|
subtitle: String
|
||||||
|
cover: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input ProfileInput {
|
||||||
|
slug: String
|
||||||
|
name: String
|
||||||
|
pic: String
|
||||||
|
links: [String]
|
||||||
|
bio: String
|
||||||
|
about: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input TopicInput {
|
||||||
|
id: Int
|
||||||
|
slug: String!
|
||||||
|
title: String
|
||||||
|
body: String
|
||||||
|
pic: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input ReactionInput {
|
||||||
|
kind: ReactionKind!
|
||||||
|
shout: Int!
|
||||||
|
quote: String
|
||||||
|
body: String
|
||||||
|
reply_to: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
input AuthorsBy {
|
||||||
|
last_seen: Int
|
||||||
|
created_at: Int
|
||||||
|
slug: String
|
||||||
|
name: String
|
||||||
|
topic: String
|
||||||
|
order: String
|
||||||
|
after: Int
|
||||||
|
stat: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input LoadShoutsFilters {
|
||||||
|
topic: String
|
||||||
|
author: String
|
||||||
|
layouts: [String]
|
||||||
|
featured: Boolean
|
||||||
|
reacted: Boolean
|
||||||
|
after: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
input LoadShoutsOptions {
|
||||||
|
filters: LoadShoutsFilters
|
||||||
|
with_author_captions: Boolean
|
||||||
|
limit: Int!
|
||||||
|
random_limit: Int
|
||||||
|
offset: Int
|
||||||
|
order_by: String
|
||||||
|
order_by_desc: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
input ReactionBy {
|
||||||
|
shout: String
|
||||||
|
shouts: [String]
|
||||||
|
search: String
|
||||||
|
comment: Boolean
|
||||||
|
topic: String
|
||||||
|
created_by: Int
|
||||||
|
after: Int
|
||||||
|
sort: String
|
||||||
|
}
|
31
schema/mutation.graphql
Normal file
31
schema/mutation.graphql
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
type Mutation {
|
||||||
|
# author
|
||||||
|
rate_author(rated_slug: String!, value: Int!): Result!
|
||||||
|
update_profile(profile: ProfileInput!): Result!
|
||||||
|
|
||||||
|
# editor
|
||||||
|
create_shout(inp: ShoutInput!): Result!
|
||||||
|
update_shout(shout_id: Int!, shout_input: ShoutInput, publish: Boolean): Result!
|
||||||
|
delete_shout(shout_id: Int!): Result!
|
||||||
|
|
||||||
|
# follower
|
||||||
|
follow(what: FollowingEntity!, slug: String!): Result!
|
||||||
|
unfollow(what: FollowingEntity!, slug: String!): Result!
|
||||||
|
|
||||||
|
# topic
|
||||||
|
create_topic(input: TopicInput!): Result!
|
||||||
|
update_topic(input: TopicInput!): Result!
|
||||||
|
delete_topic(slug: String!): Result!
|
||||||
|
|
||||||
|
# reaction
|
||||||
|
create_reaction(reaction: ReactionInput!): Result!
|
||||||
|
update_reaction(id: Int!, reaction: ReactionInput!): Result!
|
||||||
|
delete_reaction(reaction_id: Int!): Result!
|
||||||
|
|
||||||
|
# collab
|
||||||
|
create_invite(slug: String, author_id: Int): Result!
|
||||||
|
remove_author(slug: String, author_id: Int): Result!
|
||||||
|
remove_invite(invite_id: Int!): Result!
|
||||||
|
accept_invite(invite_id: Int!): Result!
|
||||||
|
reject_invite(invite_id: Int!): Result!
|
||||||
|
}
|
41
schema/query.graphql
Normal file
41
schema/query.graphql
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
type Query {
|
||||||
|
# author
|
||||||
|
get_author(slug: String, author_id: Int): Author
|
||||||
|
get_author_id(user: String!): Author
|
||||||
|
get_authors_all: [Author]
|
||||||
|
get_author_followers(slug: String, user: String, author_id: Int): [Author]
|
||||||
|
get_author_followed(slug: String, user: String, author_id: Int): [Author]
|
||||||
|
load_authors_by(by: AuthorsBy!, limit: Int, offset: Int): [Author]
|
||||||
|
|
||||||
|
# community
|
||||||
|
get_community: Community
|
||||||
|
get_communities_all: [Community]
|
||||||
|
|
||||||
|
# editor
|
||||||
|
get_shouts_drafts: [Shout]
|
||||||
|
|
||||||
|
# follower
|
||||||
|
get_my_followed: Result # { authors topics communities }
|
||||||
|
get_shout_followers(slug: String, shout_id: Int): [Author]
|
||||||
|
|
||||||
|
# reaction
|
||||||
|
load_reactions_by(by: ReactionBy!, limit: Int, offset: Int): [Reaction]
|
||||||
|
|
||||||
|
# reader
|
||||||
|
get_shout(slug: String, shout_id: Int): Shout
|
||||||
|
load_shouts_followed(follower_id: Int!, limit: Int, offset: Int): [Shout] # userReactedShouts
|
||||||
|
load_shouts_by(options: LoadShoutsOptions): [Shout]
|
||||||
|
load_shouts_search(text: String!, limit: Int, offset: Int): [SearchResult]
|
||||||
|
load_shouts_feed(options: LoadShoutsOptions): [Shout]
|
||||||
|
load_shouts_unrated(limit: Int, offset: Int): [Shout]
|
||||||
|
load_shouts_random_top(options: LoadShoutsOptions): [Shout]
|
||||||
|
load_shouts_random_topic(limit: Int!): Result! # { topic shouts }
|
||||||
|
load_shouts_drafts: [Shout]
|
||||||
|
|
||||||
|
# topic
|
||||||
|
get_topic(slug: String!): Topic
|
||||||
|
get_topics_all: [Topic]
|
||||||
|
get_topics_random(amount: Int): [Topic]
|
||||||
|
get_topics_by_author(slug: String, user: String, author_id: Int): [Topic]
|
||||||
|
get_topics_by_community(slug: String, community_id: Int): [Topic]
|
||||||
|
}
|
182
schema/type.graphql
Normal file
182
schema/type.graphql
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
type AuthorFollowings {
|
||||||
|
unread: Int
|
||||||
|
topics: [String]
|
||||||
|
authors: [String]
|
||||||
|
reactions: [Int]
|
||||||
|
communities: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthorStat {
|
||||||
|
shouts: Int
|
||||||
|
followings: Int
|
||||||
|
followers: Int
|
||||||
|
rating: Int
|
||||||
|
rating_shouts: Int
|
||||||
|
rating_comments: Int
|
||||||
|
commented: Int
|
||||||
|
viewed: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Author {
|
||||||
|
id: Int!
|
||||||
|
user: String! # user.id
|
||||||
|
slug: String! # user.nickname
|
||||||
|
name: String # user.preferred_username
|
||||||
|
pic: String
|
||||||
|
bio: String
|
||||||
|
about: String
|
||||||
|
links: [String]
|
||||||
|
created_at: Int
|
||||||
|
last_seen: Int
|
||||||
|
updated_at: Int
|
||||||
|
deleted_at: Int
|
||||||
|
seo: String
|
||||||
|
# synthetic
|
||||||
|
stat: AuthorStat # ratings inside
|
||||||
|
communities: [Community]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReactionUpdating {
|
||||||
|
error: String
|
||||||
|
status: ReactionStatus
|
||||||
|
reaction: Reaction
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rating {
|
||||||
|
rater: String!
|
||||||
|
value: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Reaction {
|
||||||
|
id: Int!
|
||||||
|
shout: Shout!
|
||||||
|
created_at: Int!
|
||||||
|
created_by: Author!
|
||||||
|
updated_at: Int
|
||||||
|
deleted_at: Int
|
||||||
|
deleted_by: Author
|
||||||
|
range: String
|
||||||
|
kind: ReactionKind!
|
||||||
|
body: String
|
||||||
|
reply_to: Int
|
||||||
|
stat: Stat
|
||||||
|
oid: String
|
||||||
|
# old_thread: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Shout {
|
||||||
|
id: Int!
|
||||||
|
slug: String!
|
||||||
|
body: String!
|
||||||
|
lead: String
|
||||||
|
description: String
|
||||||
|
main_topic: String
|
||||||
|
topics: [Topic]
|
||||||
|
created_by: Author!
|
||||||
|
updated_by: Author
|
||||||
|
deleted_by: Author
|
||||||
|
authors: [Author]
|
||||||
|
communities: [Community]
|
||||||
|
title: String!
|
||||||
|
subtitle: String
|
||||||
|
lang: String
|
||||||
|
community: String
|
||||||
|
cover: String
|
||||||
|
cover_caption: String
|
||||||
|
layout: String!
|
||||||
|
visibility: String
|
||||||
|
|
||||||
|
created_at: Int!
|
||||||
|
updated_at: Int
|
||||||
|
published_at: Int
|
||||||
|
featured_at: Int
|
||||||
|
deleted_at: Int
|
||||||
|
|
||||||
|
version_of: Shout # TODO: use version_of somewhere
|
||||||
|
|
||||||
|
media: String
|
||||||
|
stat: Stat
|
||||||
|
score: Float
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stat {
|
||||||
|
viewed: Int
|
||||||
|
reacted: Int
|
||||||
|
rating: Int
|
||||||
|
commented: Int
|
||||||
|
ranking: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Community {
|
||||||
|
id: Int!
|
||||||
|
slug: String!
|
||||||
|
name: String!
|
||||||
|
desc: String
|
||||||
|
pic: String!
|
||||||
|
created_at: Int!
|
||||||
|
created_by: Author!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Collection {
|
||||||
|
id: Int!
|
||||||
|
slug: String!
|
||||||
|
title: String!
|
||||||
|
desc: String
|
||||||
|
amount: Int
|
||||||
|
published_at: Int
|
||||||
|
created_at: Int!
|
||||||
|
created_by: Author!
|
||||||
|
}
|
||||||
|
|
||||||
|
type TopicStat {
|
||||||
|
shouts: Int!
|
||||||
|
followers: Int!
|
||||||
|
authors: Int!
|
||||||
|
viewed: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Topic {
|
||||||
|
id: Int!
|
||||||
|
slug: String!
|
||||||
|
title: String
|
||||||
|
body: String
|
||||||
|
pic: String
|
||||||
|
stat: TopicStat
|
||||||
|
oid: String
|
||||||
|
}
|
||||||
|
|
||||||
|
# output type
|
||||||
|
|
||||||
|
type Result {
|
||||||
|
error: String
|
||||||
|
slugs: [String]
|
||||||
|
shout: Shout
|
||||||
|
shouts: [Shout]
|
||||||
|
author: Author
|
||||||
|
authors: [Author]
|
||||||
|
reaction: Reaction
|
||||||
|
reactions: [Reaction]
|
||||||
|
topic: Topic
|
||||||
|
topics: [Topic]
|
||||||
|
community: Community
|
||||||
|
communities: [Community]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchResult {
|
||||||
|
slug: String!
|
||||||
|
title: String!
|
||||||
|
cover: String
|
||||||
|
main_topic: String
|
||||||
|
created_at: Int
|
||||||
|
authors: [Author]
|
||||||
|
topics: [Topic]
|
||||||
|
score: Float!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Invite {
|
||||||
|
id: Int!
|
||||||
|
inviter_id: Int!
|
||||||
|
author_id: Int!
|
||||||
|
shout_id: Int!
|
||||||
|
status: InviteStatus
|
||||||
|
}
|
|
@ -1,242 +0,0 @@
|
||||||
scalar Dict
|
|
||||||
|
|
||||||
type ConfigType {
|
|
||||||
authorizerURL: String!
|
|
||||||
redirectURL: String!
|
|
||||||
clientID: String!
|
|
||||||
extraHeaders: [Header]
|
|
||||||
}
|
|
||||||
|
|
||||||
type User {
|
|
||||||
id: ID!
|
|
||||||
email: String!
|
|
||||||
preferred_username: String!
|
|
||||||
email_verified: Boolean!
|
|
||||||
signup_methods: String!
|
|
||||||
given_name: String
|
|
||||||
family_name: String
|
|
||||||
middle_name: String
|
|
||||||
nickname: String
|
|
||||||
picture: String
|
|
||||||
gender: String
|
|
||||||
birthdate: String
|
|
||||||
phone_number: String
|
|
||||||
phone_number_verified: Boolean
|
|
||||||
roles: [String]
|
|
||||||
created_at: Int!
|
|
||||||
updated_at: Int!
|
|
||||||
is_multi_factor_auth_enabled: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthToken {
|
|
||||||
message: String
|
|
||||||
access_token: String!
|
|
||||||
expires_in: Int!
|
|
||||||
id_token: String!
|
|
||||||
refresh_token: String
|
|
||||||
user: User
|
|
||||||
should_show_email_otp_screen: Boolean
|
|
||||||
should_show_mobile_otp_screen: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type Response {
|
|
||||||
message: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Header {
|
|
||||||
key: String!
|
|
||||||
value: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input HeaderIn {
|
|
||||||
key: String!
|
|
||||||
value: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input LoginInput {
|
|
||||||
email: String!
|
|
||||||
password: String!
|
|
||||||
roles: [String]
|
|
||||||
scope: [String]
|
|
||||||
state: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input SignupInput {
|
|
||||||
email: String!
|
|
||||||
password: String!
|
|
||||||
confirm_password: String!
|
|
||||||
given_name: String
|
|
||||||
family_name: String
|
|
||||||
middle_name: String
|
|
||||||
nickname: String
|
|
||||||
picture: String
|
|
||||||
gender: String
|
|
||||||
birthdate: String
|
|
||||||
phone_number: String
|
|
||||||
roles: [String]
|
|
||||||
scope: [String]
|
|
||||||
redirect_uri: String
|
|
||||||
is_multi_factor_auth_enabled: Boolean
|
|
||||||
state: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input MagicLinkLoginInput {
|
|
||||||
email: String!
|
|
||||||
roles: [String]
|
|
||||||
scopes: [String]
|
|
||||||
state: String
|
|
||||||
redirect_uri: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input VerifyEmailInput {
|
|
||||||
token: String!
|
|
||||||
state: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input VerifyOtpInput {
|
|
||||||
email: String
|
|
||||||
phone_number: String
|
|
||||||
otp: String!
|
|
||||||
state: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input ResendOtpInput {
|
|
||||||
email: String
|
|
||||||
phone_number: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input GraphqlQueryInput {
|
|
||||||
query: String!
|
|
||||||
variables: Dict
|
|
||||||
headers: [HeaderIn]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type MetaData {
|
|
||||||
version: String!
|
|
||||||
client_id: String!
|
|
||||||
is_google_login_enabled: Boolean!
|
|
||||||
is_facebook_login_enabled: Boolean!
|
|
||||||
is_github_login_enabled: Boolean!
|
|
||||||
is_linkedin_login_enabled: Boolean!
|
|
||||||
is_apple_login_enabled: Boolean!
|
|
||||||
is_twitter_login_enabled: Boolean!
|
|
||||||
is_microsoft_login_enabled: Boolean!
|
|
||||||
is_email_verification_enabled: Boolean!
|
|
||||||
is_basic_authentication_enabled: Boolean!
|
|
||||||
is_magic_link_login_enabled: Boolean!
|
|
||||||
is_sign_up_enabled: Boolean!
|
|
||||||
is_strong_password_enabled: Boolean!
|
|
||||||
}
|
|
||||||
|
|
||||||
input UpdateProfileInput {
|
|
||||||
old_password: String
|
|
||||||
new_password: String
|
|
||||||
confirm_new_password: String
|
|
||||||
email: String
|
|
||||||
given_name: String
|
|
||||||
family_name: String
|
|
||||||
middle_name: String
|
|
||||||
nickname: String
|
|
||||||
gender: String
|
|
||||||
birthdate: String
|
|
||||||
phone_number: String
|
|
||||||
picture: String
|
|
||||||
is_multi_factor_auth_enabled: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
input ForgotPasswordInput {
|
|
||||||
email: String!
|
|
||||||
state: String
|
|
||||||
redirect_uri: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input ResetPasswordInput {
|
|
||||||
token: String!
|
|
||||||
password: String!
|
|
||||||
confirm_password: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input SessionQueryInput {
|
|
||||||
roles: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
input IsValidJWTQueryInput {
|
|
||||||
jwt: String!
|
|
||||||
roles: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ValidJWTResponse {
|
|
||||||
valid: String!
|
|
||||||
message: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum OAuthProviders {
|
|
||||||
Apple
|
|
||||||
Github
|
|
||||||
Google
|
|
||||||
Facebook
|
|
||||||
LinkedIn
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ResponseTypes {
|
|
||||||
Code
|
|
||||||
Token
|
|
||||||
}
|
|
||||||
|
|
||||||
input AuthorizeInput {
|
|
||||||
response_type: ResponseTypes!
|
|
||||||
use_refresh_token: Boolean
|
|
||||||
response_mode: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthorizeResponse {
|
|
||||||
state: String!
|
|
||||||
code: String
|
|
||||||
error: String
|
|
||||||
error_description: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input RevokeTokenInput {
|
|
||||||
refresh_token: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
input GetTokenInput {
|
|
||||||
code: String
|
|
||||||
grant_type: String
|
|
||||||
refresh_token: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetTokenResponse {
|
|
||||||
access_token: String!
|
|
||||||
expires_in: Int!
|
|
||||||
id_token: String!
|
|
||||||
refresh_token: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input ValidateJWTTokenInput {
|
|
||||||
token_type: TokenType!
|
|
||||||
token: String!
|
|
||||||
roles: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ValidateJWTTokenResponse {
|
|
||||||
is_valid: Boolean!
|
|
||||||
claims: Dict
|
|
||||||
}
|
|
||||||
|
|
||||||
input ValidateSessionInput {
|
|
||||||
cookie: String
|
|
||||||
roles: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ValidateSessionResponse {
|
|
||||||
is_valid: Boolean!
|
|
||||||
user: User
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TokenType {
|
|
||||||
access_token
|
|
||||||
id_token
|
|
||||||
refresh_token
|
|
||||||
}
|
|
|
@ -1,385 +0,0 @@
|
||||||
enum ShoutVisibility {
|
|
||||||
AUTHORS
|
|
||||||
COMMUNITY
|
|
||||||
PUBLIC
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ReactionStatus {
|
|
||||||
NEW
|
|
||||||
UPDATED
|
|
||||||
CHANGED
|
|
||||||
EXPLAINED
|
|
||||||
DELETED
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ReactionKind {
|
|
||||||
# collabs
|
|
||||||
AGREE
|
|
||||||
DISAGREE
|
|
||||||
ASK
|
|
||||||
PROPOSE
|
|
||||||
PROOF
|
|
||||||
DISPROOF
|
|
||||||
ACCEPT
|
|
||||||
REJECT
|
|
||||||
# public feed
|
|
||||||
QUOTE
|
|
||||||
COMMENT
|
|
||||||
LIKE
|
|
||||||
DISLIKE
|
|
||||||
}
|
|
||||||
|
|
||||||
enum FollowingEntity {
|
|
||||||
TOPIC
|
|
||||||
AUTHOR
|
|
||||||
COMMUNITY
|
|
||||||
REACTIONS
|
|
||||||
}
|
|
||||||
|
|
||||||
enum InviteStatus {
|
|
||||||
PENDING
|
|
||||||
ACCEPTED
|
|
||||||
REJECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
# Типы
|
|
||||||
|
|
||||||
type AuthorFollowings {
|
|
||||||
unread: Int
|
|
||||||
topics: [String]
|
|
||||||
authors: [String]
|
|
||||||
reactions: [Int]
|
|
||||||
communities: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthorStat {
|
|
||||||
shouts: Int
|
|
||||||
followings: Int
|
|
||||||
followers: Int
|
|
||||||
rating: Int
|
|
||||||
rating_shouts: Int
|
|
||||||
rating_comments: Int
|
|
||||||
commented: Int
|
|
||||||
viewed: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Author {
|
|
||||||
id: Int!
|
|
||||||
user: String! # user.id
|
|
||||||
slug: String! # user.nickname
|
|
||||||
name: String # user.preferred_username
|
|
||||||
pic: String
|
|
||||||
bio: String
|
|
||||||
about: String
|
|
||||||
links: [String]
|
|
||||||
created_at: Int
|
|
||||||
last_seen: Int
|
|
||||||
updated_at: Int
|
|
||||||
deleted_at: Int
|
|
||||||
seo: String
|
|
||||||
# synthetic
|
|
||||||
stat: AuthorStat # ratings inside
|
|
||||||
communities: [Community]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReactionUpdating {
|
|
||||||
error: String
|
|
||||||
status: ReactionStatus
|
|
||||||
reaction: Reaction
|
|
||||||
}
|
|
||||||
|
|
||||||
type Rating {
|
|
||||||
rater: String!
|
|
||||||
value: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Reaction {
|
|
||||||
id: Int!
|
|
||||||
shout: Shout!
|
|
||||||
created_at: Int!
|
|
||||||
created_by: Author!
|
|
||||||
updated_at: Int
|
|
||||||
deleted_at: Int
|
|
||||||
deleted_by: Author
|
|
||||||
range: String
|
|
||||||
kind: ReactionKind!
|
|
||||||
body: String
|
|
||||||
reply_to: Int
|
|
||||||
stat: Stat
|
|
||||||
oid: String
|
|
||||||
# old_thread: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type Shout {
|
|
||||||
id: Int!
|
|
||||||
slug: String!
|
|
||||||
body: String!
|
|
||||||
lead: String
|
|
||||||
description: String
|
|
||||||
created_at: Int!
|
|
||||||
main_topic: String
|
|
||||||
topics: [Topic]
|
|
||||||
created_by: Author!
|
|
||||||
updated_by: Author
|
|
||||||
deleted_by: Author
|
|
||||||
authors: [Author]
|
|
||||||
communities: [Community]
|
|
||||||
title: String!
|
|
||||||
subtitle: String
|
|
||||||
lang: String
|
|
||||||
community: String
|
|
||||||
cover: String
|
|
||||||
cover_caption: String
|
|
||||||
layout: String!
|
|
||||||
version_of: String
|
|
||||||
visibility: String
|
|
||||||
updated_at: Int
|
|
||||||
deleted_at: Int
|
|
||||||
published_at: Int
|
|
||||||
media: String
|
|
||||||
stat: Stat
|
|
||||||
score: Float
|
|
||||||
}
|
|
||||||
|
|
||||||
type Stat {
|
|
||||||
viewed: Int
|
|
||||||
reacted: Int
|
|
||||||
rating: Int
|
|
||||||
commented: Int
|
|
||||||
ranking: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Community {
|
|
||||||
id: Int!
|
|
||||||
slug: String!
|
|
||||||
name: String!
|
|
||||||
desc: String
|
|
||||||
pic: String!
|
|
||||||
created_at: Int!
|
|
||||||
created_by: Author!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Collection {
|
|
||||||
id: Int!
|
|
||||||
slug: String!
|
|
||||||
title: String!
|
|
||||||
desc: String
|
|
||||||
amount: Int
|
|
||||||
published_at: Int
|
|
||||||
created_at: Int!
|
|
||||||
created_by: Author!
|
|
||||||
}
|
|
||||||
|
|
||||||
type TopicStat {
|
|
||||||
shouts: Int!
|
|
||||||
followers: Int!
|
|
||||||
authors: Int!
|
|
||||||
viewed: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Topic {
|
|
||||||
id: Int!
|
|
||||||
slug: String!
|
|
||||||
title: String
|
|
||||||
body: String
|
|
||||||
pic: String
|
|
||||||
stat: TopicStat
|
|
||||||
oid: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type Invite {
|
|
||||||
id: Int!
|
|
||||||
inviter_id: Int!
|
|
||||||
author_id: Int!
|
|
||||||
shout_id: Int!
|
|
||||||
status: InviteStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
# Входные типы
|
|
||||||
|
|
||||||
input ShoutInput {
|
|
||||||
slug: String
|
|
||||||
title: String
|
|
||||||
body: String
|
|
||||||
lead: String
|
|
||||||
description: String
|
|
||||||
layout: String
|
|
||||||
media: String
|
|
||||||
authors: [String]
|
|
||||||
topics: [TopicInput]
|
|
||||||
community: Int
|
|
||||||
subtitle: String
|
|
||||||
cover: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input ProfileInput {
|
|
||||||
slug: String
|
|
||||||
name: String
|
|
||||||
pic: String
|
|
||||||
links: [String]
|
|
||||||
bio: String
|
|
||||||
about: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input TopicInput {
|
|
||||||
id: Int
|
|
||||||
slug: String!
|
|
||||||
title: String
|
|
||||||
body: String
|
|
||||||
pic: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input ReactionInput {
|
|
||||||
kind: ReactionKind!
|
|
||||||
shout: Int!
|
|
||||||
quote: String
|
|
||||||
body: String
|
|
||||||
reply_to: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
input AuthorsBy {
|
|
||||||
last_seen: Int
|
|
||||||
created_at: Int
|
|
||||||
slug: String
|
|
||||||
name: String
|
|
||||||
topic: String
|
|
||||||
order: String
|
|
||||||
after: Int
|
|
||||||
stat: String
|
|
||||||
}
|
|
||||||
|
|
||||||
input LoadShoutsFilters {
|
|
||||||
topic: String
|
|
||||||
author: String
|
|
||||||
layouts: [String]
|
|
||||||
published: Boolean
|
|
||||||
after: Int
|
|
||||||
reacted: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
input LoadShoutsOptions {
|
|
||||||
filters: LoadShoutsFilters
|
|
||||||
with_author_captions: Boolean
|
|
||||||
limit: Int!
|
|
||||||
random_limit: Int
|
|
||||||
offset: Int
|
|
||||||
order_by: String
|
|
||||||
order_by_desc: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
input ReactionBy {
|
|
||||||
shout: String
|
|
||||||
shouts: [String]
|
|
||||||
search: String
|
|
||||||
comment: Boolean
|
|
||||||
topic: String
|
|
||||||
created_by: Int
|
|
||||||
after: Int
|
|
||||||
sort: String
|
|
||||||
}
|
|
||||||
|
|
||||||
# output type
|
|
||||||
|
|
||||||
type Result {
|
|
||||||
error: String
|
|
||||||
slugs: [String]
|
|
||||||
shout: Shout
|
|
||||||
shouts: [Shout]
|
|
||||||
author: Author
|
|
||||||
authors: [Author]
|
|
||||||
reaction: Reaction
|
|
||||||
reactions: [Reaction]
|
|
||||||
topic: Topic
|
|
||||||
topics: [Topic]
|
|
||||||
community: Community
|
|
||||||
communities: [Community]
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchResult {
|
|
||||||
slug: String!
|
|
||||||
title: String!
|
|
||||||
cover: String
|
|
||||||
main_topic: String
|
|
||||||
created_at: Int
|
|
||||||
authors: [Author]
|
|
||||||
topics: [Topic]
|
|
||||||
score: Float!
|
|
||||||
}
|
|
||||||
|
|
||||||
# Мутации
|
|
||||||
|
|
||||||
type Mutation {
|
|
||||||
# author
|
|
||||||
rate_author(rated_slug: String!, value: Int!): Result!
|
|
||||||
update_profile(profile: ProfileInput!): Result!
|
|
||||||
|
|
||||||
# editor
|
|
||||||
create_shout(inp: ShoutInput!): Result!
|
|
||||||
update_shout(shout_id: Int!, shout_input: ShoutInput, publish: Boolean): Result!
|
|
||||||
delete_shout(shout_id: Int!): Result!
|
|
||||||
|
|
||||||
# follower
|
|
||||||
follow(what: FollowingEntity!, slug: String!): Result!
|
|
||||||
unfollow(what: FollowingEntity!, slug: String!): Result!
|
|
||||||
|
|
||||||
# topic
|
|
||||||
create_topic(input: TopicInput!): Result!
|
|
||||||
update_topic(input: TopicInput!): Result!
|
|
||||||
delete_topic(slug: String!): Result!
|
|
||||||
|
|
||||||
# reaction
|
|
||||||
create_reaction(reaction: ReactionInput!): Result!
|
|
||||||
update_reaction(id: Int!, reaction: ReactionInput!): Result!
|
|
||||||
delete_reaction(reaction_id: Int!): Result!
|
|
||||||
|
|
||||||
# collab
|
|
||||||
create_invite(slug: String, author_id: Int): Result!
|
|
||||||
remove_author(slug: String, author_id: Int): Result!
|
|
||||||
remove_invite(invite_id: Int!): Result!
|
|
||||||
accept_invite(invite_id: Int!): Result!
|
|
||||||
reject_invite(invite_id: Int!): Result!
|
|
||||||
}
|
|
||||||
|
|
||||||
# Запросы
|
|
||||||
|
|
||||||
type Query {
|
|
||||||
# author
|
|
||||||
get_author(slug: String, author_id: Int): Author
|
|
||||||
get_author_id(user: String!): Author
|
|
||||||
get_authors_all: [Author]
|
|
||||||
get_author_followers(slug: String, user: String, author_id: Int): [Author]
|
|
||||||
get_author_followed(slug: String, user: String, author_id: Int): [Author]
|
|
||||||
load_authors_by(by: AuthorsBy!, limit: Int, offset: Int): [Author]
|
|
||||||
|
|
||||||
# community
|
|
||||||
get_community: Community
|
|
||||||
get_communities_all: [Community]
|
|
||||||
|
|
||||||
# editor
|
|
||||||
get_shouts_drafts: [Shout]
|
|
||||||
|
|
||||||
# follower
|
|
||||||
get_my_followed: Result # { authors topics communities }
|
|
||||||
get_shout_followers(slug: String, shout_id: Int): [Author]
|
|
||||||
|
|
||||||
# reaction
|
|
||||||
load_reactions_by(by: ReactionBy!, limit: Int, offset: Int): [Reaction]
|
|
||||||
|
|
||||||
# reader
|
|
||||||
get_shout(slug: String, shout_id: Int): Shout
|
|
||||||
load_shouts_followed(follower_id: Int!, limit: Int, offset: Int): [Shout] # userReactedShouts
|
|
||||||
load_shouts_by(options: LoadShoutsOptions): [Shout]
|
|
||||||
load_shouts_search(text: String!, limit: Int, offset: Int): [SearchResult]
|
|
||||||
load_shouts_feed(options: LoadShoutsOptions): [Shout]
|
|
||||||
load_shouts_unrated(limit: Int, offset: Int): [Shout]
|
|
||||||
load_shouts_random_top(options: LoadShoutsOptions): [Shout]
|
|
||||||
load_shouts_random_topic(limit: Int!): Result! # { topic shouts }
|
|
||||||
load_shouts_drafts: [Shout]
|
|
||||||
|
|
||||||
# topic
|
|
||||||
get_topic(slug: String!): Topic
|
|
||||||
get_topics_all: [Topic]
|
|
||||||
get_topics_random(amount: Int): [Topic]
|
|
||||||
get_topics_by_author(slug: String, user: String, author_id: Int): [Topic]
|
|
||||||
get_topics_by_community(slug: String, community_id: Int): [Topic]
|
|
||||||
}
|
|
46
services/diff.py
Normal file
46
services/diff.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import re
|
||||||
|
from difflib import ndiff
|
||||||
|
|
||||||
|
|
||||||
|
def get_diff(original, modified):
|
||||||
|
"""
|
||||||
|
Get the difference between two strings using difflib.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- original: The original string.
|
||||||
|
- modified: The modified string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of differences.
|
||||||
|
"""
|
||||||
|
diff = list(ndiff(original.split(), modified.split()))
|
||||||
|
return diff
|
||||||
|
|
||||||
|
def apply_diff(original, diff):
|
||||||
|
"""
|
||||||
|
Apply the difference to the original string.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- original: The original string.
|
||||||
|
- diff: The difference obtained from get_diff function.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The modified string.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
pattern = re.compile(r'^(\+|-) ')
|
||||||
|
|
||||||
|
for line in diff:
|
||||||
|
match = pattern.match(line)
|
||||||
|
if match:
|
||||||
|
op = match.group(1)
|
||||||
|
content = line[2:]
|
||||||
|
if op == '+':
|
||||||
|
result.append(content)
|
||||||
|
elif op == '-':
|
||||||
|
# Ignore deleted lines
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
result.append(line)
|
||||||
|
|
||||||
|
return ' '.join(result)
|
Loading…
Reference in New Issue
Block a user