diff --git a/litecord/blueprints/channels.py b/litecord/blueprints/channels.py index ff80c42..feea63e 100644 --- a/litecord/blueprints/channels.py +++ b/litecord/blueprints/channels.py @@ -46,6 +46,7 @@ from litecord.embed.messages import process_url_embed, msg_update_embeds from litecord.common.channels import channel_ack from litecord.pubsub.user import dispatch_user from litecord.permissions import get_permissions, Permissions +from litecord.errors import GuildNotFound log = Logger(__name__) bp = Blueprint("channels", __name__) @@ -397,13 +398,16 @@ async def _process_overwrites(guild_id: int, channel_id: int, overwrites: list) if target.is_user: perms = Permissions(overwrite["allow"] & ~overwrite["deny"]) + assert target.user_id is not None await _dispatch_action(guild_id, channel_id, target.user_id, perms) elif target.is_role: + assert target.role_id is not None user_ids.extend(await app.storage.get_role_members(target.role_id)) for user_id in user_ids: perms = await get_permissions(user_id, channel_id) + assert target.user_id is not None await _dispatch_action(guild_id, channel_id, target.user_id, perms) @@ -675,7 +679,12 @@ async def ack_channel(channel_id, message_id): async def delete_read_state(channel_id): """Delete the read state of a channel.""" user_id = await token_check() - await channel_check(user_id, channel_id) + try: + await channel_check(user_id, channel_id) + except GuildNotFound: + # ignore when guild isn't found because we're deleting the + # read state regardless. + pass await app.db.execute( """ diff --git a/litecord/blueprints/user/settings.py b/litecord/blueprints/user/settings.py index 0d81f9d..ae05b59 100644 --- a/litecord/blueprints/user/settings.py +++ b/litecord/blueprints/user/settings.py @@ -23,6 +23,7 @@ from litecord.auth import token_check from litecord.schemas import validate, USER_SETTINGS, GUILD_SETTINGS from litecord.blueprints.checks import guild_check from litecord.pubsub.user import dispatch_user +from litecord.errors import UserNotFound bp = Blueprint("users_settings", __name__) @@ -129,6 +130,26 @@ async def patch_guild_settings(guild_id: int): return jsonify(settings) +@bp.route("/@me/notes/", methods=["GET"]) +async def get_note(target_id: int): + """Get a single note from a user.""" + user_id = await token_check() + note = await app.db.fetchval( + """ + SELECT note + FROM notes + WHERE user_id = $1 AND target_id = $2 + """, + user_id, + target_id, + ) + + if note is None: + raise UserNotFound() + + return jsonify({"user_id": user_id, "note_user_id": target_id, "note": note}) + + @bp.route("/@me/notes/", methods=["PUT"]) async def put_note(target_id: int): """Put a note to a user. diff --git a/litecord/blueprints/voice.py b/litecord/blueprints/voice.py index 7011211..dd3cad5 100644 --- a/litecord/blueprints/voice.py +++ b/litecord/blueprints/voice.py @@ -104,9 +104,7 @@ async def majority_region(user_id: int) -> Optional[str]: return _majority_region_count(regions) -@bp.route("/regions", methods=["GET"]) -async def voice_regions(): - """Return voice regions.""" +async def _all_regions(): user_id = await token_check() best_region = await majority_region(user_id) @@ -116,3 +114,16 @@ async def voice_regions(): region["optimal"] = region["id"] == best_region return jsonify(regions) + + +@bp.route("/regions", methods=["GET"]) +async def voice_regions(): + """Return voice regions.""" + return await _all_regions() + + +@bp.route("/guilds//regions", methods=["GET"]) +async def guild_voice_regions(): + """Return voice regions.""" + # we return the same list as the normal /regions route on purpose. + return await _all_regions() diff --git a/litecord/errors.py b/litecord/errors.py index 0820f51..bef8c04 100644 --- a/litecord/errors.py +++ b/litecord/errors.py @@ -142,6 +142,10 @@ class WebhookNotFound(NotFound): error_code = 10015 +class UserNotFound(NotFound): + error_code = 10013 + + class Ratelimited(LitecordError): status_code = 429 diff --git a/litecord/permissions.py b/litecord/permissions.py index 6a77582..654e2be 100644 --- a/litecord/permissions.py +++ b/litecord/permissions.py @@ -18,7 +18,7 @@ along with this program. If not, see . """ import ctypes -from typing import Optional +from typing import Optional, Union from quart import current_app as app @@ -77,8 +77,10 @@ class Permissions(ctypes.Union): _fields_ = [("bits", _RawPermsBits), ("binary", ctypes.c_uint64)] - def __init__(self, val: int): - self.binary = val + def __init__(self, val: Union[str, int]): + # always coerce to int, even when the user gives us a str, because + # python ints are infinity-sized (yes, yes, the memory concerns, yes) + self.binary = int(val) def __repr__(self): return f"" @@ -87,7 +89,7 @@ class Permissions(ctypes.Union): return self.binary -ALL_PERMISSIONS = Permissions(0b01111111111101111111110111111111) +ALL_PERMISSIONS = Permissions(0b01111111111111111111111111111111) EMPTY_PERMISSIONS = Permissions(0) diff --git a/litecord/storage.py b/litecord/storage.py index c2790b1..819e1f1 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -469,6 +469,10 @@ class Storage: def _overwrite_convert(row): drow = dict(row) + drow["allow_new"] = str(drow["allow"]) + drow["deny_new"] = str(drow["deny"]) + drow["allow"] = drow["allow"] & ((2 << 31) - 1) + drow["deny"] = drow["deny"] & ((2 << 31) - 1) target_type = drow["target_type"] drow["type"] = "member" if target_type == 0 else "role" @@ -673,7 +677,12 @@ class Storage: if not row: return None - return dict(row) + drow = dict(row) + + drow["permissions_new"] = str(drow["permissions"]) + drow["permissions"] = drow["permissions"] & ((2 << 31) - 1) + + return drow async def get_role_data(self, guild_id: int) -> List[Dict[str, Any]]: """Get role list information on a guild.""" diff --git a/manage/cmd/migration/scripts/10_permissions.sql b/manage/cmd/migration/scripts/10_permissions.sql new file mode 100644 index 0000000..61b8063 --- /dev/null +++ b/manage/cmd/migration/scripts/10_permissions.sql @@ -0,0 +1,3 @@ +-- channel_overwrites table already has allow and deny as bigints. +alter table roles + alter column permissions type bigint;