mirror of https://gitlab.com/litecord/litecord.git
relationship support!
friendships and blocks are possible, however presence code isn't ready to handle presences of people who are friends. SQL for instances, this is going to fix bad timestamps on the messages: ```sql ALTER TABLE ONLY members ALTER COLUMN joined_at SET DEFAULT (now() at time zone 'utc'); ALTER TABLE ONLY messages ALTER COLUMN created_at SET DEFAULT (now() at time zone 'utc'); ALTER TABLE ONLY invites ALTER COLUMN created_at SET DEFAULT (now() at time zone 'utc'); ``` After that, rerun the schema.sql file to have the new relationships table. - blueprints: add relationships blueprint - enums: add RelationshipType - storage: add get_relationships - storage: fix bug on lazy guild changes and messages - schemas: return validator.document instead of reqjson - gateway.websocket: use Storage.get_relationships
This commit is contained in:
parent
b091bd5c49
commit
c7923da124
|
|
@ -7,3 +7,4 @@ from .webhooks import bp as webhooks
|
|||
from .science import bp as science
|
||||
from .voice import bp as voice
|
||||
from .invites import bp as invites
|
||||
from .relationships import bp as relationships
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
from quart import Blueprint, jsonify, request, current_app as app
|
||||
from asyncpg import UniqueViolationError
|
||||
|
||||
from ..auth import token_check
|
||||
from ..schemas import validate, RELATIONSHIP
|
||||
from ..enums import RelationshipType
|
||||
|
||||
|
||||
bp = Blueprint('relationship', __name__)
|
||||
|
||||
|
||||
@bp.route('/@me/relationships', methods=['GET'])
|
||||
async def get_me_relationships():
|
||||
user_id = await token_check()
|
||||
return jsonify(await app.storage.get_relationships(user_id))
|
||||
|
||||
|
||||
@bp.route('/@me/relationships/<int:peer_id>', methods=['PUT'])
|
||||
async def add_relationship(peer_id: int):
|
||||
"""Add a relationship to the peer."""
|
||||
user_id = await token_check()
|
||||
payload = validate(await request.get_json(), RELATIONSHIP)
|
||||
rel_type = payload['type']
|
||||
_friend = RelationshipType.FRIEND.value
|
||||
|
||||
await app.db.execute("""
|
||||
INSERT INTO relationships (user_id, peer_id, rel_type)
|
||||
VALUES ($1, $2, $3)
|
||||
""", user_id, peer_id, rel_type)
|
||||
|
||||
# check if this is an acceptance
|
||||
# of a friend request
|
||||
existing = await app.db.fetchrow("""
|
||||
SELECT user_id, peer_id
|
||||
FROM relationships
|
||||
WHERE user_id = $1 AND peer_id = $2 AND rel_type = $3
|
||||
""", peer_id, user_id, _friend)
|
||||
|
||||
_dispatch = app.dispatcher.dispatch_user
|
||||
|
||||
if existing:
|
||||
# accepted a friend request, dispatch respective
|
||||
# relationship events
|
||||
await _dispatch(user_id, 'RELATIONSHIP_REMOVE', {
|
||||
'type': RelationshipType.INCOMING.value,
|
||||
'id': str(peer_id)
|
||||
})
|
||||
|
||||
await _dispatch(user_id, 'RELATIONSHIP_ADD', {
|
||||
'type': _friend,
|
||||
'id': str(peer_id),
|
||||
'user': await app.storage.get_user(peer_id)
|
||||
})
|
||||
|
||||
await _dispatch(peer_id, 'RELATIONSHIP_ADD', {
|
||||
'type': _friend,
|
||||
'id': str(user_id),
|
||||
'user': await app.storage.get_user(user_id)
|
||||
})
|
||||
|
||||
return '', 204
|
||||
|
||||
# is a blocky!
|
||||
await app.dispatcher.dispatch_user(user_id, 'RELATIONSHIP_ADD', {
|
||||
'id': str(peer_id),
|
||||
'type': RelationshipType.BLOCK.value,
|
||||
'user': await app.storage.get_user(peer_id)
|
||||
})
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
@bp.route('/@me/relationships/<int:peer_id>', methods=['DELETE'])
|
||||
async def remove_relationship(peer_id: int):
|
||||
"""Remove an existing relationship"""
|
||||
user_id = await token_check()
|
||||
_friend = RelationshipType.FRIEND.value
|
||||
_block = RelationshipType.BLOCK.value
|
||||
_dispatch = app.dispatcher.dispatch_user
|
||||
|
||||
rel_type = await app.db.fetchval("""
|
||||
SELECT rel_type
|
||||
FROM relationships
|
||||
WHERE user_id = $1 AND peer_id = $2
|
||||
""", user_id, peer_id)
|
||||
|
||||
incoming_rel_type = await app.db.fetchval("""
|
||||
SELECT rel_type
|
||||
FROM relationships
|
||||
WHERE user_id = $1 AND peer_id = $2
|
||||
""", peer_id, user_id)
|
||||
|
||||
if rel_type == _friend:
|
||||
# closing the friendship, have to delete both rows
|
||||
await app.db.execute("""
|
||||
DELETE FROM relationships
|
||||
WHERE (
|
||||
(user_id = $1 AND peer_id = $2) OR
|
||||
(user_id = $2 AND peer_id = $1)
|
||||
) AND rel_type = $3
|
||||
""", user_id, peer_id, _friend)
|
||||
|
||||
# if there wasnt any mutual friendship before,
|
||||
# assume they were requests of INCOMING
|
||||
# and OUTGOING.
|
||||
user_del_type = RelationshipType.OUTGOING.value if \
|
||||
incoming_rel_type != _friend else _friend
|
||||
|
||||
await _dispatch(user_id, 'RELATIONSHIP_REMOVE', {
|
||||
'id': str(peer_id),
|
||||
'type': user_del_type,
|
||||
})
|
||||
|
||||
peer_del_type = RelationshipType.INCOMING.value if \
|
||||
incoming_rel_type != _friend else _friend
|
||||
|
||||
await _dispatch(peer_id, 'RELATIONSHIP_REMOVE', {
|
||||
'id': str(user_id),
|
||||
'type': peer_del_type,
|
||||
})
|
||||
|
||||
return '', 204
|
||||
|
||||
# was a block!
|
||||
await app.db.execute("""
|
||||
DELETE FROM relationships
|
||||
WHERE user_id = $1 AND peer_id = $2 AND rel_type = $3
|
||||
""", user_id, peer_id, _block)
|
||||
|
||||
await _dispatch(user_id, 'RELATIONSHIP_REMOVE', {
|
||||
'id': str(peer_id),
|
||||
'type': _block,
|
||||
})
|
||||
|
||||
return '', 204
|
||||
|
|
@ -77,3 +77,10 @@ class ExplicitFilter(EasyEnum):
|
|||
EDGE = 0
|
||||
FRIENDS = 1
|
||||
SAFE = 2
|
||||
|
||||
|
||||
class RelationshipType(EasyEnum):
|
||||
FRIEND = 1
|
||||
BLOCK = 2
|
||||
INCOMING = 3
|
||||
OUTGOING = 4
|
||||
|
|
|
|||
|
|
@ -206,11 +206,9 @@ class GatewayWebsocket:
|
|||
return {
|
||||
'user_settings': await self.storage.get_user_settings(user_id),
|
||||
'notes': await self.storage.fetch_notes(user_id),
|
||||
'relationships': await self.storage.get_relationships(user_id),
|
||||
'friend_suggestion_count': 0,
|
||||
|
||||
# TODO
|
||||
'relationships': [],
|
||||
|
||||
# TODO
|
||||
'user_guild_settings': [],
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from cerberus import Validator
|
|||
from logbook import Logger
|
||||
|
||||
from .errors import BadRequest
|
||||
from .enums import ActivityType, StatusType, ExplicitFilter
|
||||
from .enums import ActivityType, StatusType, ExplicitFilter, RelationshipType
|
||||
|
||||
|
||||
log = Logger(__name__)
|
||||
|
|
@ -57,20 +57,29 @@ class LitecordValidator(Validator):
|
|||
|
||||
return val in ExplicitFilter.values()
|
||||
|
||||
def _validate_type_rel_type(self, value: str) -> bool:
|
||||
try:
|
||||
val = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
return val in (RelationshipType.FRIEND.value,
|
||||
RelationshipType.BLOCK.value)
|
||||
|
||||
|
||||
def validate(reqjson, schema, raise_err: bool = True):
|
||||
validator = LitecordValidator(schema)
|
||||
|
||||
if not validator.validate(reqjson):
|
||||
errs = validator.errors
|
||||
log.warning('Error validating doc: {!r}', errs)
|
||||
log.warning('Error validating doc {!r}: {!r}', reqjson, errs)
|
||||
|
||||
if raise_err:
|
||||
raise BadRequest('bad payload', errs)
|
||||
|
||||
return None
|
||||
|
||||
return reqjson
|
||||
return validator.document
|
||||
|
||||
|
||||
GUILD_UPDATE = {
|
||||
|
|
@ -262,3 +271,11 @@ USER_SETTINGS = {
|
|||
|
||||
'timezone_offset': {'type': 'number', 'required': False},
|
||||
}
|
||||
|
||||
RELATIONSHIP = {
|
||||
'type': {
|
||||
'type': 'rel_type',
|
||||
'required': False,
|
||||
'default': RelationshipType.FRIEND.value
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import List, Dict, Any
|
|||
|
||||
from logbook import Logger
|
||||
|
||||
from .enums import ChannelType
|
||||
from .enums import ChannelType, RelationshipType
|
||||
from .schemas import USER_MENTION, ROLE_MENTION
|
||||
|
||||
|
||||
|
|
@ -61,6 +61,7 @@ class Storage:
|
|||
"""Get a single user payload."""
|
||||
user_id = int(user_id)
|
||||
|
||||
# TODO: query less instead of popping when secure=True
|
||||
user_row = await self.db.fetchrow("""
|
||||
SELECT id::text, username, discriminator, avatar, email,
|
||||
flags, bot, mfa_enabled, verified, premium
|
||||
|
|
@ -463,7 +464,7 @@ class Storage:
|
|||
SELECT guild_id
|
||||
FROM guild_channels
|
||||
WHERE guild_channels.id = $1
|
||||
""", res['channel_id'])
|
||||
""", int(res['channel_id']))
|
||||
|
||||
# only insert when the channel
|
||||
# is actually from a guild.
|
||||
|
|
@ -581,3 +582,96 @@ class Storage:
|
|||
drow = dict(row)
|
||||
drow.pop('id')
|
||||
return drow
|
||||
|
||||
async def get_relationships(self, user_id: int) -> List[Dict[str, Any]]:
|
||||
"""Get all relationships for a user."""
|
||||
# first, fetch all friendships outgoing
|
||||
# from the user
|
||||
_friend = RelationshipType.FRIEND.value
|
||||
_block = RelationshipType.BLOCK.value
|
||||
_incoming = RelationshipType.INCOMING.value
|
||||
_outgoing = RelationshipType.OUTGOING.value
|
||||
|
||||
# check all outgoing friends
|
||||
friends = await self.db.fetch("""
|
||||
SELECT user_id, peer_id, rel_type
|
||||
FROM relationships
|
||||
WHERE user_id = $1 AND rel_type = $2
|
||||
""", user_id, _friend)
|
||||
friends = list(map(dict, friends))
|
||||
|
||||
# mutuals is a list of ints
|
||||
# of people who are actually friends
|
||||
# and accepted the friend request
|
||||
mutuals = []
|
||||
|
||||
# for each outgoing, find if theres an outgoing from them
|
||||
for row in friends:
|
||||
is_friend = await self.db.fetchrow(
|
||||
"""
|
||||
SELECT user_id, peer_id
|
||||
FROM relationships
|
||||
WHERE user_id = $1 AND peer_id = $2 AND rel_type = $3
|
||||
""", row['peer_id'], row['user_id'],
|
||||
_friend)
|
||||
|
||||
if is_friend is not None:
|
||||
mutuals.append(row['peer_id'])
|
||||
|
||||
# fetch friend requests directed at us
|
||||
incoming_friends = await self.db.fetch("""
|
||||
SELECT user_id, peer_id
|
||||
FROM relationships
|
||||
WHERE peer_id = $1 AND rel_type = $2
|
||||
""", user_id, _friend)
|
||||
|
||||
# only need their ids
|
||||
incoming_friends = [r['user_id'] for r in incoming_friends
|
||||
if r['user_id'] not in mutuals]
|
||||
|
||||
# only fetch blocks we did,
|
||||
# not fetching the ones people did to us
|
||||
blocks = await self.db.fetch("""
|
||||
SELECT user_id, peer_id
|
||||
FROM relationships
|
||||
WHERE user_id = $1 AND rel_type = $2
|
||||
""", user_id, _block)
|
||||
blocks = list(map(dict, blocks))
|
||||
|
||||
res = []
|
||||
|
||||
for drow in friends:
|
||||
drow['type'] = drow['rel_type']
|
||||
drow.pop('rel_type')
|
||||
|
||||
# check if the receiver is a mutual
|
||||
# if it isnt, its still on a friend request stage
|
||||
if drow['peer_id'] not in mutuals:
|
||||
drow['id'] = str(drow['peer_id'])
|
||||
drow['type'] = _outgoing
|
||||
|
||||
drow['user'] = await self.get_user(drow['peer_id'])
|
||||
|
||||
drow.pop('user_id')
|
||||
drow.pop('peer_id')
|
||||
res.append(drow)
|
||||
|
||||
for peer_id in incoming_friends:
|
||||
res.append({
|
||||
'id': str(peer_id),
|
||||
'user': await self.get_user(peer_id),
|
||||
'type': _incoming,
|
||||
})
|
||||
|
||||
for drow in blocks:
|
||||
drow['type'] = drow['rel_type']
|
||||
drow.pop('rel_type')
|
||||
|
||||
drow['id'] = str(drow['peer_id'])
|
||||
drow['user'] = await self.get_user(drow['peer_id'])
|
||||
|
||||
drow.pop('user_id')
|
||||
drow.pop('peer_id')
|
||||
res.append(drow)
|
||||
|
||||
return res
|
||||
|
|
|
|||
5
run.py
5
run.py
|
|
@ -11,7 +11,7 @@ from logbook.compat import redirect_logging
|
|||
|
||||
import config
|
||||
from litecord.blueprints import gateway, auth, users, guilds, channels, \
|
||||
webhooks, science, voice, invites
|
||||
webhooks, science, voice, invites, relationships
|
||||
from litecord.gateway import websocket_handler
|
||||
from litecord.errors import LitecordError
|
||||
from litecord.gateway.state_manager import StateManager
|
||||
|
|
@ -49,12 +49,13 @@ bps = {
|
|||
gateway: None,
|
||||
auth: '/auth',
|
||||
users: '/users',
|
||||
relationships: '/users',
|
||||
guilds: '/guilds',
|
||||
channels: '/channels',
|
||||
webhooks: None,
|
||||
science: None,
|
||||
voice: '/voice',
|
||||
invites: None
|
||||
invites: None,
|
||||
}
|
||||
|
||||
for bp, suffix in bps.items():
|
||||
|
|
|
|||
21
schema.sql
21
schema.sql
|
|
@ -143,6 +143,21 @@ CREATE TABLE IF NOT EXISTS user_settings (
|
|||
);
|
||||
|
||||
|
||||
-- main user relationships
|
||||
CREATE TABLE IF NOT EXISTS relationships (
|
||||
-- the id of who made the relationship
|
||||
user_id bigint REFERENCES users (id),
|
||||
|
||||
-- the id of the peer who got a friendship
|
||||
-- request or a block.
|
||||
peer_id bigint REFERENCES users (id),
|
||||
|
||||
rel_type SMALLINT,
|
||||
|
||||
PRIMARY KEY (user_id, peer_id)
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
user_id bigint REFERENCES users (id),
|
||||
target_id bigint REFERENCES users (id),
|
||||
|
|
@ -299,7 +314,7 @@ CREATE TABLE IF NOT EXISTS invites (
|
|||
channel_id bigint REFERENCES channels (id) ON DELETE CASCADE,
|
||||
inviter bigint REFERENCES users (id),
|
||||
|
||||
created_at timestamp without time zone default now(),
|
||||
created_at timestamp without time zone default (now() at time zone 'utc'),
|
||||
uses bigint DEFAULT 0,
|
||||
|
||||
-- -1 means infinite here
|
||||
|
|
@ -333,7 +348,7 @@ CREATE TABLE IF NOT EXISTS members (
|
|||
user_id bigint REFERENCES users (id) ON DELETE CASCADE,
|
||||
guild_id bigint REFERENCES guilds (id) ON DELETE CASCADE,
|
||||
nickname text DEFAULT NULL,
|
||||
joined_at timestamp without time zone default now(),
|
||||
joined_at timestamp without time zone default (now() at time zone 'utc'),
|
||||
deafened boolean DEFAULT false,
|
||||
muted boolean DEFAULT false,
|
||||
PRIMARY KEY (user_id, guild_id)
|
||||
|
|
@ -404,7 +419,7 @@ CREATE TABLE IF NOT EXISTS messages (
|
|||
|
||||
content text,
|
||||
|
||||
created_at timestamp without time zone default now(),
|
||||
created_at timestamp without time zone default (now() at time zone 'utc'),
|
||||
edited_at timestamp without time zone default NULL,
|
||||
|
||||
tts bool default false,
|
||||
|
|
|
|||
Loading…
Reference in New Issue