async def guild_bindings(self, member): """Return all bound messages for guild_id.""" async with connection().transaction(): await self.wiki_db.check_permissions(member, Permissions.view) async for row in connection().cursor(self.queries.guild_bindings(), member.guild.id): yield AttrDict(row)
async def delete_page(self, member, title) -> bool: """delete a page or alias return whether an alias was deleted """ async with connection().transaction(): # we use resolve_page here for separate permissions check depending on type is_alias = (await self.resolve_page(member, title)).alias if is_alias: # why Permissions.edit and not Permissions.delete? # deleting an alias is a prerequisite to recreating it with a different title # and deleting an alias is nowhere near as destructive as deleting a page await self.check_permissions(member, Permissions.edit) command_tag = await connection().execute( self.queries.delete_alias(), member.guild.id, title) if command_tag.split()[-1] == '0': raise RuntimeError( 'page is supposed to be an alias but delete_alias did not delete it', title) return True await self.check_permissions(member, Permissions.delete, title) command_tag = await connection().execute( self.queries.delete_page(), member.guild.id, title) if command_tag.split()[-1] == '0': raise RuntimeError( 'page is not supposed to be an alias but delete_page did not delete it', title) return False raise errors.PageNotFoundError(title)
async def revise_page(self, member, title, new_content) -> typing.Optional[str]: self.check_title(title) self.check_content(new_content) async with connection().transaction(isolation='serializable'): await self.check_permissions(member, Permissions.edit, title) page = await connection().fetchrow(self.queries.get_page_basic(), member.guild.id, title) if page is None: raise errors.PageNotFoundError(title) content_id = await connection().fetchval( self.queries.create_content(), new_content) await connection().execute( self.queries.create_revision(), page['page_id'], member.id, page['original_title'], content_id, ) if page['alias']: return page['original_title']
async def unmark(self, user_id, points): indices = list(map(bingo.index, points)) mask = functools.reduce(operator.or_, (1 << i for i in indices)) async with connection().transaction(isolation='serializable'): params = list(zip(itertools.repeat(user_id), indices)) await connection().executemany(self.queries.delete_board_mark(), params) await connection().execute(self.queries.delete_board_marks_by_mask(), user_id, mask)
async def unwatch(self, ctx, *, title: clean_content): """Removes a page from your watch list.""" async with connection().transaction(): await self.wiki_db.get_page(ctx.author, title, partial=True, check_permissions=False) await self.db.unwatch_page(ctx.author, title) await ctx.message.add_reaction(self.bot.config['success_emojis'][True])
async def unbind(self, member, message: discord.Message): """Unbind a message. Return whether the message was successfully unbound.""" async with connection().transaction(): page = await self.get_bound_page(message) await self.wiki_db.check_permissions(member, Permissions.manage_bindings, page.title) tag = await connection().execute(self.queries.unbind(), message.id) return tag == 'DELETE 1'
async def watch_page(self, member, title) -> bool: """subscribe the given user to the given page. return success, ie True if they were not a subscriber before. """ async with connection().transaction(): title = (await self.wiki_db.resolve_page(member, title)).target tag = await connection().execute(self.queries.watch_page(), member.guild.id, member.id, title) if tag.rsplit(None, 1)[-1] == '0': raise errors.PageNotFoundError(title)
async def mark(self, user_id, marks): async with connection().transaction(isolation='repeatable_read'): marks = list(marks) params = ( (user_id, bingo.index(point), emote.nsfw, emote.name, emote.id, emote.animated) for point, emote in marks) await connection().executemany(self.queries.set_board_mark(), params) indices = map(compose(bingo.index, operator.itemgetter(0)), marks) mask = functools.reduce(operator.or_, (1 << i for i in indices)) await connection().execute(self.queries.add_board_marks_by_mask(), user_id, mask)
async def mark(self, context, *, args: MultiConverter[str.upper, DatabaseOrLoggedEmote]): """Adds one or more marks to your board.""" if not args: raise commands.BadArgument( _('You must specify at least one position and emote name.')) async with connection().transaction(isolation='repeatable_read'): await self.db.mark(context.author.id, args) board = await self.db.get_board(context.author.id) message = _('You win! Your new bingo board:') if board.has_won( ) else _('Your new bingo board:') await self.send_board(context, message, board)
async def new_board(self, user_id): async with connection().transaction(isolation='repeatable_read'): await connection().execute(self.queries.delete_board(), user_id) await connection().execute(self.queries.set_board_value(), user_id, DEFAULT_BOARD_VALUE) rows = await connection().fetch(self.queries.get_categories(), bingo.BingoBoard.SQUARES) to_insert = [ (user_id, pos + 1 if pos >= bingo.BingoBoard.FREE_SPACE_I else pos, category_id) # skip free space for pos, (category_id, category_text) in enumerate(rows)] await connection().copy_records_to_table( 'bingo_board_categories', records=to_insert, columns=('user_id', 'pos', 'category_id')) return bingo.EmoteCollectorBingoBoard(categories=[category_text for __, category_text in rows])
async def get_page_overwrites_for( self, guild_id, entity_id: int, title) -> typing.Tuple[Permissions, Permissions]: async with connection().transaction(): page_id = await connection().fetchval(self.queries.get_page_id(), guild_id, title) if page_id is None: raise errors.PageNotFoundError(title) row = await connection().fetchrow( self.queries.get_page_overwrites_for(), page_id, entity_id) if row is None: return (Permissions.none, Permissions.none) return tuple(map(Permissions, row))
async def get_page_overwrites( self, guild_id, title ) -> typing.Mapping[int, typing.Tuple[Permissions, Permissions]]: """get the allowed and denied permissions for a particular page""" async with connection().transaction(): page_id = await connection().fetchval(self.queries.get_page_id(), guild_id, title) if page_id is None: raise errors.PageNotFoundError(title) return { entity: (Permissions(allow), Permissions(deny)) for entity, allow, deny in await connection().fetch( self.queries.get_page_overwrites(), page_id) }
async def alias_page(self, member, alias_title, target_title): self.check_title(alias_title) async with connection().transaction(): await self.check_permissions(member, Permissions.create) await self.check_permissions(member, Permissions.view, target_title) await self.ensure_title_available(member, alias_title) try: await connection().execute(self.queries.alias_page(), member.guild.id, alias_title, target_title) except asyncpg.NotNullViolationError: # the CTE returned no rows raise errors.PageNotFoundError(target_title) except asyncpg.UniqueViolationError: raise errors.PageExistsError
async def bind(self, member, message: discord.Message, title, *, check_permissions=True): async with connection().transaction(): page = await self.wiki_db.get_page(member, title, check_permissions=False) if check_permissions: await self.wiki_db.check_permissions( member, Permissions.manage_bindings, title) await connection().execute(self.queries.bind(), message.channel.id, message.id, page.page_id) binding = page binding.channel_id = message.channel.id binding.message_id = message.id return binding
async def resolve_page(self, member, title): # XXX if a user is denied permissions for a page, that applies to its aliases too. # So if a user is denied view permissions for a single page, and they request info on an alias to that page, # the fact that they were denied permission to view that alias would leak information about what # page it is an alias to. Consider allowing anyone to resolve an alias, or only denying those who # were globally denied view permissions. async with connection().transaction(): await self.check_permissions(member, Permissions.view, title) row = await connection().fetchrow(self.queries.get_alias(), member.guild.id, title) if row is not None: return AttrDict(row) row = await connection().fetchrow(self.queries.get_page_no_alias(), member.guild.id, title) if row is not None: return AttrDict(row) raise errors.PageNotFoundError(title)
async def rename_page(self, member, title, new_title): self.check_title(new_title) async with connection().transaction(isolation='serializable'): await self.ensure_title_available(member, new_title) try: page_id = await connection().fetchval( self.queries.rename_page(), member.guild.id, title, new_title) except asyncpg.UniqueViolationError: raise errors.PageExistsError if page_id is None: raise errors.PageNotFoundError(title) content_id = await connection().fetchval( self.queries.get_content_id(), page_id) await connection().execute(self.queries.log_page_rename(), page_id, member.id, content_id, new_title)
async def create_page(self, member, title, content): self.check_title(title) self.check_content(content) async with connection().transaction(isolation='serializable'): await self.check_permissions(member, Permissions.create) if await connection().fetchrow(self.queries.get_alias(), member.guild.id, title): raise errors.PageExistsError try: page_id = await connection().fetchval( self.queries.create_page(), member.guild.id, title) except asyncpg.UniqueViolationError: raise errors.PageExistsError content_id = await connection().fetchval( self.queries.create_content(), content) await connection().execute(self.queries.create_first_revision(), page_id, member.id, content_id, title)
async def on_cm_page_edit(self, revision_id): async with connection().transaction(): old, new = await self.get_revision_and_previous(revision_id) guild = self.bot.get_guild(new.guild_id) if guild is None: logger.warning( f'on_cm_page_edit: guild_id {new.guild_id} not found!') return async def send(user_id): # editing a page you subscribe to should not notify yourself if user_id == new.author_id: return try: recipient = await utils.fetch_member(guild, user_id) except discord.NotFound: return try: await self.wiki_db.check_permissions( recipient, Permissions.view, new.current_title) except errors.MissingPagePermissionsError: return with contextlib.suppress(discord.NotFound): new.author = await utils.fetch_member(guild, new.author_id) with contextlib.suppress(discord.NotFound): old.author = await utils.fetch_member(guild, old.author_id) await recipient.send( embed=self.page_edit_notification(recipient, old, new)) await asyncio.gather( *map(send, await self.page_subscribers(new.page_id)))
async def _bound_messages(self, page_id): async with connection().transaction(): async for row in connection().cursor(self.queries.bound_messages(), page_id): yield AttrDict(row)
async def bound_messages(self, member, title): async with connection().transaction(): page = await self.wiki_db.get_page(member, title, partial=True) async for row in self._bound_messages(page.page_id): yield row
async def latest_message_per_channel(self, cutoff: int): async with connection().transaction(): async for row in connection().cursor( self.queries.latest_message_per_channel(), cutoff): yield row
async def watch_list(self, member): async with connection().transaction(): async for page_id, title in connection().cursor( self.queries.watch_list(), member.guild.id, member.id): yield page_id, title
async def cursor(self, query, *args): """return an async iterator over all rows matched by query and args. Lazy equivalent to fetch()""" async with connection().transaction(): async for row in connection().cursor(query, *args): yield AttrDict(row)