mirror of https://gitlab.com/litecord/litecord.git
541 lines
15 KiB
Python
541 lines
15 KiB
Python
import time
|
|
|
|
from quart import Blueprint, request, current_app as app, jsonify
|
|
from logbook import Logger
|
|
|
|
from ..auth import token_check
|
|
from ..snowflake import get_snowflake, snowflake_datetime
|
|
from ..enums import ChannelType, MessageType, GUILD_CHANS
|
|
from ..errors import Forbidden, ChannelNotFound, MessageNotFound
|
|
from ..schemas import validate, MESSAGE_CREATE
|
|
|
|
from .checks import channel_check, guild_check
|
|
|
|
log = Logger(__name__)
|
|
bp = Blueprint('channels', __name__)
|
|
|
|
|
|
@bp.route('/<int:channel_id>', methods=['GET'])
|
|
async def get_channel(channel_id):
|
|
"""Get a single channel's information"""
|
|
user_id = await token_check()
|
|
|
|
# channel_check takes care of checking
|
|
# DMs and group DMs
|
|
await channel_check(user_id, channel_id)
|
|
chan = await app.storage.get_channel(channel_id)
|
|
|
|
if not chan:
|
|
raise ChannelNotFound('single channel not found')
|
|
|
|
return jsonify(chan)
|
|
|
|
|
|
async def __guild_chan_sql(guild_id, channel_id, field: str) -> str:
|
|
"""Update a guild's channel id field to NULL,
|
|
if it was set to the given channel id before."""
|
|
return await app.db.execute(f"""
|
|
UPDATE guilds
|
|
SET {field} = NULL
|
|
WHERE guilds.id = $1 AND {field} = $2
|
|
""", guild_id, channel_id)
|
|
|
|
|
|
async def _update_guild_chan_text(guild_id: int, channel_id: int):
|
|
res_embed = await __guild_chan_sql(
|
|
guild_id, channel_id, 'embed_channel_id')
|
|
|
|
res_widget = await __guild_chan_sql(
|
|
guild_id, channel_id, 'widget_channel_id')
|
|
|
|
res_system = await __guild_chan_sql(
|
|
guild_id, channel_id, 'system_channel_id')
|
|
|
|
# if none of them were actually updated,
|
|
# ignore and dont dispatch anything
|
|
if 'UPDATE 1' not in (res_embed, res_widget, res_system):
|
|
return
|
|
|
|
# at least one of the fields were updated,
|
|
# dispatch GUILD_UPDATE
|
|
guild = await app.storage.get_guild(guild_id)
|
|
await app.dispatcher.dispatch_guild(
|
|
guild_id, 'GUILD_UPDATE', guild)
|
|
|
|
|
|
async def _update_guild_chan_voice(guild_id: int, channel_id: int):
|
|
res = await __guild_chan_sql(guild_id, channel_id, 'afk_channel_id')
|
|
|
|
# guild didnt update
|
|
if res == 'UPDATE 0':
|
|
return
|
|
|
|
guild = await app.storage.get_guild(guild_id)
|
|
await app.dispatcher.dispatch_guild(
|
|
guild_id, 'GUILD_UPDATE', guild)
|
|
|
|
|
|
async def _update_guild_chan_cat(guild_id: int, channel_id: int):
|
|
# get all channels that were childs of the category
|
|
childs = await app.db.fetch("""
|
|
SELECT id
|
|
FROM guild_channels
|
|
WHERE guild_id = $1 AND parent_id = $2
|
|
""", guild_id, channel_id)
|
|
childs = [c['id'] for c in childs]
|
|
|
|
# update every child channel to parent_id = NULL
|
|
await app.db.execute("""
|
|
UPDATE guild_channels
|
|
SET parent_id = NULL
|
|
WHERE guild_id = $1 AND parent_id = $2
|
|
""", guild_id, channel_id)
|
|
|
|
# tell all people in the guild of the category removal
|
|
for child_id in childs:
|
|
child = await app.storage.get_channel(child_id)
|
|
await app.dispatcher.dispatch_guild(
|
|
guild_id, 'CHANNEL_UPDATE', child
|
|
)
|
|
|
|
|
|
async def delete_messages(channel_id):
|
|
await app.db.execute("""
|
|
DELETE FROM channel_pins
|
|
WHERE channel_id = $1
|
|
""", channel_id)
|
|
|
|
await app.db.execute("""
|
|
DELETE FROM user_read_state
|
|
WHERE channel_id = $1
|
|
""", channel_id)
|
|
|
|
await app.db.execute("""
|
|
DELETE FROM messages
|
|
WHERE channel_id = $1
|
|
""", channel_id)
|
|
|
|
|
|
async def guild_cleanup(channel_id):
|
|
await app.db.execute("""
|
|
DELETE FROM channel_overwrites
|
|
WHERE channel_id = $1
|
|
""", channel_id)
|
|
|
|
await app.db.execute("""
|
|
DELETE FROM invites
|
|
WHERE channel_id = $1
|
|
""", channel_id)
|
|
|
|
await app.db.execute("""
|
|
DELETE FROM webhooks
|
|
WHERE channel_id = $1
|
|
""", channel_id)
|
|
|
|
|
|
@bp.route('/<int:channel_id>', methods=['DELETE'])
|
|
async def close_channel(channel_id):
|
|
user_id = await token_check()
|
|
|
|
chan_type = await app.storage.get_chan_type(channel_id)
|
|
ctype = ChannelType(chan_type)
|
|
|
|
if ctype in GUILD_CHANS:
|
|
_, guild_id = await channel_check(user_id, channel_id)
|
|
chan = await app.storage.get_channel(channel_id)
|
|
|
|
# the selected function will take care of checking
|
|
# the sanity of tables once the channel becomes deleted.
|
|
_update_func = {
|
|
ChannelType.GUILD_TEXT: _update_guild_chan_text,
|
|
ChannelType.GUILD_VOICE: _update_guild_chan_voice,
|
|
ChannelType.GUILD_CATEGORY: _update_guild_chan_cat,
|
|
}[ctype]
|
|
|
|
main_tbl = {
|
|
ChannelType.GUILD_TEXT: 'guild_text_channels',
|
|
ChannelType.GUILD_VOICE: 'guild_voice_channels',
|
|
|
|
# TODO: categories?
|
|
}[ctype]
|
|
|
|
await _update_func(guild_id, channel_id)
|
|
|
|
# for some reason ON DELETE CASCADE
|
|
# didn't work on my setup, so I delete
|
|
# everything before moving to the main
|
|
# channel table deletes
|
|
await delete_messages(channel_id)
|
|
await guild_cleanup(channel_id)
|
|
|
|
await app.db.execute(f"""
|
|
DELETE FROM {main_tbl}
|
|
WHERE id = $1
|
|
""", channel_id)
|
|
|
|
await app.db.execute("""
|
|
DELETE FROM guild_channels
|
|
WHERE id = $1
|
|
""", channel_id)
|
|
|
|
await app.db.execute("""
|
|
DELETE FROM channels
|
|
WHERE id = $1
|
|
""", channel_id)
|
|
|
|
await app.dispatcher.dispatch_guild(
|
|
guild_id, 'CHANNEL_DELETE', chan)
|
|
return jsonify(chan)
|
|
|
|
if ctype == ChannelType.DM:
|
|
chan = await app.storage.get_channel(channel_id)
|
|
|
|
# we don't ever actually delete DM channels off the database.
|
|
# instead, we close the channel for the user that is making
|
|
# the request via removing the link between them and
|
|
# the channel on dm_channel_state
|
|
await app.db.execute("""
|
|
DELETE FROM dm_channel_state (user_id, dm_id)
|
|
VALUES ($1, $2)
|
|
""", user_id, channel_id)
|
|
|
|
# nothing happens to the other party of the dm channel
|
|
await app.dispacher.dispatch_user(user_id, 'CHANNEL_DELETE', chan)
|
|
return jsonify(chan)
|
|
|
|
if ctype == ChannelType.GROUP_DM:
|
|
# TODO: group dm
|
|
pass
|
|
|
|
return '', 404
|
|
|
|
|
|
@bp.route('/<int:channel_id>/messages', methods=['GET'])
|
|
async def get_messages(channel_id):
|
|
user_id = await token_check()
|
|
await channel_check(user_id, channel_id)
|
|
|
|
# TODO: before, after, around keys
|
|
|
|
message_ids = await app.db.fetch(f"""
|
|
SELECT id
|
|
FROM messages
|
|
WHERE channel_id = $1
|
|
ORDER BY id DESC
|
|
LIMIT 100
|
|
""", channel_id)
|
|
|
|
result = []
|
|
|
|
for message_id in message_ids:
|
|
msg = await app.storage.get_message(message_id['id'])
|
|
|
|
if msg is None:
|
|
continue
|
|
|
|
result.append(msg)
|
|
|
|
log.info('Fetched {} messages', len(result))
|
|
return jsonify(result)
|
|
|
|
|
|
@bp.route('/<int:channel_id>/messages/<int:message_id>', methods=['GET'])
|
|
async def get_single_message(channel_id, message_id):
|
|
user_id = await token_check()
|
|
await channel_check(user_id, channel_id)
|
|
|
|
# TODO: check READ_MESSAGE_HISTORY permissions
|
|
message = await app.storage.get_message(message_id)
|
|
|
|
if not message:
|
|
raise MessageNotFound()
|
|
|
|
return jsonify(message)
|
|
|
|
|
|
@bp.route('/<int:channel_id>/messages', methods=['POST'])
|
|
async def create_message(channel_id):
|
|
user_id = await token_check()
|
|
_ctype, guild_id = await channel_check(user_id, channel_id)
|
|
|
|
j = validate(await request.get_json(), MESSAGE_CREATE)
|
|
message_id = get_snowflake()
|
|
|
|
# TODO: check SEND_MESSAGES permission
|
|
# TODO: check SEND_TTS_MESSAGES
|
|
# TODO: check connection to the gateway
|
|
|
|
await app.db.execute(
|
|
"""
|
|
INSERT INTO messages (id, channel_id, author_id, content, tts,
|
|
mention_everyone, nonce, message_type)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
""",
|
|
message_id,
|
|
channel_id,
|
|
user_id,
|
|
j['content'],
|
|
j.get('tts', False),
|
|
'@everyone' in j['content'],
|
|
int(j.get('nonce', 0)),
|
|
MessageType.DEFAULT.value
|
|
)
|
|
|
|
# TODO: dispatch_channel
|
|
# we really need dispatch_channel to make dm messages work,
|
|
# since they aren't part of any existing guild.
|
|
payload = await app.storage.get_message(message_id)
|
|
|
|
await app.dispatcher.dispatch('channel', channel_id,
|
|
'MESSAGE_CREATE', payload)
|
|
|
|
# TODO: dispatch the MESSAGE_CREATE to any mentioning user.
|
|
|
|
for str_uid in payload['mentions']:
|
|
uid = int(str_uid)
|
|
|
|
await app.db.execute("""
|
|
UPDATE user_read_state
|
|
SET mention_count += 1
|
|
WHERE user_id = $1 AND channel_id = $2
|
|
""", uid, channel_id)
|
|
|
|
return jsonify(payload)
|
|
|
|
|
|
@bp.route('/<int:channel_id>/messages/<int:message_id>', methods=['PATCH'])
|
|
async def edit_message(channel_id, message_id):
|
|
user_id = await token_check()
|
|
_ctype, guild_id = await channel_check(user_id, channel_id)
|
|
|
|
author_id = await app.db.fetchval("""
|
|
SELECT author_id FROM messages
|
|
WHERE messages.id = $1
|
|
""", message_id)
|
|
|
|
if not author_id == user_id:
|
|
raise Forbidden('You can not edit this message')
|
|
|
|
j = await request.get_json()
|
|
updated = 'content' in j or 'embed' in j
|
|
|
|
if 'content' in j:
|
|
await app.db.execute("""
|
|
UPDATE messages
|
|
SET content=$1
|
|
WHERE messages.id = $2
|
|
""", j['content'], message_id)
|
|
|
|
# TODO: update embed
|
|
|
|
message = await app.storage.get_message(message_id)
|
|
|
|
# only dispatch MESSAGE_UPDATE if we actually had any update to start with
|
|
if updated:
|
|
await app.dispatcher.dispatch('channel', channel_id,
|
|
'MESSAGE_UPDATE', message)
|
|
|
|
return jsonify(message)
|
|
|
|
|
|
@bp.route('/<int:channel_id>/messages/<int:message_id>', methods=['DELETE'])
|
|
async def delete_message(channel_id, message_id):
|
|
user_id = await token_check()
|
|
_ctype, guild_id = await channel_check(user_id, channel_id)
|
|
|
|
author_id = await app.db.fetchval("""
|
|
SELECT author_id FROM messages
|
|
WHERE messages.id = $1
|
|
""", message_id)
|
|
|
|
# TODO: MANAGE_MESSAGES permission check
|
|
if author_id != user_id:
|
|
raise Forbidden('You can not delete this message')
|
|
|
|
await app.db.execute("""
|
|
DELETE FROM messages
|
|
WHERE messages.id = $1
|
|
""", message_id)
|
|
|
|
await app.dispatcher.dispatch(
|
|
'channel', channel_id,
|
|
'MESSAGE_DELETE', {
|
|
'id': str(message_id),
|
|
'channel_id': str(channel_id),
|
|
|
|
# for lazy guilds
|
|
'guild_id': str(guild_id),
|
|
})
|
|
|
|
return '', 204
|
|
|
|
|
|
@bp.route('/<int:channel_id>/pins', methods=['GET'])
|
|
async def get_pins(channel_id):
|
|
user_id = await token_check()
|
|
await channel_check(user_id, channel_id)
|
|
|
|
ids = await app.db.fetch("""
|
|
SELECT message_id
|
|
FROM channel_pins
|
|
WHERE channel_id = $1
|
|
ORDER BY message_id ASC
|
|
""", channel_id)
|
|
|
|
ids = [r['message_id'] for r in ids]
|
|
res = []
|
|
|
|
for message_id in ids:
|
|
message = await app.storage.get_message(message_id)
|
|
if message is not None:
|
|
res.append(message)
|
|
|
|
return jsonify(message)
|
|
|
|
|
|
@bp.route('/<int:channel_id>/pins/<int:message_id>', methods=['PUT'])
|
|
async def add_pin(channel_id, message_id):
|
|
user_id = await token_check()
|
|
_ctype, guild_id = await channel_check(user_id, channel_id)
|
|
|
|
# TODO: check MANAGE_MESSAGES permission
|
|
|
|
await app.db.execute("""
|
|
INSERT INTO channel_pins (channel_id, message_id)
|
|
VALUES ($1, $2)
|
|
""", channel_id, message_id)
|
|
|
|
row = await app.db.fetchrow("""
|
|
SELECT message_id
|
|
FROM channel_pins
|
|
WHERE channel_id = $1
|
|
ORDER BY message_id ASC
|
|
LIMIT 1
|
|
""", channel_id)
|
|
|
|
timestamp = snowflake_datetime(row['message_id'])
|
|
|
|
await app.dispatcher.dispatch_guild(guild_id, 'CHANNEL_PINS_UPDATE', {
|
|
'channel_id': str(channel_id),
|
|
'last_pin_timestamp': timestamp.isoformat()
|
|
})
|
|
|
|
return '', 204
|
|
|
|
|
|
@bp.route('/<int:channel_id>/pins/<int:message_id>', methods=['DELETE'])
|
|
async def delete_pin(channel_id, message_id):
|
|
user_id = await token_check()
|
|
_ctype, guild_id = await channel_check(user_id, channel_id)
|
|
|
|
# TODO: check MANAGE_MESSAGES permission
|
|
|
|
await app.db.execute("""
|
|
DELETE FROM channel_pins
|
|
WHERE channel_id = $1 AND message_id = $2
|
|
""", channel_id, message_id)
|
|
|
|
row = await app.db.fetchrow("""
|
|
SELECT message_id
|
|
FROM channel_pins
|
|
WHERE channel_id = $1
|
|
ORDER BY message_id ASC
|
|
LIMIT 1
|
|
""", channel_id)
|
|
|
|
timestamp = snowflake_datetime(row['message_id'])
|
|
|
|
# TODO: dispatch_channel
|
|
await app.dispatcher.dispatch_guild(guild_id, 'CHANNEL_PINS_UPDATE', {
|
|
'channel_id': str(channel_id),
|
|
'last_pin_timestamp': timestamp.isoformat()
|
|
})
|
|
|
|
return '', 204
|
|
|
|
|
|
@bp.route('/<int:channel_id>/typing', methods=['POST'])
|
|
async def trigger_typing(channel_id):
|
|
user_id = await token_check()
|
|
_ctype, guild_id = await channel_check(user_id, channel_id)
|
|
|
|
# TODO: dispatch_channel
|
|
await app.dispatcher.dispatch_guild(guild_id, 'TYPING_START', {
|
|
'channel_id': str(channel_id),
|
|
'user_id': str(user_id),
|
|
'timestamp': int(time.time()),
|
|
|
|
# guild_id for lazy guilds
|
|
'guild_id': str(guild_id),
|
|
})
|
|
|
|
return '', 204
|
|
|
|
|
|
async def channel_ack(user_id, guild_id, channel_id, message_id: int = None):
|
|
"""ACK a channel."""
|
|
|
|
if not message_id:
|
|
message_id = await app.storage.chan_last_message(channel_id)
|
|
|
|
res = await app.db.execute("""
|
|
UPDATE user_read_state
|
|
|
|
SET last_message_id = $1,
|
|
mention_count = 0
|
|
|
|
WHERE user_id = $2 AND channel_id = $3
|
|
""", message_id, user_id, channel_id)
|
|
|
|
if res == 'UPDATE 0':
|
|
await app.db.execute("""
|
|
INSERT INTO user_read_state
|
|
(user_id, channel_id, last_message_id, mention_count)
|
|
VALUES ($1, $2, $3, $4)
|
|
""", user_id, channel_id, message_id, 0)
|
|
|
|
if guild_id:
|
|
await app.dispatcher.dispatch_user_guild(
|
|
user_id, guild_id, 'MESSAGE_ACK', {
|
|
'message_id': str(message_id),
|
|
'channel_id': str(channel_id)
|
|
})
|
|
else:
|
|
# TODO: use ChannelDispatcher
|
|
await app.dispatcher.dispatch_user(
|
|
user_id, 'MESSAGE_ACK', {
|
|
'message_id': str(message_id),
|
|
'channel_id': str(channel_id)
|
|
})
|
|
|
|
|
|
@bp.route('/<int:channel_id>/messages/<int:message_id>/ack', methods=['POST'])
|
|
async def ack_channel(channel_id, message_id):
|
|
user_id = await token_check()
|
|
ctype, guild_id = await channel_check(user_id, channel_id)
|
|
|
|
if ctype == ChannelType.DM:
|
|
guild_id = None
|
|
|
|
await channel_ack(user_id, guild_id, channel_id, message_id)
|
|
|
|
return jsonify({
|
|
# token seems to be used for
|
|
# data collection activities,
|
|
# so we never use it.
|
|
'token': None
|
|
})
|
|
|
|
|
|
@bp.route('/<int:channel_id>/messages/ack', methods=['DELETE'])
|
|
async def delete_read_state(channel_id):
|
|
user_id = await token_check()
|
|
await channel_check(user_id, channel_id)
|
|
|
|
await app.db.execute("""
|
|
DELETE FROM user_read_state
|
|
WHERE user_id = $1 AND channel_id = $2
|
|
""", user_id, channel_id)
|
|
|
|
return '', 204
|