async def _get_bundled_playlist_tracks(self): async with aiohttp.ClientSession(json_serialize=json.dumps) as session: async with session.get( CURRATED_DATA + f"?timestamp={int(time.time())}", headers={"content-type": "application/json"}, ) as response: if response.status != 200: return 0, [] try: data = json.loads(await response.read()) except Exception: log.exception( "Curated playlist couldn't be parsed, report this error." ) data = {} web_version = data.get("version", 0) entries = data.get("entries", []) if entries: random.shuffle(entries) tracks = [] async for entry in AsyncIter(entries, steps=25): with contextlib.suppress(Exception): tracks.append(self.decode_track(entry)) return web_version, tracks
async def command_queue_search(self, ctx: commands.Context, *, search_words: str): """Search the queue.""" try: player = lavalink.get_player(ctx.guild.id) except KeyError: return await self.send_embed_msg( ctx, title=_("There's nothing in the queue.")) if not self._player_check(ctx) or not player.queue: return await self.send_embed_msg( ctx, title=_("There's nothing in the queue.")) search_list = await self._build_queue_search_list( player.queue, search_words) if not search_list: return await self.send_embed_msg(ctx, title=_("No matches.")) len_search_pages = math.ceil(len(search_list) / 10) search_page_list = [] async for page_num in AsyncIter(range(1, len_search_pages + 1)): embed = await self._build_queue_search_page( ctx, page_num, search_list) search_page_list.append(embed) await menu(ctx, search_page_list, DEFAULT_CONTROLS)
async def guildChannelsDenyList(self, ctx: Context): """List the channels in the denylist.""" dlChannels = await self.config.guild(ctx.guild ).get_attr(KEY_CHANNEL_DENYLIST)() channelMentions: List[str] = [] if dlChannels: channelMentions = [ channelObject.mention for channelObject in list( map( lambda chId: discord.utils.get(ctx.guild.text_channels, id=chId), dlChannels, )) if channelObject ] if channelMentions: pageList = [] msg = "\n".join(channelMentions) pages = list(chat_formatting.pagify(msg, page_length=300)) totalPages = len(pages) totalEntries = len(dlChannels) async for pageNumber, page in AsyncIter(pages).enumerate(start=1): embed = discord.Embed( title=f"Denylist channels for **{ctx.guild.name}**", description=page) embed.set_footer( text= f"Page {pageNumber}/{totalPages} ({totalEntries} entries)") embed.colour = discord.Colour.red() pageList.append(embed) await menu(ctx, pageList, DEFAULT_CONTROLS) else: await ctx.send( f"There are no channels on the denylist for **{ctx.guild.name}**!" )
async def get_localtrack_folder_tracks( self, ctx, player: lavalink.player_manager.Player, query: Query) -> List[lavalink.rest_api.Track]: """Return a list of tracks per the provided query.""" if not await self.localtracks_folder_exists( ctx) or self.api_interface is None: return [] audio_data = LocalPath(None, self.local_folder_current_path) try: if query.local_track_path is not None: query.local_track_path.path.relative_to(audio_data.to_string()) else: return [] except ValueError: return [] local_tracks = [] async for local_file in AsyncIter( await self.get_all_localtrack_folder_tracks(ctx, query)): with contextlib.suppress(IndexError, TrackEnqueueError): trackdata, called_api = await self.api_interface.fetch_track( ctx, player, local_file) local_tracks.append(trackdata.tracks[0]) return local_tracks
async def post_time_to_game_start(self, bot, time_left): """ Post when there is 60, 30, and 10 minutes until the game starts in all channels """ post_state = ["all", self.home_team, self.away_team] msg = _("{time} minutes until {away_emoji} {away} @ {home_emoji} {home} starts!").format( time=time_left, away_emoji=self.away_emoji, away=self.away_team, home_emoji=self.home_emoji, home=self.home_team, ) tasks = [] all_channels = await bot.get_cog("Hockey").config.all_channels() async for channel_id, data in AsyncIter(all_channels.items(), steps=100): channel = await get_channel_obj(bot, channel_id, data) if not channel: continue should_post = await check_to_post(bot, channel, data, post_state, self.game_state) team_to_post = await bot.get_cog("Hockey").config.channel(channel).team() if should_post and "all" not in team_to_post: tasks.append(self.post_game_start(channel, msg)) await bounded_gather(*tasks)
async def post_game_state(self, bot): """ When a game state has changed this is called to create the embed and post in all channels """ post_state = ["all", self.home_team, self.away_team] state_embed = await self.game_state_embed() state_text = await self.game_state_text() tasks = [] all_channels = await bot.get_cog("Hockey").config.all_channels() async for channel_id, data in AsyncIter(all_channels.items(), steps=100): channel = await get_channel_obj(bot, channel_id, data) if not channel: continue should_post = await check_to_post(bot, channel, data, post_state, self.game_state) if should_post: tasks.append(self.actually_post_state(bot, channel, state_embed, state_text)) previews = await bounded_gather(*tasks) for preview in previews: if preview is None: continue else: bot.dispatch("hockey_preview_message", preview[0], preview[1], self)
async def fetch_all_converter( self, scope: str, playlist_name, playlist_id ) -> List[PlaylistFetchResult]: """Fetch all playlists with the specified filter""" scope_type = self.get_scope_type(scope) try: playlist_id = int(playlist_id) except Exception as exc: debug_exc_log(log, exc, "Failed converting playlist_id to int") playlist_id = -1 output = [] with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: for future in concurrent.futures.as_completed( [ executor.submit( self.database.cursor().execute, self.statement.get_all_converter, ( { "scope_type": scope_type, "playlist_name": playlist_name, "playlist_id": playlist_id, } ), ) ] ): try: row_result = future.result() except Exception as exc: debug_exc_log(log, exc, "Failed to completed fetch from database") async for row in AsyncIter(row_result): output.append(PlaylistFetchResult(*row)) return output
async def get_leaderboard(cog, guild) -> List[tuple]: raw_accounts = await cog.config.all_users() if guild is not None: tmp = raw_accounts.copy() for acc in tmp: if not guild.get_member(acc): del raw_accounts[acc] a = [] async for (k, v) in AsyncIter(raw_accounts.items(), steps=100): lsum, rsu, usum, csum, gsum = ( sum(v["legendary"].values()), sum(v["rare"].values()), sum(v["uncommon"].values()), sum(v["common"].values()), sum(v["garbage"].values()), ) _sum = lsum + rsu + usum + csum + gsum a.append((k, {"fishes": _sum, "legendaries": lsum})) sorted_acc = sorted( a, key=lambda x: x[1]["fishes"], reverse=True, ) return sorted_acc
async def fetch_playlist_tracks( self, ctx: commands.Context, player: lavalink.player_manager.Player, query: Query, skip_cache: bool = False, ) -> Union[discord.Message, None, List[MutableMapping]]: search = query.is_search tracklist = [] if query.is_spotify: try: if self.play_lock[ctx.message.guild.id]: return await self.send_embed_msg( ctx, title=_("Unable To Get Tracks"), description=_( "Wait until the playlist has finished loading."), ) except KeyError: pass tracks = await self._get_spotify_tracks(ctx, query, forced=skip_cache) if isinstance(tracks, discord.Message): return None if not tracks: embed = discord.Embed(title=_("Nothing found.")) if query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT: embed = discord.Embed(title=_("Track is not playable.")) embed.description = _( "**{suffix}** is not a fully supported format and some " "tracks may not play.").format(suffix=query.suffix) return await self.send_embed_msg(ctx, embed=embed) async for track in AsyncIter(tracks): track_obj = self.get_track_json(player, other_track=track) tracklist.append(track_obj) self.update_player_lock(ctx, False) elif query.is_search: try: result, called_api = await self.api_interface.fetch_track( ctx, player, query, forced=skip_cache) except TrackEnqueueError: self.update_player_lock(ctx, False) return await self.send_embed_msg( ctx, title=_("Unable to Get Track"), description=_( "I'm unable to get a track from Lavalink at the moment, try again in a few " "minutes."), ) except Exception as e: self.update_player_lock(ctx, False) raise e tracks = result.tracks if not tracks: embed = discord.Embed(title=_("Nothing found.")) if query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT: embed = discord.Embed(title=_("Track is not playable.")) embed.description = _( "**{suffix}** is not a fully supported format and some " "tracks may not play.").format(suffix=query.suffix) return await self.send_embed_msg(ctx, embed=embed) else: try: result, called_api = await self.api_interface.fetch_track( ctx, player, query, forced=skip_cache) except TrackEnqueueError: self.update_player_lock(ctx, False) return await self.send_embed_msg( ctx, title=_("Unable to Get Track"), description=_( "I'm unable to get a track from Lavalink at the moment, try again in a few " "minutes."), ) except Exception as e: self.update_player_lock(ctx, False) raise e tracks = result.tracks if not search and len(tracklist) == 0: async for track in AsyncIter(tracks): track_obj = self.get_track_json(player, other_track=track) tracklist.append(track_obj) elif len(tracklist) == 0: track_obj = self.get_track_json(player, other_track=tracks[0]) tracklist.append(track_obj) return tracklist
async def _load_v3_playlist( self, ctx: commands.Context, scope: str, uploaded_playlist_name: str, uploaded_playlist_url: str, track_list: List, author: Union[discord.User, discord.Member], guild: Union[discord.Guild], ) -> None: embed1 = discord.Embed(title=_("Please wait, adding tracks...")) playlist_msg = await self.send_embed_msg(ctx, embed=embed1) track_count = len(track_list) uploaded_track_count = len(track_list) await asyncio.sleep(1) embed2 = discord.Embed( colour=await ctx.embed_colour(), title=_("Loading track {num}/{total}...").format( num=track_count, total=uploaded_track_count), ) await playlist_msg.edit(embed=embed2) playlist = await create_playlist( ctx, self.playlist_api, scope, uploaded_playlist_name, uploaded_playlist_url, track_list, author, guild, ) scope_name = self.humanize_scope( scope, ctx=guild if scope == PlaylistScope.GUILD.value else author) if not track_count: msg = _("Empty playlist {name} (`{id}`) [**{scope}**] created." ).format(name=playlist.name, id=playlist.id, scope=scope_name) elif uploaded_track_count != track_count: bad_tracks = uploaded_track_count - track_count msg = _( "Added {num} tracks from the {playlist_name} playlist. {num_bad} track(s) " "could not be loaded.").format(num=track_count, playlist_name=playlist.name, num_bad=bad_tracks) else: msg = _("Added {num} tracks from the {playlist_name} playlist." ).format(num=track_count, playlist_name=playlist.name) embed3 = discord.Embed(colour=await ctx.embed_colour(), title=_("Playlist Saved"), description=msg) await playlist_msg.edit(embed=embed3) database_entries = [] time_now = int( datetime.datetime.now(datetime.timezone.utc).timestamp()) async for t in AsyncIter(track_list): uri = t.get("info", {}).get("uri") if uri: t = {"loadType": "V2_COMPAT", "tracks": [t], "query": uri} data = json.dumps(t) if all(k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]): database_entries.append({ "query": uri, "data": data, "last_updated": time_now, "last_fetched": time_now, }) if database_entries: await self.api_interface.local_cache_api.lavalink.insert( database_entries)
async def player_automated_timer(self) -> None: stop_times: Dict = {} pause_times: Dict = {} while True: async for p in AsyncIter(lavalink.all_players()): server = p.channel.guild if await self.bot.cog_disabled_in_guild(self, server): continue if [self.bot.user] == p.channel.members: stop_times.setdefault(server.id, time.time()) pause_times.setdefault(server.id, time.time()) else: stop_times.pop(server.id, None) if p.paused and server.id in pause_times: try: await p.pause(False) except Exception as err: debug_exc_log( log, err, f"Exception raised in Audio's unpausing player for {server.id}.", ) pause_times.pop(server.id, None) servers = stop_times.copy() servers.update(pause_times) async for sid in AsyncIter(servers, steps=5): server_obj = self.bot.get_guild(sid) if sid in stop_times and await self.config.guild( server_obj).emptydc_enabled(): emptydc_timer = await self.config.guild(server_obj ).emptydc_timer() if (time.time() - stop_times[sid]) >= emptydc_timer: stop_times.pop(sid) try: player = lavalink.get_player(sid) await self.api_interface.persistent_queue_api.drop( sid) await player.stop() await player.disconnect() except Exception as err: if "No such player for that guild" in str(err): stop_times.pop(sid, None) debug_exc_log( log, err, f"Exception raised in Audio's emptydc_timer for {sid}." ) elif (sid in pause_times and await self.config.guild(server_obj).emptypause_enabled()): emptypause_timer = await self.config.guild( server_obj).emptypause_timer() if (time.time() - pause_times.get(sid, 0)) >= emptypause_timer: try: await lavalink.get_player(sid).pause() except Exception as err: if "No such player for that guild" in str(err): pause_times.pop(sid, None) debug_exc_log( log, err, f"Exception raised in Audio's pausing for {sid}." ) await asyncio.sleep(5)
async def post_automatic_standings(bot): """ Automatically update a standings embed with the latest stats run when new games for the day is updated """ log.debug("Updating Standings.") config = bot.get_cog("Hockey").config async with bot.get_cog("Hockey").session.get( BASE_URL + "/api/v1/standings") as resp: standings_data = await resp.json() all_guilds = await config.all_guilds() async for guild_id, data in AsyncIter(all_guilds.items(), steps=100): guild = bot.get_guild(guild_id) if guild is None: continue log.debug(guild.name) if data["post_standings"]: search = data["standings_type"] if search is None: continue standings_channel = data["standings_channel"] if standings_channel is None: continue channel = guild.get_channel(standings_channel) if channel is None: continue standings_msg = data["standings_msg"] if standings_msg is None: continue try: if version_info >= VersionInfo.from_str("3.4.6"): message = channel.get_partial_message(standings_msg) else: message = await channel.fetch_message(standings_msg) except (discord.errors.NotFound, discord.errors.Forbidden): await config.guild(guild).post_standings.clear() await config.guild(guild).standings_type.clear() await config.guild(guild).standings_channel.clear() await config.guild(guild).standings_msg.clear() continue standings, page = await Standings.get_team_standings_from_data( search, standings_data) team_stats = standings[page] if search in DIVISIONS: em = await Standings.make_division_standings_embed( team_stats) elif search in CONFERENCES: em = await Standings.make_conference_standings_embed( team_stats) else: em = await Standings.all_standing_embed(standings) if message is not None: try: await message.edit(embed=em) except (discord.errors.NotFound, discord.errors.Forbidden): await config.guild(guild).post_standings.clear() await config.guild(guild).standings_type.clear() await config.guild(guild).standings_channel.clear() await config.guild(guild).standings_msg.clear() except Exception: log.exception( f"Error editing standings message in {guild.id}")
async def data_schema_migration(self, from_version: int, to_version: int) -> None: database_entries = [] time_now = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) if from_version == to_version: return if from_version < 2 <= to_version: all_guild_data = await self.config.all_guilds() all_playlist = {} async for guild_id, guild_data in AsyncIter(all_guild_data.items()): temp_guild_playlist = guild_data.pop("playlists", None) if temp_guild_playlist: guild_playlist = {} async for count, (name, data) in AsyncIter( temp_guild_playlist.items() ).enumerate(start=1000): if not data or not name: continue playlist = {"id": count, "name": name, "guild": int(guild_id)} playlist.update(data) guild_playlist[str(count)] = playlist tracks_in_playlist = data.get("tracks", []) or [] async for t in AsyncIter(tracks_in_playlist): uri = t.get("info", {}).get("uri") if uri: t = {"loadType": "V2_COMPAT", "tracks": [t], "query": uri} data = json.dumps(t) if all( k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"] ): database_entries.append( { "query": uri, "data": data, "last_updated": time_now, "last_fetched": time_now, } ) if guild_playlist: all_playlist[str(guild_id)] = guild_playlist await self.config.custom(PlaylistScope.GUILD.value).set(all_playlist) # new schema is now in place await self.config.schema_version.set(2) # migration done, now let's delete all the old stuff async for guild_id in AsyncIter(all_guild_data): await self.config.guild( cast(discord.Guild, discord.Object(id=guild_id)) ).clear_raw("playlists") if from_version < 3 <= to_version: for scope in PlaylistScope.list(): scope_playlist = await get_all_playlist_for_migration23( self.bot, self.playlist_api, self.config, scope ) async for p in AsyncIter(scope_playlist): await p.save() await self.config.custom(scope).clear() await self.config.schema_version.set(3) if database_entries: await self.api_interface.local_cache_api.lavalink.insert(database_entries)
async def upcoming(self, ctx: commands.Context, days: int = 7): """View upcoming birthdays. **Examples:** - `[p]birthday upcoming` - default of 7 days - `[p]birthday upcoming 14` - 14 days """ # guild only check in group if TYPE_CHECKING: assert isinstance(ctx.author, discord.Member) assert ctx.guild is not None if days < 1 or days > 365: await ctx.send( "You must enter a number of days greater than 0 and smaller than 365." ) return today_dt = datetime.datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) all_birthdays: dict[int, dict[str, dict]] = await self.config.all_members( ctx.guild) parsed_bdays: dict[int, list[str]] = defaultdict(list) number_day_mapping: dict[int, str] = {} async for member_id, member_data in AsyncIter(all_birthdays.items(), steps=50): member = ctx.guild.get_member(member_id) if not isinstance(member, discord.Member): continue birthday_dt = datetime.datetime( year=member_data["birthday"]["year"] or 1, month=member_data["birthday"]["month"], day=member_data["birthday"]["day"], ) if today_dt.month == birthday_dt.month and today_dt.day == birthday_dt.day: parsed_bdays[0].append(member.mention + ( "" if birthday_dt.year == 1 else f" turns {today_dt.year - birthday_dt.year}")) number_day_mapping[0] = "Today" continue this_year_bday = birthday_dt.replace(year=today_dt.year) next_year_bday = birthday_dt.replace(year=today_dt.year + 1) next_birthday_dt = this_year_bday if this_year_bday > today_dt else next_year_bday diff = next_birthday_dt - today_dt if diff.days > days: continue parsed_bdays[diff.days].append(member.mention + ( "" if birthday_dt.year == 1 else f" will turn {today_dt.year - birthday_dt.year}")) number_day_mapping[diff.days] = next_birthday_dt.strftime("%B %d") if len(parsed_bdays) == 0: await ctx.send("No upcoming birthdays.") return sorted_parsed_bdays = sorted(parsed_bdays.items(), key=lambda x: x[0]) embed = discord.Embed(title="Upcoming Birthdays", colour=await ctx.embed_colour()) if len(parsed_bdays) > 25: embed.description = "Too many days to display. I've had to stop at 25." for day, members in sorted_parsed_bdays: embed.add_field(name=number_day_mapping.get(day), value="\n".join(members)) await ctx.send(embed=embed)
async def command_queue(self, ctx: commands.Context, *, page: int = 1): """List the songs in the queue.""" # Check to avoid an IndexError further down in the code. if page < 1: page = 1 async def _queue_menu( ctx: commands.Context, pages: list, controls: MutableMapping, message: discord.Message, page: int, timeout: float, emoji: str, ): if message: await ctx.send_help(self.command_queue) with contextlib.suppress(discord.HTTPException): await message.delete() return None queue_controls = { "\N{LEFTWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}": prev_page, "\N{CROSS MARK}": close_menu, "\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16}": next_page, "\N{INFORMATION SOURCE}\N{VARIATION SELECTOR-16}": _queue_menu, } if not self._player_check(ctx): return await self.send_embed_msg( ctx, title=_("There's nothing in the queue.")) player = lavalink.get_player(ctx.guild.id) if player.current and not player.queue: arrow = await self.draw_time(ctx) pos = self.format_time(player.position) if player.current.is_stream: dur = "LIVE" else: dur = self.format_time(player.current.length) song = (await self.get_track_description( player.current, self.local_folder_current_path) or "") song += _("\n Requested by: **{track.requester}**").format( track=player.current) song += f"\n\n{arrow}`{pos}`/`{dur}`" embed = discord.Embed(title=_("Now Playing"), description=song) guild_data = await self.config.guild(ctx.guild).all() if guild_data[ "thumbnail"] and player.current and player.current.thumbnail: embed.set_thumbnail(url=player.current.thumbnail) shuffle = guild_data["shuffle"] repeat = guild_data["repeat"] autoplay = guild_data["auto_play"] text = "" text += (_("Auto-Play") + ": " + ("\N{WHITE HEAVY CHECK MARK}" if autoplay else "\N{CROSS MARK}")) text += ((" | " if text else "") + _("Shuffle") + ": " + ("\N{WHITE HEAVY CHECK MARK}" if shuffle else "\N{CROSS MARK}")) text += ( (" | " if text else "") + _("Repeat") + ": " + ("\N{WHITE HEAVY CHECK MARK}" if repeat else "\N{CROSS MARK}")) embed.set_footer(text=text) message = await self.send_embed_msg(ctx, embed=embed) dj_enabled = self._dj_status_cache.setdefault( ctx.guild.id, guild_data["dj_enabled"]) vote_enabled = guild_data["vote_enabled"] if ((dj_enabled or vote_enabled) and not await self._can_instaskip(ctx, ctx.author) and not await self.is_requester_alone(ctx)): return emoji = { "prev": "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", "stop": "\N{BLACK SQUARE FOR STOP}\N{VARIATION SELECTOR-16}", "pause": "\N{BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR}\N{VARIATION SELECTOR-16}", "next": "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}\N{VARIATION SELECTOR-16}", "close": "\N{CROSS MARK}", } expected = tuple(emoji.values()) if not player.queue and not autoplay: expected = (emoji["stop"], emoji["pause"], emoji["close"]) if player.current: task: Optional[asyncio.Task] = start_adding_reactions( message, expected[:5]) else: task: Optional[asyncio.Task] = None try: (r, u) = await self.bot.wait_for( "reaction_add", check=ReactionPredicate.with_emojis( expected, message, ctx.author), timeout=30.0, ) except asyncio.TimeoutError: return await self._clear_react(message, emoji) else: if task is not None: task.cancel() reacts = {v: k for k, v in emoji.items()} react = reacts[r.emoji] if react == "prev": await self._clear_react(message, emoji) await ctx.invoke(self.command_prev) elif react == "stop": await self._clear_react(message, emoji) await ctx.invoke(self.command_stop) elif react == "pause": await self._clear_react(message, emoji) await ctx.invoke(self.command_pause) elif react == "next": await self._clear_react(message, emoji) await ctx.invoke(self.command_skip) elif react == "close": await message.delete() return elif not player.current and not player.queue: return await self.send_embed_msg( ctx, title=_("There's nothing in the queue.")) async with ctx.typing(): limited_queue = player.queue[: 500] # TODO: Improve when Toby menu's are merged len_queue_pages = math.ceil(len(limited_queue) / 10) queue_page_list = [] async for page_num in AsyncIter(range(1, len_queue_pages + 1)): embed = await self._build_queue_page(ctx, limited_queue, player, page_num) queue_page_list.append(embed) if page > len_queue_pages: page = len_queue_pages return await menu(ctx, queue_page_list, queue_controls, page=(page - 1))
async def command_remove(self, ctx: commands.Context, index_or_url: Union[int, str]): """Remove a specific track number from the queue.""" dj_enabled = self._dj_status_cache.setdefault( ctx.guild.id, await self.config.guild(ctx.guild).dj_enabled()) if not self._player_check(ctx): return await self.send_embed_msg(ctx, title=_("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) can_skip = await self._can_instaskip(ctx, ctx.author) if not player.queue: return await self.send_embed_msg(ctx, title=_("Nothing queued.")) if dj_enabled and not can_skip: return await self.send_embed_msg( ctx, title=_("Unable To Modify Queue"), description=_("You need the DJ role to remove tracks."), ) if (not ctx.author.voice or ctx.author.voice.channel != player.channel) and not can_skip: return await self.send_embed_msg( ctx, title=_("Unable To Modify Queue"), description=_( "You must be in the voice channel to manage the queue."), ) player.store("notify_channel", ctx.channel.id) if isinstance(index_or_url, int): if index_or_url > len(player.queue) or index_or_url < 1: return await self.send_embed_msg( ctx, title=_("Unable To Modify Queue"), description= _("Song number must be greater than 1 and within the queue limit." ), ) index_or_url -= 1 removed = player.queue.pop(index_or_url) await self.api_interface.persistent_queue_api.played( ctx.guild.id, removed.extras.get("enqueue_time")) removed_title = await self.get_track_description( removed, self.local_folder_current_path) await self.send_embed_msg( ctx, title=_("Removed track from queue"), description=_("Removed {track} from the queue.").format( track=removed_title), ) else: clean_tracks = [] removed_tracks = 0 async for track in AsyncIter(player.queue): if track.uri != index_or_url: clean_tracks.append(track) else: await self.api_interface.persistent_queue_api.played( ctx.guild.id, track.extras.get("enqueue_time")) removed_tracks += 1 player.queue = clean_tracks if removed_tracks == 0: await self.send_embed_msg( ctx, title=_("Unable To Modify Queue"), description=_( "Removed 0 tracks, nothing matches the URL provided."), ) else: await self.send_embed_msg( ctx, title=_("Removed track from queue"), description=_("Removed {removed_tracks} tracks from queue " "which matched the URL provided.").format( removed_tracks=removed_tracks), )
async def loot(self, ctx: commands.Context, box_type: str = None, number: int = 1): """This opens one of your precious treasure chests. Use the box rarity type with the command: normal, rare, epic, legendary, ascended or set. """ if (not is_dev(ctx.author) and number > 100) or number < 1: return await smart_embed(ctx, _("Nice try :smirk:.")) if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to open a loot chest but then realised you left them all back at the inn." ), ) if not await self.allow_in_dm(ctx): return await smart_embed( ctx, _("This command is not available in DM's on this bot.")) msgs = [] async with self.get_lock(ctx.author): try: c = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return if not box_type: return await ctx.send( box( _("{author} owns {normal} normal, " "{rare} rare, {epic} epic, {leg} legendary, {asc} ascended and {set} set chests." ).format( author=escape(ctx.author.display_name), normal=str(c.treasure[0]), rare=str(c.treasure[1]), epic=str(c.treasure[2]), leg=str(c.treasure[3]), asc=str(c.treasure[4]), set=str(c.treasure[5]), ), lang="css", )) if c.is_backpack_full(is_dev=is_dev(ctx.author)): await ctx.send( _("**{author}**, your backpack is currently full.").format( author=escape(ctx.author.display_name))) return if box_type == "normal": redux = 0 elif box_type == "rare": redux = 1 elif box_type == "epic": redux = 2 elif box_type == "legendary": redux = 3 elif box_type == "ascended": redux = 4 elif box_type == "set": redux = 5 else: return await smart_embed( ctx, _("There is talk of a {} treasure chest but nobody ever saw one." ).format(box_type), ) treasure = c.treasure[redux] if treasure < 1 or treasure < number: await smart_embed( ctx, _("**{author}**, you do not have enough {box} treasure chests to open." ).format(author=escape(ctx.author.display_name), box=box_type), ) else: if number > 1: async with ctx.typing(): # atomically save reduced loot count then lock again when saving inside # open chests c.treasure[redux] -= number await self.config.user(ctx.author).set(await c.to_json( ctx, self.config)) items = await self._open_chests(ctx, box_type, number, character=c) msg = _("{}, you've opened the following items:\n\n" ).format(escape(ctx.author.display_name)) msg_len = len(msg) table = BeautifulTable(default_alignment=ALIGN_LEFT, maxwidth=500) table.set_style(BeautifulTable.STYLE_RST) msgs = [] total = len(items.values()) table.columns.header = [ "Name", "Slot", "ATT", "CHA", "INT", "DEX", "LUC", "LVL", "QTY", "DEG", "SET", ] async for index, item in AsyncIter( items.values(), steps=100).enumerate(start=1): if len(str(table)) > 1500: table.rows.sort("LVL", reverse=True) msgs.append( box(msg + str(table) + f"\nPage {len(msgs) + 1}", lang="css")) table = BeautifulTable( default_alignment=ALIGN_LEFT, maxwidth=500) table.set_style(BeautifulTable.STYLE_RST) table.columns.header = [ "Name", "Slot", "ATT", "CHA", "INT", "DEX", "LUC", "LVL", "QTY", "DEG", "SET", ] table.rows.append(( str(item), item.slot[0] if len(item.slot) == 1 else "two handed", item.att, item.cha, item.int, item.dex, item.luck, f"[{r}]" if (r := c.equip_level(item)) is not None and r > c.lvl else f"{r}", item.owned, f"[{item.degrade}]" if item.rarity in ["legendary", "event", "ascended"] and item.degrade >= 0 else "N/A", item.set or "N/A", )) if index == total: table.rows.sort("LVL", reverse=True) msgs.append( box(msg + str(table) + f"\nPage {len(msgs) + 1}", lang="css")) else: # atomically save reduced loot count then lock again when saving inside # open chests c.treasure[redux] -= 1
async def commands_cbackpack_sell(self, ctx: commands.Context, *, query: BackpackFilterParser): """Sell items from your backpack. Forged items cannot be sold using this command. Please read the usage instructions [here](https://github.com/aikaterna/gobcog/blob/master/docs/cbackpack.md) """ if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to go sell your items but the monster ahead is not allowing you to leave." ), ) query.pop("degrade", None) # Disallow selling by degrade levels async with self.get_lock(ctx.author): try: character = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return slots = await character.get_argparse_backpack_items( query, rarity_exclude=["forged"]) if (total_items := sum(len(i) for s, i in slots)) > 2: msg = await ctx.send( "Are you sure you want to sell {count} items in your inventory that match this query?" .format(count=humanize_number(total_items))) start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS) pred = ReactionPredicate.yes_or_no(msg, ctx.author) try: await ctx.bot.wait_for("reaction_add", check=pred, timeout=60) except asyncio.TimeoutError: await self._clear_react(msg) return if not pred.result: await ctx.send("Not selling those items.") return total_price = 0 msg = "" async with ctx.typing(): async for slot_name, slot_group in AsyncIter(slots, steps=100): async for item_name, item in AsyncIter(slot_group, steps=100): old_owned = item.owned item_price = 0 async for _loop_counter in AsyncIter(range( 0, old_owned), steps=100): item.owned -= 1 item_price += _sell(character, item) if item.owned <= 0 and item.name in character.backpack: del character.backpack[item.name] item_price = max(item_price, 0) msg += _("{old_item} sold for {price}.\n").format( old_item=str(old_owned) + " " + str(item), price=humanize_number(item_price), ) total_price += item_price if total_price > 0: try: await bank.deposit_credits(ctx.author, total_price) except BalanceTooHigh as e: await bank.set_balance(ctx.author, e.max_balance) character.last_known_currency = await bank.get_balance( ctx.author) character.last_currency_check = time.time() await self.config.user(ctx.author).set(await character.to_json( ctx, self.config)) if total_price == 0: return await smart_embed( ctx, _("No items matched your query.").format(), ) if msg: msg_list = [] new_msg = _( "{author} sold {number} items and their duplicates for {price}.\n\n{items}" ).format( author=escape(ctx.author.display_name), number=humanize_number(total_items), price=humanize_number(total_price), items=msg, ) for page in pagify(new_msg, shorten_by=10, page_length=1900): msg_list.append(box(page, lang="css")) await BaseMenu( source=SimpleSource(msg_list), delete_message_after=True, clear_reactions_after=True, timeout=60, ).start(ctx=ctx)
async def _enqueue_tracks( self, ctx: commands.Context, query: Union[Query, list], enqueue: bool = True ) -> Union[discord.Message, List[lavalink.Track], lavalink.Track]: player = lavalink.get_player(ctx.guild.id) try: if self.play_lock[ctx.message.guild.id]: return await self.send_embed_msg( ctx, title=_("Unable To Get Tracks"), description=_("Wait until the playlist has finished loading."), ) except KeyError: self.update_player_lock(ctx, True) guild_data = await self.config.guild(ctx.guild).all() first_track_only = False single_track = None index = None playlist_data = None playlist_url = None seek = 0 if type(query) is not list: if not await self.is_query_allowed(self.config, ctx, f"{query}", query_obj=query): raise QueryUnauthorized( _("{query} is not an allowed query.").format(query=query.to_string_user()) ) if query.single_track: first_track_only = True index = query.track_index if query.start_time: seek = query.start_time try: result, called_api = await self.api_interface.fetch_track(ctx, player, query) except TrackEnqueueError: self.update_player_lock(ctx, False) return await self.send_embed_msg( ctx, title=_("Unable to Get Track"), description=_( "I'm unable to get a track from Lavalink at the moment, " "try again in a few minutes." ), ) except Exception as e: self.update_player_lock(ctx, False) raise e tracks = result.tracks playlist_data = result.playlist_info if not enqueue: return tracks if not tracks: self.update_player_lock(ctx, False) title = _("Nothing found.") embed = discord.Embed(title=title) if result.exception_message: if "Status Code" in result.exception_message: embed.set_footer(text=result.exception_message[:2000]) else: embed.set_footer(text=result.exception_message[:2000].replace("\n", "")) if await self.config.use_external_lavalink() and query.is_local: embed.description = _( "Local tracks will not work " "if the `Lavalink.jar` cannot see the track.\n" "This may be due to permissions or because Lavalink.jar is being run " "in a different machine than the local tracks." ) elif query.is_local and query.suffix in _PARTIALLY_SUPPORTED_MUSIC_EXT: title = _("Track is not playable.") embed = discord.Embed(title=title) embed.description = _( "**{suffix}** is not a fully supported format and some " "tracks may not play." ).format(suffix=query.suffix) return await self.send_embed_msg(ctx, embed=embed) else: tracks = query queue_dur = await self.queue_duration(ctx) queue_total_duration = self.format_time(queue_dur) before_queue_length = len(player.queue) if not first_track_only and len(tracks) > 1: # a list of Tracks where all should be enqueued # this is a Spotify playlist already made into a list of Tracks or a # url where Lavalink handles providing all Track objects to use, like a # YouTube or Soundcloud playlist if len(player.queue) >= 10000: return await self.send_embed_msg(ctx, title=_("Queue size limit reached.")) track_len = 0 empty_queue = not player.queue async for track in AsyncIter(tracks): if len(player.queue) >= 10000: continue query = Query.process_input(track, self.local_folder_current_path) if not await self.is_query_allowed( self.config, ctx, f"{track.title} {track.author} {track.uri} " f"{str(query)}", query_obj=query, ): if IS_DEBUG: log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})") continue elif guild_data["maxlength"] > 0: if self.is_track_length_allowed(track, guild_data["maxlength"]): track_len += 1 track.extras.update( { "enqueue_time": int(time.time()), "vc": player.channel.id, "requester": ctx.author.id, } ) player.add(ctx.author, track) self.bot.dispatch( "red_audio_track_enqueue", player.channel.guild, track, ctx.author ) else: track_len += 1 track.extras.update( { "enqueue_time": int(time.time()), "vc": player.channel.id, "requester": ctx.author.id, } ) player.add(ctx.author, track) self.bot.dispatch( "red_audio_track_enqueue", player.channel.guild, track, ctx.author ) player.maybe_shuffle(0 if empty_queue else 1) if len(tracks) > track_len: maxlength_msg = _(" {bad_tracks} tracks cannot be queued.").format( bad_tracks=(len(tracks) - track_len) ) else: maxlength_msg = "" playlist_name = escape( playlist_data.name if playlist_data else _("No Title"), formatting=True ) embed = discord.Embed( description=bold(f"[{playlist_name}]({playlist_url})") if playlist_url else playlist_name, title=_("Playlist Enqueued"), ) embed.set_footer( text=_("Added {num} tracks to the queue.{maxlength_msg}").format( num=track_len, maxlength_msg=maxlength_msg ) ) if not guild_data["shuffle"] and queue_dur > 0: embed.set_footer( text=_( "{time} until start of playlist playback: starts at #{position} in queue" ).format(time=queue_total_duration, position=before_queue_length + 1) ) if not player.current: await player.play() self.update_player_lock(ctx, False) message = await self.send_embed_msg(ctx, embed=embed) return tracks or message else: single_track = None # a ytsearch: prefixed item where we only need the first Track returned # this is in the case of [p]play <query>, a single Spotify url/code # or this is a localtrack item try: if len(player.queue) >= 10000: return await self.send_embed_msg(ctx, title=_("Queue size limit reached.")) single_track = ( tracks if isinstance(tracks, lavalink.rest_api.Track) else tracks[index] if index else tracks[0] ) if seek and seek > 0: single_track.start_timestamp = seek * 1000 query = Query.process_input(single_track, self.local_folder_current_path) if not await self.is_query_allowed( self.config, ctx, ( f"{single_track.title} {single_track.author} {single_track.uri} " f"{str(query)}" ), query_obj=query, ): if IS_DEBUG: log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})") self.update_player_lock(ctx, False) return await self.send_embed_msg( ctx, title=_("This track is not allowed in this server.") ) elif guild_data["maxlength"] > 0: if self.is_track_length_allowed(single_track, guild_data["maxlength"]): single_track.extras.update( { "enqueue_time": int(time.time()), "vc": player.channel.id, "requester": ctx.author.id, } ) player.add(ctx.author, single_track) player.maybe_shuffle() self.bot.dispatch( "red_audio_track_enqueue", player.channel.guild, single_track, ctx.author, ) else: self.update_player_lock(ctx, False) return await self.send_embed_msg( ctx, title=_("Track exceeds maximum length.") ) else: single_track.extras.update( { "enqueue_time": int(time.time()), "vc": player.channel.id, "requester": ctx.author.id, } ) player.add(ctx.author, single_track) player.maybe_shuffle() self.bot.dispatch( "red_audio_track_enqueue", player.channel.guild, single_track, ctx.author ) except IndexError: self.update_player_lock(ctx, False) title = _("Nothing found") desc = EmptyEmbed if await self.bot.is_owner(ctx.author): desc = _("Please check your console or logs for details.") return await self.send_embed_msg(ctx, title=title, description=desc) except Exception as e: self.update_player_lock(ctx, False) raise e description = await self.get_track_description( single_track, self.local_folder_current_path ) embed = discord.Embed(title=_("Track Enqueued"), description=description) if not guild_data["shuffle"] and queue_dur > 0: embed.set_footer( text=_("{time} until track playback: #{position} in queue").format( time=queue_total_duration, position=before_queue_length + 1 ) ) if not player.current: await player.play() self.update_player_lock(ctx, False) message = await self.send_embed_msg(ctx, embed=embed) return single_track or message
async def backpack_disassemble(self, ctx: commands.Context, *, backpack_items: ItemsConverter): """ Disassemble items from your backpack. This will provide a chance for a chest, or the item might break while you are handling it... """ if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to disassemble an item but the monster ahead of you commands your attention." ), ) async with self.get_lock(ctx.author): if len(backpack_items[1]) > 2: msg = await ctx.send( "Are you sure you want to disassemble {count} unique items and their duplicates?" .format(count=humanize_number(len(backpack_items[1])))) start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS) pred = ReactionPredicate.yes_or_no(msg, ctx.author) try: await ctx.bot.wait_for("reaction_add", check=pred, timeout=60) except asyncio.TimeoutError: await self._clear_react(msg) return if not pred.result: await ctx.send("Not disassembling those items.") return try: character = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return failed = 0 success = 0 op = backpack_items[0] disassembled = set() async for item in AsyncIter(backpack_items[1], steps=100): try: item = character.backpack[item.name] except KeyError: continue if item.name in disassembled: continue if item.rarity in ["forged"]: continue index = min(RARITIES.index(item.rarity), 4) if op == "single": if character.heroclass["name"] != "Tinkerer": roll = random.randint(0, 5) chests = 1 else: roll = random.randint(0, 3) chests = random.randint(1, 2) if roll != 0: item.owned -= 1 if item.owned <= 0: del character.backpack[item.name] await self.config.user(ctx.author).set( await character.to_json(ctx, self.config)) return await smart_embed( ctx, _("Your attempt at disassembling `{}` failed and it has been destroyed." ).format(item.name), ) else: item.owned -= 1 if item.owned <= 0: del character.backpack[item.name] character.treasure[index] += chests await self.config.user(ctx.author).set( await character.to_json(ctx, self.config)) return await smart_embed( ctx, _("Your attempt at disassembling `{}` was successful and you have received {} {}." ).format( item.name, chests, _("chests") if chests > 1 else _("chest")), ) elif op == "all": disassembled.add(item.name) owned = item.owned async for _loop_counter in AsyncIter(range(0, owned), steps=100): if character.heroclass["name"] != "Tinkerer": roll = random.randint(0, 5) chests = 1 else: roll = random.randint(0, 3) chests = random.randint(1, 2) if roll != 0: item.owned -= 1 if item.owned <= 0 and item.name in character.backpack: del character.backpack[item.name] failed += 1 else: item.owned -= 1 if item.owned <= 0 and item.name in character.backpack: del character.backpack[item.name] character.treasure[index] += chests success += 1 await self.config.user(ctx.author ).set(await character.to_json(ctx, self.config)) return await smart_embed( ctx, _("You attempted to disassemble multiple items: {succ} were successful and {fail} failed." ).format(succ=humanize_number(success), fail=humanize_number(failed)), )
async def initialize(self): async for guild_id, guild_data in AsyncIter( (await self.config.all_guilds()).items(), steps=100 ): if guild_data["channel"]: self.channel_cache[guild_id] = guild_data["channel"]
async def backpack_sellall( self, ctx: commands.Context, rarity: Optional[RarityConverter] = None, *, slot: Optional[SlotConverter] = None, ): """Sell all items in your backpack. Optionally specify rarity or slot.""" assert isinstance(rarity, str) or rarity is None assert isinstance(slot, str) or slot is None if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to go sell your items but the monster ahead is not allowing you to leave." ), ) if rarity: rarity = rarity.lower() if rarity not in RARITIES: return await smart_embed( ctx, _("{} is not a valid rarity, select one of {}").format( rarity, humanize_list(RARITIES)), ) if rarity.lower() in ["forged"]: return await smart_embed( ctx, _("You cannot sell `{rarity}` rarity items.").format( rarity=rarity)) if slot: slot = slot.lower() if slot not in ORDER: return await smart_embed( ctx, _("{} is not a valid slot, select one of {}").format( slot, humanize_list(ORDER)), ) async with self.get_lock(ctx.author): if rarity and slot: msg = await ctx.send( "Are you sure you want to sell all {rarity} {slot} items in your inventory?" .format(rarity=rarity, slot=slot)) elif rarity or slot: msg = await ctx.send( "Are you sure you want to sell all{rarity}{slot} items in your inventory?" .format(rarity=f" {rarity}" if rarity else "", slot=f" {slot}" if slot else "")) else: msg = await ctx.send( "Are you sure you want to sell **ALL ITEMS** in your inventory?" ) start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS) pred = ReactionPredicate.yes_or_no(msg, ctx.author) try: await ctx.bot.wait_for("reaction_add", check=pred, timeout=60) except asyncio.TimeoutError: await self._clear_react(msg) return if not pred.result: await ctx.send("Not selling those items.") return msg = "" try: c = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return total_price = 0 async with ctx.typing(): items = [ i for n, i in c.backpack.items() if i.rarity not in ["forged"] ] count = 0 async for item in AsyncIter(items, steps=100): if rarity and item.rarity != rarity: continue if slot: if len(item.slot) == 1 and slot != item.slot[0]: continue elif len(item.slot) == 2 and slot != "two handed": continue item_price = 0 old_owned = item.owned async for _loop_counter in AsyncIter(range(0, old_owned), steps=100): item.owned -= 1 item_price += _sell(c, item) if item.owned <= 0: del c.backpack[item.name] item_price = max(item_price, 0) msg += _("{old_item} sold for {price}.\n").format( old_item=str(old_owned) + " " + str(item), price=humanize_number(item_price), ) total_price += item_price if total_price > 0: try: await bank.deposit_credits(ctx.author, total_price) except BalanceTooHigh as e: await bank.set_balance(ctx.author, e.max_balance) c.last_known_currency = await bank.get_balance(ctx.author) c.last_currency_check = time.time() await self.config.user(ctx.author ).set(await c.to_json(ctx, self.config)) msg_list = [] new_msg = _( "{author} sold all their{rarity} items for {price}.\n\n{items}" ).format( author=escape(ctx.author.display_name), rarity=f" {rarity}" if rarity else "", price=humanize_number(total_price), items=msg, ) for page in pagify(new_msg, shorten_by=10, page_length=1900): msg_list.append(box(page, lang="css")) await BaseMenu( source=SimpleSource(msg_list), delete_message_after=True, clear_reactions_after=True, timeout=60, ).start(ctx=ctx)
async def cache_guild(self, guild_id: int, guild_data: dict): async for tag_name, tag_data in AsyncIter(guild_data["tags"].items(), steps=50): tag = Tag.from_dict(self, tag_name, tag_data, guild_id=guild_id) tag.add_to_cache()
class BackPackCommands(AdventureMixin): """This class will handle interacting with adventures backpack""" @commands.group(name="backpack", autohelp=False) @commands.bot_has_permissions(add_reactions=True) async def _backpack( self, ctx: commands.Context, show_diff: Optional[bool] = False, rarity: Optional[RarityConverter] = None, *, slot: Optional[SlotConverter] = None, ): """This shows the contents of your backpack. Give it a rarity and/or slot to filter what backpack items to show. Selling: `[p]backpack sell item_name` Trading: `[p]backpack trade @user price item_name` Equip: `[p]backpack equip item_name` Sell All: `[p]backpack sellall rarity slot` Disassemble: `[p]backpack disassemble item_name` Note: An item **degrade** level is how many rebirths it will last, before it is broken down. """ assert isinstance(rarity, str) or rarity is None assert isinstance(slot, str) or slot is None if not await self.allow_in_dm(ctx): return await smart_embed( ctx, _("This command is not available in DM's on this bot.")) if not ctx.invoked_subcommand: try: c = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return if rarity: rarity = rarity.lower() if rarity not in RARITIES: return await smart_embed( ctx, _("{} is not a valid rarity, select one of {}").format( rarity, humanize_list(RARITIES)), ) if slot: slot = slot.lower() if slot not in ORDER: return await smart_embed( ctx, _("{} is not a valid slot, select one of {}").format( slot, humanize_list(ORDER)), ) msgs = await c.get_backpack(rarity=rarity, slot=slot, show_delta=show_diff) if not msgs: return await smart_embed( ctx, _("You have no items in your backpack."), ) await BackpackMenu( source=SimpleSource(msgs), help_command=self._backpack, delete_message_after=True, clear_reactions_after=True, timeout=60, ).start(ctx=ctx) @_backpack.command(name="equip") async def backpack_equip(self, ctx: commands.Context, *, equip_item: EquipableItemConverter): """Equip an item from your backpack.""" assert isinstance(equip_item, Item) if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to equip an item but the monster ahead of you commands your attention." ), ) async with self.get_lock(ctx.author): try: c = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return equiplevel = c.equip_level(equip_item) if is_dev(ctx.author): # FIXME: equiplevel = 0 if not c.can_equip(equip_item): return await smart_embed( ctx, _("You need to be level `{level}` to equip this item."). format(level=equiplevel), ) equip = c.backpack.get(equip_item.name) if equip: slot = equip.slot[0] if len(equip.slot) > 1: slot = "two handed" if not getattr(c, equip.slot[0]): equip_msg = box( _("{author} equipped {item} ({slot} slot).").format( author=escape(ctx.author.display_name), item=str(equip), slot=slot), lang="css", ) else: equip_msg = box( _("{author} equipped {item} ({slot} slot) and put {put} into their backpack." ).format( author=escape(ctx.author.display_name), item=str(equip), slot=slot, put=getattr(c, equip.slot[0]), ), lang="css", ) await ctx.send(equip_msg) c = await c.equip_item(equip, True, is_dev(ctx.author)) # FIXME: await self.config.user(ctx.author ).set(await c.to_json(ctx, self.config)) @_backpack.command(name="eset", cooldown_after_parsing=True) @commands.cooldown(rate=1, per=600, type=commands.BucketType.user) async def backpack_eset(self, ctx: commands.Context, *, set_name: str): """Equip all parts of a set that you own.""" if self.in_adventure(ctx): ctx.command.reset_cooldown(ctx) return await smart_embed( ctx, _("You tried to magically equip multiple items at once, but the monster ahead nearly killed you." ), ) set_list = humanize_list( sorted([f"`{i}`" for i in self.SET_BONUSES.keys()], key=str.lower)) if set_name is None: ctx.command.reset_cooldown(ctx) return await smart_embed( ctx, _("Use this command with one of the following set names: \n{sets}" ).format(sets=set_list), ) async with self.get_lock(ctx.author): try: character = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) ctx.command.reset_cooldown(ctx) return pieces = await character.get_set_count(return_items=True, set_name=set_name.title()) if not pieces: ctx.command.reset_cooldown(ctx) return await smart_embed( ctx, _("You have no pieces of `{set_name}` that you can equip." ).format(set_name=set_name), ) for piece in pieces: character = await character.equip_item(piece, from_backpack=True) await self.config.user(ctx.author ).set(await character.to_json(ctx, self.config)) await smart_embed( ctx, _("I've equipped all pieces of `{set_name}` that you are able to equip." ).format(set_name=set_name), ) @_backpack.command(name="disassemble") async def backpack_disassemble(self, ctx: commands.Context, *, backpack_items: ItemsConverter): """ Disassemble items from your backpack. This will provide a chance for a chest, or the item might break while you are handling it... """ if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to disassemble an item but the monster ahead of you commands your attention." ), ) async with self.get_lock(ctx.author): if len(backpack_items[1]) > 2: msg = await ctx.send( "Are you sure you want to disassemble {count} unique items and their duplicates?" .format(count=humanize_number(len(backpack_items[1])))) start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS) pred = ReactionPredicate.yes_or_no(msg, ctx.author) try: await ctx.bot.wait_for("reaction_add", check=pred, timeout=60) except asyncio.TimeoutError: await self._clear_react(msg) return if not pred.result: await ctx.send("Not disassembling those items.") return try: character = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return failed = 0 success = 0 op = backpack_items[0] disassembled = set() async for item in AsyncIter(backpack_items[1], steps=100): try: item = character.backpack[item.name] except KeyError: continue if item.name in disassembled: continue if item.rarity in ["forged"]: continue index = min(RARITIES.index(item.rarity), 4) if op == "single": if character.heroclass["name"] != "Tinkerer": roll = random.randint(0, 5) chests = 1 else: roll = random.randint(0, 3) chests = random.randint(1, 2) if roll != 0: item.owned -= 1 if item.owned <= 0: del character.backpack[item.name] await self.config.user(ctx.author).set( await character.to_json(ctx, self.config)) return await smart_embed( ctx, _("Your attempt at disassembling `{}` failed and it has been destroyed." ).format(item.name), ) else: item.owned -= 1 if item.owned <= 0: del character.backpack[item.name] character.treasure[index] += chests await self.config.user(ctx.author).set( await character.to_json(ctx, self.config)) return await smart_embed( ctx, _("Your attempt at disassembling `{}` was successful and you have received {} {}." ).format( item.name, chests, _("chests") if chests > 1 else _("chest")), ) elif op == "all": disassembled.add(item.name) owned = item.owned async for _loop_counter in AsyncIter(range(0, owned), steps=100): if character.heroclass["name"] != "Tinkerer": roll = random.randint(0, 5) chests = 1 else: roll = random.randint(0, 3) chests = random.randint(1, 2) if roll != 0: item.owned -= 1 if item.owned <= 0 and item.name in character.backpack: del character.backpack[item.name] failed += 1 else: item.owned -= 1 if item.owned <= 0 and item.name in character.backpack: del character.backpack[item.name] character.treasure[index] += chests success += 1 await self.config.user(ctx.author ).set(await character.to_json(ctx, self.config)) return await smart_embed( ctx, _("You attempted to disassemble multiple items: {succ} were successful and {fail} failed." ).format(succ=humanize_number(success), fail=humanize_number(failed)), ) @_backpack.command(name="sellall") async def backpack_sellall( self, ctx: commands.Context, rarity: Optional[RarityConverter] = None, *, slot: Optional[SlotConverter] = None, ): """Sell all items in your backpack. Optionally specify rarity or slot.""" assert isinstance(rarity, str) or rarity is None assert isinstance(slot, str) or slot is None if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to go sell your items but the monster ahead is not allowing you to leave." ), ) if rarity: rarity = rarity.lower() if rarity not in RARITIES: return await smart_embed( ctx, _("{} is not a valid rarity, select one of {}").format( rarity, humanize_list(RARITIES)), ) if rarity.lower() in ["forged"]: return await smart_embed( ctx, _("You cannot sell `{rarity}` rarity items.").format( rarity=rarity)) if slot: slot = slot.lower() if slot not in ORDER: return await smart_embed( ctx, _("{} is not a valid slot, select one of {}").format( slot, humanize_list(ORDER)), ) async with self.get_lock(ctx.author): if rarity and slot: msg = await ctx.send( "Are you sure you want to sell all {rarity} {slot} items in your inventory?" .format(rarity=rarity, slot=slot)) elif rarity or slot: msg = await ctx.send( "Are you sure you want to sell all{rarity}{slot} items in your inventory?" .format(rarity=f" {rarity}" if rarity else "", slot=f" {slot}" if slot else "")) else: msg = await ctx.send( "Are you sure you want to sell **ALL ITEMS** in your inventory?" ) start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS) pred = ReactionPredicate.yes_or_no(msg, ctx.author) try: await ctx.bot.wait_for("reaction_add", check=pred, timeout=60) except asyncio.TimeoutError: await self._clear_react(msg) return if not pred.result: await ctx.send("Not selling those items.") return msg = "" try: c = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return total_price = 0 async with ctx.typing(): items = [ i for n, i in c.backpack.items() if i.rarity not in ["forged"] ] count = 0 async for item in AsyncIter(items, steps=100): if rarity and item.rarity != rarity: continue if slot: if len(item.slot) == 1 and slot != item.slot[0]: continue elif len(item.slot) == 2 and slot != "two handed": continue item_price = 0 old_owned = item.owned async for _loop_counter in AsyncIter(range(0, old_owned), steps=100): item.owned -= 1 item_price += _sell(c, item) if item.owned <= 0: del c.backpack[item.name] item_price = max(item_price, 0) msg += _("{old_item} sold for {price}.\n").format( old_item=str(old_owned) + " " + str(item), price=humanize_number(item_price), ) total_price += item_price if total_price > 0: try: await bank.deposit_credits(ctx.author, total_price) except BalanceTooHigh as e: await bank.set_balance(ctx.author, e.max_balance) c.last_known_currency = await bank.get_balance(ctx.author) c.last_currency_check = time.time() await self.config.user(ctx.author ).set(await c.to_json(ctx, self.config)) msg_list = [] new_msg = _( "{author} sold all their{rarity} items for {price}.\n\n{items}" ).format( author=escape(ctx.author.display_name), rarity=f" {rarity}" if rarity else "", price=humanize_number(total_price), items=msg, ) for page in pagify(new_msg, shorten_by=10, page_length=1900): msg_list.append(box(page, lang="css")) await BaseMenu( source=SimpleSource(msg_list), delete_message_after=True, clear_reactions_after=True, timeout=60, ).start(ctx=ctx) @_backpack.command(name="sell", cooldown_after_parsing=True) @commands.cooldown(rate=3, per=60, type=commands.BucketType.user) async def backpack_sell(self, ctx: commands.Context, *, item: ItemConverter): """Sell an item from your backpack.""" if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to go sell your items but the monster ahead is not allowing you to leave." ), ) if item.rarity in ["forged"]: ctx.command.reset_cooldown(ctx) return await ctx.send( box( _("\n{author}, your {device} is refusing to be sold and bit your finger for trying." ).format(author=escape(ctx.author.display_name), device=str(item)), lang="css", )) async with self.get_lock(ctx.author): try: c = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: ctx.command.reset_cooldown(ctx) log.exception("Error with the new character sheet", exc_info=exc) return price_shown = _sell(c, item) messages = [ _("**{author}**, do you want to sell this item for {price} each? {item}" ).format( author=escape(ctx.author.display_name), item=box(str(item), lang="css"), price=humanize_number(price_shown), ) ] try: item = c.backpack[item.name] except KeyError: return async def _backpack_sell_menu( ctx: commands.Context, pages: list, controls: dict, message: discord.Message, page: int, timeout: float, emoji: str, ): if message: with contextlib.suppress(discord.HTTPException): await message.delete() await self._backpack_sell_button_action( ctx, emoji, page, item, price_shown, c) return None back_pack_sell_controls = { "\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": _backpack_sell_menu, "\N{CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS}": _backpack_sell_menu, "\N{CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY}": _backpack_sell_menu, "\N{CROSS MARK}": _backpack_sell_menu, } await menu(ctx, messages, back_pack_sell_controls, timeout=60) async def _backpack_sell_button_action(self, ctx, emoji, page, item, price_shown, character): currency_name = await bank.get_currency_name(ctx.guild, ) msg = "" if emoji == "\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": # user reacted with one to sell. ctx.command.reset_cooldown(ctx) # sell one of the item price = 0 item.owned -= 1 price += price_shown msg += _( "**{author}** sold one {item} for {price} {currency_name}.\n" ).format( author=escape(ctx.author.display_name), item=box(item, lang="css"), price=humanize_number(price), currency_name=currency_name, ) if item.owned <= 0: del character.backpack[item.name] price = max(price, 0) if price > 0: try: await bank.deposit_credits(ctx.author, price) except BalanceTooHigh as e: await bank.set_balance(ctx.author, e.max_balance) elif emoji == "\N{CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS}": # user wants to sell all owned. ctx.command.reset_cooldown(ctx) price = 0 old_owned = item.owned count = 0 async for _loop_counter in AsyncIter(range(0, item.owned), steps=50): item.owned -= 1 price += price_shown if item.owned <= 0: del character.backpack[item.name] count += 1 msg += _( "**{author}** sold all their {old_item} for {price} {currency_name}.\n" ).format( author=escape(ctx.author.display_name), old_item=box(str(item) + " - " + str(old_owned), lang="css"), price=humanize_number(price), currency_name=currency_name, ) price = max(price, 0) if price > 0: try: await bank.deposit_credits(ctx.author, price) except BalanceTooHigh as e: await bank.set_balance(ctx.author, e.max_balance) elif (emoji == "\N{CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY}" ): # user wants to sell all but one. if item.owned == 1: ctx.command.reset_cooldown(ctx) return await smart_embed( ctx, _("You already only own one of those items.")) price = 0 old_owned = item.owned count = 0 async for _loop_counter in AsyncIter(range(1, item.owned), steps=50): item.owned -= 1 price += price_shown count += 1 if price != 0: msg += _( "**{author}** sold all but one of their {old_item} for {price} {currency_name}.\n" ).format( author=escape(ctx.author.display_name), old_item=box(str(item) + " - " + str(old_owned - 1), lang="css"), price=humanize_number(price), currency_name=currency_name, ) price = max(price, 0) if price > 0: try: await bank.deposit_credits(ctx.author, price) except BalanceTooHigh as e: await bank.set_balance(ctx.author, e.max_balance) else: # user doesn't want to sell those items. await ctx.send(_("Not selling those items.")) if msg: character.last_known_currency = await bank.get_balance(ctx.author) character.last_currency_check = time.time() await self.config.user(ctx.author ).set(await character.to_json(ctx, self.config)) pages = [ page for page in pagify(msg, delims=["\n"], page_length=1900) ] await BaseMenu( source=SimpleSource(pages), delete_message_after=True, clear_reactions_after=True, timeout=60, ).start(ctx=ctx) @_backpack.command(name="trade") async def backpack_trade( self, ctx: commands.Context, buyer: discord.Member, asking: Optional[int] = 1000, *, item: ItemConverter, ): """Trade an item from your backpack to another user.""" if ctx.author == buyer: return await smart_embed( ctx, _("You take the item and pass it from one hand to the other. Congratulations, you traded yourself." ), ) if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to trade an item to a party member but the monster ahead commands your attention." ), ) if self.in_adventure(user=buyer): return await smart_embed( ctx, _("**{buyer}** is currently in an adventure... you were unable to reach them via pigeon." ).format(buyer=escape(buyer.display_name)), ) if asking < 0: return await ctx.send(_("You can't *sell* for less than 0...")) try: c = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return try: buy_user = await Character.from_json(ctx, self.config, buyer, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return if buy_user.is_backpack_full(is_dev=is_dev(buyer)): await ctx.send( _("**{author}**'s backpack is currently full.").format( author=escape(buyer.display_name))) return if not any([x for x in c.backpack if item.name.lower() == x.lower()]): return await smart_embed( ctx, _("**{author}**, you have to specify an item from your backpack to trade." ).format(author=escape(ctx.author.display_name)), ) lookup = list(x for n, x in c.backpack.items() if str(item) == str(x)) if len(lookup) > 1: await smart_embed( ctx, _("**{author}**, I found multiple items ({items}) " "matching that name in your backpack.\nPlease be more specific." ).format( author=escape(ctx.author.display_name), items=humanize_list([x.name for x in lookup]), ), ) return if any([x for x in lookup if x.rarity == "forged"]): device = [x for x in lookup if x.rarity == "forged"] return await ctx.send( box( _("\n{author}, your {device} does not want to leave you."). format(author=escape(ctx.author.display_name), device=str(device[0])), lang="css", )) elif any([x for x in lookup if x.rarity == "set"]): return await ctx.send( box( _("\n{character}, you cannot trade Set items as they are bound to your soul." ).format(character=escape(ctx.author.display_name)), lang="css", )) else: item = lookup[0] hand = item.slot[0] if len(item.slot) < 2 else "two handed" currency_name = await bank.get_currency_name(ctx.guild, ) if str(currency_name).startswith("<"): currency_name = "credits" trade_talk = box( _("{author} wants to sell {item}. " "(ATT: {att_item} | " "CHA: {cha_item} | " "INT: {int_item} | " "DEX: {dex_item} | " "LUCK: {luck_item}) " "[{hand}])\n{buyer}, " "do you want to buy this item for {asking} {currency_name}?" ).format( author=escape(ctx.author.display_name), item=item, att_item=str(item.att), cha_item=str(item.cha), int_item=str(item.int), dex_item=str(item.dex), luck_item=str(item.luck), hand=hand, buyer=escape(buyer.display_name), asking=str(asking), currency_name=currency_name, ), lang="css", ) async with self.get_lock(ctx.author): trade_msg = await ctx.send(f"{buyer.mention}\n{trade_talk}") start_adding_reactions(trade_msg, ReactionPredicate.YES_OR_NO_EMOJIS) pred = ReactionPredicate.yes_or_no(trade_msg, buyer) try: await ctx.bot.wait_for("reaction_add", check=pred, timeout=60) except asyncio.TimeoutError: await self._clear_react(trade_msg) return if pred.result: # buyer reacted with Yes. with contextlib.suppress(discord.errors.NotFound): if await bank.can_spend(buyer, asking): if buy_user.rebirths + 1 < c.rebirths: return await smart_embed( ctx, _("You can only trade with people that are the same " "rebirth level, one rebirth level less than you, " "or a higher rebirth level than yours."), ) try: await bank.transfer_credits( buyer, ctx.author, asking) except BalanceTooHigh as e: await bank.withdraw_credits(buyer, asking) await bank.set_balance(ctx.author, e.max_balance) c.backpack[item.name].owned -= 1 newly_owned = c.backpack[item.name].owned if c.backpack[item.name].owned <= 0: del c.backpack[item.name] async with self.get_lock(buyer): if item.name in buy_user.backpack: buy_user.backpack[item.name].owned += 1 else: item.owned = 1 buy_user.backpack[item.name] = item await self.config.user(buyer).set( await buy_user.to_json(ctx, self.config)) item.owned = newly_owned await self.config.user(ctx.author).set( await c.to_json(ctx, self.config)) await trade_msg.edit(content=(box( _("\n{author} traded {item} to {buyer} for {asking} {currency_name}." ).format( author=escape(ctx.author.display_name), item=item, buyer=escape(buyer.display_name), asking=asking, currency_name=currency_name, ), lang="css", ))) await self._clear_react(trade_msg) else: await trade_msg.edit(content=_( "**{buyer}**, you do not have enough {currency_name}." ).format( buyer=escape(buyer.display_name), currency_name=currency_name, )) else: with contextlib.suppress(discord.HTTPException): await trade_msg.delete() @commands.command(name="ebackpack") @commands.bot_has_permissions(add_reactions=True) async def commands_equipable_backpack( self, ctx: commands.Context, show_diff: Optional[bool] = False, rarity: Optional[RarityConverter] = None, *, slot: Optional[SlotConverter] = None, ): """This shows the contents of your backpack that can be equipped. Give it a rarity and/or slot to filter what backpack items to show. Note: An item **degrade** level is how many rebirths it will last, before it is broken down. """ assert isinstance(rarity, str) or rarity is None assert isinstance(slot, str) or slot is None if not await self.allow_in_dm(ctx): return await smart_embed( ctx, _("This command is not available in DM's on this bot.")) if not ctx.invoked_subcommand: try: c = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return if rarity: rarity = rarity.lower() if rarity not in RARITIES: return await smart_embed( ctx, _("{} is not a valid rarity, select one of {}").format( rarity, humanize_list(RARITIES)), ) if slot: slot = slot.lower() if slot not in ORDER: return await smart_embed( ctx, _("{} is not a valid slot, select one of {}").format( slot, humanize_list(ORDER)), ) backpack_pages = await c.get_backpack(rarity=rarity, slot=slot, show_delta=show_diff, equippable=True) if backpack_pages: await BackpackMenu( source=SimpleSource(backpack_pages), help_command=self.commands_equipable_backpack, delete_message_after=True, clear_reactions_after=True, timeout=60, ).start(ctx=ctx) else: return await smart_embed( ctx, _("You have no equippable items that match this query."), ) @commands.group(name="cbackpack") @commands.bot_has_permissions(add_reactions=True) async def commands_cbackpack( self, ctx: commands.Context, ): """Complex backpack management tools. Please read the usage instructions [here](https://github.com/aikaterna/gobcog/blob/master/docs/cbackpack.md) """ @commands_cbackpack.command(name="show") async def commands_cbackpack_show( self, ctx: commands.Context, *, query: BackpackFilterParser, ): """This shows the contents of your backpack. Please read the usage instructions [here](https://github.com/aikaterna/gobcog/blob/master/docs/cbackpack.md) """ if not await self.allow_in_dm(ctx): return await smart_embed( ctx, _("This command is not available in DM's on this bot.")) try: c = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return backpack_pages = await c.get_argparse_backpack(query) if backpack_pages: await BackpackMenu( source=SimpleSource(backpack_pages), help_command=self.commands_cbackpack, delete_message_after=True, clear_reactions_after=True, timeout=60, ).start(ctx=ctx) else: return await smart_embed( ctx, _("You have no items that match this query."), ) @commands_cbackpack.command(name="disassemble") async def commands_cbackpack_disassemble(self, ctx: commands.Context, *, query: BackpackFilterParser): """ Disassemble items from your backpack. This will provide a chance for a chest, or the item might break while you are handling it... Please read the usage instructions [here](https://github.com/aikaterna/gobcog/blob/master/docs/cbackpack.md) """ if self.in_adventure(ctx): return await smart_embed( ctx, _("You tried to disassemble an item but the monster ahead of you commands your attention." ), ) query.pop("degrade", None) # Disallow selling by degrade levels async with self.get_lock(ctx.author): try: character = await Character.from_json(ctx, self.config, ctx.author, self._daily_bonus) except Exception as exc: log.exception("Error with the new character sheet", exc_info=exc) return slots = await character.get_argparse_backpack_items( query, rarity_exclude=["forged"]) if (total_items := sum(len(i) for s, i in slots)) > 2: msg = await ctx.send( "Are you sure you want to disassemble {count} unique items and their duplicates?" .format(count=humanize_number(total_items))) start_adding_reactions(msg, ReactionPredicate.YES_OR_NO_EMOJIS) pred = ReactionPredicate.yes_or_no(msg, ctx.author) try: await ctx.bot.wait_for("reaction_add", check=pred, timeout=60) except asyncio.TimeoutError: await self._clear_react(msg) return if not pred.result: await ctx.send("Not disassembling those items.") return failed = 0 success = 0 disassembled = set() async for slot_name, slot_group in AsyncIter(slots, steps=100): async for item_name, item in AsyncIter(slot_group, steps=100): try: item = character.backpack[item.name] except KeyError: continue if item.name in disassembled: continue if item.rarity in ["forged"]: failed += 1 continue index = min(RARITIES.index(item.rarity), 4) disassembled.add(item.name) owned = item.owned async for _loop_counter in AsyncIter(range(0, owned), steps=100): if character.heroclass["name"] != "Tinkerer": roll = random.randint(0, 5) chests = 1 else: roll = random.randint(0, 3) chests = random.randint(1, 2) if roll != 0: item.owned -= 1 if item.owned <= 0 and item.name in character.backpack: del character.backpack[item.name] failed += 1 else: item.owned -= 1 if item.owned <= 0 and item.name in character.backpack: del character.backpack[item.name] character.treasure[index] += chests success += 1 if (not failed) and (not success): return await smart_embed( ctx, _("No items matched your query.").format(), ) else: await self.config.user(ctx.author ).set(await character.to_json(ctx, self.config)) return await smart_embed( ctx, _("You attempted to disassemble multiple items: {succ} were successful and {fail} failed." ).format(succ=humanize_number(success), fail=humanize_number(failed)), )
async def repboard(self, ctx, page_list: int = 1): """Show the reputation leaderboard. Give a number to show a page. """ users = [] title = "Global Rep Leaderboard for {}\n".format(self.bot.user.name) all_users = await self.data.all_users() if str(all_users) == "{}": await ctx.send( "The leaderboard is empty... Nobody's popular, for now.") return for user_id in all_users: user_name = await self._get_user(user_id) users.append((user_name, all_users[user_id]["points"])) if ctx.author.id == user_id: user_stat = all_users[user_id]["points"] board_type = "Rep" icon_url = self.bot.user.avatar_url sorted_list = sorted(users, key=operator.itemgetter(1), reverse=True) rank = 1 for allusers in sorted_list: if ctx.author.name == allusers[0]: author_rank = rank break rank += 1 footer_text = "Your Rank: {} {}: {}".format( author_rank, board_type, user_stat) # multiple page support page = 1 per_page = 15 pages = math.ceil(len(sorted_list) / per_page) if 1 <= page_list <= pages: page = page_list else: await ctx.send( "**Please enter a valid page number! (1 - {})**".format( str(pages))) return msg = "" msg += "Rank Name (Page {}/{}) \n\n".format( page, pages) rank = 1 + per_page * (page - 1) start_index = per_page * page - per_page end_index = per_page * page default_label = " " special_labels = ["♔", "♕", "♖", "♗", "♘", "♙"] async for single_user in AsyncIter(sorted_list[start_index:end_index]): if rank - 1 < len(special_labels): label = special_labels[rank - 1] else: label = default_label msg += "{:<2}{:<2}{:<2} # {:<15}".format( rank, label, "➤", await self._truncate_text(single_user[0], 15)) msg += "{:>5}{:<2}{:<2}{:<5}\n".format( " ", " ", " ", " {}: ".format(board_type) + str(single_user[1])) rank += 1 msg += "-------------------------------------------- \n" msg += "{}".format(footer_text) em = discord.Embed(description="", colour=await self.bot.get_embed_colour(ctx)) em.set_author(name=title, icon_url=icon_url) em.description = box(msg) await ctx.send(embed=em)
async def _backpack_sell_button_action(self, ctx, emoji, page, item, price_shown, character): currency_name = await bank.get_currency_name(ctx.guild, ) msg = "" if emoji == "\N{DIGIT ONE}\N{COMBINING ENCLOSING KEYCAP}": # user reacted with one to sell. ctx.command.reset_cooldown(ctx) # sell one of the item price = 0 item.owned -= 1 price += price_shown msg += _( "**{author}** sold one {item} for {price} {currency_name}.\n" ).format( author=escape(ctx.author.display_name), item=box(item, lang="css"), price=humanize_number(price), currency_name=currency_name, ) if item.owned <= 0: del character.backpack[item.name] price = max(price, 0) if price > 0: try: await bank.deposit_credits(ctx.author, price) except BalanceTooHigh as e: await bank.set_balance(ctx.author, e.max_balance) elif emoji == "\N{CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS}": # user wants to sell all owned. ctx.command.reset_cooldown(ctx) price = 0 old_owned = item.owned count = 0 async for _loop_counter in AsyncIter(range(0, item.owned), steps=50): item.owned -= 1 price += price_shown if item.owned <= 0: del character.backpack[item.name] count += 1 msg += _( "**{author}** sold all their {old_item} for {price} {currency_name}.\n" ).format( author=escape(ctx.author.display_name), old_item=box(str(item) + " - " + str(old_owned), lang="css"), price=humanize_number(price), currency_name=currency_name, ) price = max(price, 0) if price > 0: try: await bank.deposit_credits(ctx.author, price) except BalanceTooHigh as e: await bank.set_balance(ctx.author, e.max_balance) elif (emoji == "\N{CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY}" ): # user wants to sell all but one. if item.owned == 1: ctx.command.reset_cooldown(ctx) return await smart_embed( ctx, _("You already only own one of those items.")) price = 0 old_owned = item.owned count = 0 async for _loop_counter in AsyncIter(range(1, item.owned), steps=50): item.owned -= 1 price += price_shown count += 1 if price != 0: msg += _( "**{author}** sold all but one of their {old_item} for {price} {currency_name}.\n" ).format( author=escape(ctx.author.display_name), old_item=box(str(item) + " - " + str(old_owned - 1), lang="css"), price=humanize_number(price), currency_name=currency_name, ) price = max(price, 0) if price > 0: try: await bank.deposit_credits(ctx.author, price) except BalanceTooHigh as e: await bank.set_balance(ctx.author, e.max_balance) else: # user doesn't want to sell those items. await ctx.send(_("Not selling those items.")) if msg: character.last_known_currency = await bank.get_balance(ctx.author) character.last_currency_check = time.time() await self.config.user(ctx.author ).set(await character.to_json(ctx, self.config)) pages = [ page for page in pagify(msg, delims=["\n"], page_length=1900) ] await BaseMenu( source=SimpleSource(pages), delete_message_after=True, clear_reactions_after=True, timeout=60, ).start(ctx=ctx)
async def _load_v2_playlist( self, ctx: commands.Context, uploaded_track_list, player: lavalink.player_manager.Player, playlist_url: str, uploaded_playlist_name: str, scope: str, author: Union[discord.User, discord.Member], guild: Union[discord.Guild], ): track_list = [] successful_count = 0 uploaded_track_count = len(uploaded_track_list) embed1 = discord.Embed(title=_("Please wait, adding tracks...")) playlist_msg = await self.send_embed_msg(ctx, embed=embed1) notifier = Notifier(ctx, playlist_msg, {"playlist": _("Loading track {num}/{total}...")}) async for track_count, song_url in AsyncIter( uploaded_track_list).enumerate(start=1): try: try: result, called_api = await self.api_interface.fetch_track( ctx, player, Query.process_input(song_url, self.local_folder_current_path)) except TrackEnqueueError: self.update_player_lock(ctx, False) return await self.send_embed_msg( ctx, title=_("Unable to Get Track"), description=_( "I'm unable to get a track from Lavalink at the moment, " "try again in a few minutes."), ) except Exception as e: self.update_player_lock(ctx, False) raise e track = result.tracks[0] except Exception as err: debug_exc_log(log, err, f"Failed to get track for {song_url}") continue try: track_obj = self.get_track_json(player, other_track=track) track_list.append(track_obj) successful_count += 1 except Exception as err: debug_exc_log(log, err, f"Failed to create track for {track}") continue if (track_count % 2 == 0) or (track_count == len(uploaded_track_list)): await notifier.notify_user(current=track_count, total=len(uploaded_track_list), key="playlist") playlist = await create_playlist( ctx, self.playlist_api, scope, uploaded_playlist_name, playlist_url, track_list, author, guild, ) scope_name = self.humanize_scope( scope, ctx=guild if scope == PlaylistScope.GUILD.value else author) if not successful_count: msg = _("Empty playlist {name} (`{id}`) [**{scope}**] created." ).format(name=playlist.name, id=playlist.id, scope=scope_name) elif uploaded_track_count != successful_count: bad_tracks = uploaded_track_count - successful_count msg = _( "Added {num} tracks from the {playlist_name} playlist. {num_bad} track(s) " "could not be loaded.").format(num=successful_count, playlist_name=playlist.name, num_bad=bad_tracks) else: msg = _("Added {num} tracks from the {playlist_name} playlist." ).format(num=successful_count, playlist_name=playlist.name) embed3 = discord.Embed(colour=await ctx.embed_colour(), title=_("Playlist Saved"), description=msg) await playlist_msg.edit(embed=embed3)
async def timerole_update(self): utcnow = datetime.utcnow() all_guilds = await self.config.all_guilds() # all_mrs = await self.config.custom("RoleMember").all() # log.debug(f"Begin timerole update") for guild in self.bot.guilds: guild_id = guild.id if guild_id not in all_guilds: log.debug(f"Guild has no configured settings: {guild}") continue add_results = "" remove_results = "" reapply = all_guilds[guild_id]["reapply"] role_dict = all_guilds[guild_id]["roles"] if not any(role_dict.values()): # No roles log.debug(f"No roles are configured for guild: {guild}") continue # all_mr = await self.config.all_custom("RoleMember") # log.debug(f"{all_mr=}") async for member in AsyncIter(guild.members, steps=10): addlist = [] removelist = [] for role_id, role_data in role_dict.items(): # Skip non-configured roles if not role_data: continue mr_dict = await self.config.custom("RoleMember", role_id, member.id).all() # Stop if they've had the role and reapplying is disabled if not reapply and mr_dict["had_role"]: log.debug(f"{member.display_name} - Not reapplying") continue # Stop if the check_again_time hasn't passed yet if ( mr_dict["check_again_time"] is not None and datetime.fromisoformat(mr_dict["check_again_time"]) >= utcnow ): log.debug(f"{member.display_name} - Not time to check again yet") continue member: discord.Member has_roles = {r.id for r in member.roles} # Stop if they currently have or don't have the role, and mark had_role if (int(role_id) in has_roles and not role_data["remove"]) or ( int(role_id) not in has_roles and role_data["remove"] ): if not mr_dict["had_role"]: await self.config.custom( "RoleMember", role_id, member.id ).had_role.set(True) log.debug(f"{member.display_name} - applying had_role") continue # Stop if they don't have all the required roles if role_data is None or ( "required" in role_data and not set(role_data["required"]) & has_roles ): continue check_time = member.joined_at + timedelta( days=role_data["days"], hours=role_data.get("hours", 0), ) # Check if enough time has passed to get the role and save the check_again_time if check_time >= utcnow: await self.config.custom( "RoleMember", role_id, member.id ).check_again_time.set(check_time.isoformat()) log.debug( f"{member.display_name} - Not enough time has passed to qualify for the role\n" f"Waiting until {check_time}" ) continue if role_data["remove"]: removelist.append(role_id) else: addlist.append(role_id) # Done iterating through roles, now add or remove the roles if not addlist and not removelist: continue # log.debug(f"{addlist=}\n{removelist=}") add_roles = [ discord.utils.get(guild.roles, id=int(role_id)) for role_id in addlist ] remove_roles = [ discord.utils.get(guild.roles, id=int(role_id)) for role_id in removelist ] if None in add_roles or None in remove_roles: log.info( f"Timerole ran into an error with the roles in: {add_roles + remove_roles}" ) if addlist: try: await member.add_roles(*add_roles, reason="Timerole", atomic=False) except (discord.Forbidden, discord.NotFound) as e: log.exception("Failed Adding Roles") add_results += f"{member.display_name} : **(Failed Adding Roles)**\n" else: add_results += " \n".join( f"{member.display_name} : {role.name}" for role in add_roles ) for role_id in addlist: await self.config.custom( "RoleMember", role_id, member.id ).had_role.set(True) if removelist: try: await member.remove_roles(*remove_roles, reason="Timerole", atomic=False) except (discord.Forbidden, discord.NotFound) as e: log.exception("Failed Removing Roles") remove_results += f"{member.display_name} : **(Failed Removing Roles)**\n" else: remove_results += " \n".join( f"{member.display_name} : {role.name}" for role in remove_roles ) for role_id in removelist: await self.config.custom( "RoleMember", role_id, member.id ).had_role.set(True) # Done iterating through members, now maybe announce to the guild channel = await self.config.guild(guild).announce() if channel is not None: channel = guild.get_channel(channel) if add_results: title = "**These members have received the following roles**\n" await announce_to_channel(channel, add_results, title) if remove_results: title = "**These members have lost the following roles**\n" await announce_to_channel(channel, remove_results, title)
async def get_playlist_match( self, context: commands.Context, matches: MutableMapping, scope: str, author: discord.User, guild: discord.Guild, specified_user: bool = False, ) -> Tuple[Optional[Playlist], str, str]: """ Parameters ---------- context: commands.Context The context in which this is being called. matches: dict A dict of the matches found where key is scope and value is matches. scope:str The custom config scope. A value from :code:`PlaylistScope`. author: discord.User The user. guild: discord.Guild The guild. specified_user: bool Whether or not a user ID was specified via argparse. Returns ------- Tuple[Optional[Playlist], str, str] Tuple of Playlist or None if none found, original user input and scope. Raises ------ `TooManyMatches` When more than 10 matches are found or When multiple matches are found but none is selected. """ correct_scope_matches: List[Playlist] original_input = matches.get("arg") lazy_match = False if scope is None: correct_scope_matches_temp: MutableMapping = matches.get("all") lazy_match = True else: correct_scope_matches_temp: MutableMapping = matches.get(scope) guild_to_query = guild.id user_to_query = author.id correct_scope_matches_user = [] correct_scope_matches_guild = [] correct_scope_matches_global = [] if not correct_scope_matches_temp: return None, original_input, scope or PlaylistScope.GUILD.value if lazy_match or (scope == PlaylistScope.USER.value): correct_scope_matches_user = [ p for p in matches.get(PlaylistScope.USER.value) if user_to_query == p.scope_id ] if lazy_match or (scope == PlaylistScope.GUILD.value and not correct_scope_matches_user): if specified_user: correct_scope_matches_guild = [ p for p in matches.get(PlaylistScope.GUILD.value) if guild_to_query == p.scope_id and p.author == user_to_query ] else: correct_scope_matches_guild = [ p for p in matches.get(PlaylistScope.GUILD.value) if guild_to_query == p.scope_id ] if lazy_match or (scope == PlaylistScope.GLOBAL.value and not correct_scope_matches_user and not correct_scope_matches_guild): if specified_user: correct_scope_matches_global = [ p for p in matches.get(PlaylistScope.GLOBAL.value) if p.author == user_to_query ] else: correct_scope_matches_global = [ p for p in matches.get(PlaylistScope.GLOBAL.value) ] correct_scope_matches = [ *correct_scope_matches_global, *correct_scope_matches_guild, *correct_scope_matches_user, ] match_count = len(correct_scope_matches) if match_count > 1: correct_scope_matches2 = [ p for p in correct_scope_matches if p.name == str(original_input).strip() ] if correct_scope_matches2: correct_scope_matches = correct_scope_matches2 elif original_input.isnumeric(): arg = int(original_input) correct_scope_matches3 = [ p for p in correct_scope_matches if p.id == arg ] if correct_scope_matches3: correct_scope_matches = correct_scope_matches3 match_count = len(correct_scope_matches) # We done all the trimming we can with the info available time to ask the user if match_count > 10: if original_input.isnumeric(): arg = int(original_input) correct_scope_matches = [ p for p in correct_scope_matches if p.id == arg ] if match_count > 10: raise TooManyMatches( _("{match_count} playlists match {original_input}: " "Please try to be more specific, or use the playlist ID." ).format(match_count=match_count, original_input=original_input)) elif match_count == 1: return correct_scope_matches[ 0], original_input, correct_scope_matches[0].scope elif match_count == 0: return None, original_input, scope or PlaylistScope.GUILD.value # TODO : Convert this section to a new paged reaction menu when Toby Menus are Merged pos_len = 3 playlists = f"{'#':{pos_len}}\n" number = 0 correct_scope_matches = sorted(correct_scope_matches, key=lambda x: x.name.lower()) async for number, playlist in AsyncIter( correct_scope_matches).enumerate(start=1): author = self.bot.get_user( playlist.author) or playlist.author or _("Unknown") line = _("{number}." " <{playlist.name}>\n" " - Scope: < {scope} >\n" " - ID: < {playlist.id} >\n" " - Tracks: < {tracks} >\n" " - Author: < {author} >\n\n").format( number=number, playlist=playlist, scope=self.humanize_scope(playlist.scope), tracks=len(playlist.tracks), author=author, ) playlists += line embed = discord.Embed( title=_("{playlists} playlists found, which one would you like?"). format(playlists=number), description=box(playlists, lang="md"), colour=await context.embed_colour(), ) msg = await context.send(embed=embed) avaliable_emojis = ReactionPredicate.NUMBER_EMOJIS[1:] avaliable_emojis.append("🔟") emojis = avaliable_emojis[:len(correct_scope_matches)] close_emoji = self.get_cross_emoji(context) emojis.append(close_emoji) start_adding_reactions(msg, emojis) pred = ReactionPredicate.with_emojis(emojis, msg, user=context.author) try: await context.bot.wait_for("reaction_add", check=pred, timeout=60) except asyncio.TimeoutError: with contextlib.suppress(discord.HTTPException): await msg.delete() raise TooManyMatches( _("Too many matches found and you did not select which one you wanted." )) if emojis[pred.result] == close_emoji: with contextlib.suppress(discord.HTTPException): await msg.delete() raise TooManyMatches( _("Too many matches found and you did not select which one you wanted." )) with contextlib.suppress(discord.HTTPException): await msg.delete() return ( correct_scope_matches[pred.result], original_input, correct_scope_matches[pred.result].scope, )
async def _build_search_page(self, ctx: commands.Context, tracks: List, page_num: int) -> discord.Embed: search_num_pages = math.ceil(len(tracks) / 5) search_idx_start = (page_num - 1) * 5 search_idx_end = search_idx_start + 5 search_list = "" command = ctx.invoked_with folder = False async for i, track in AsyncIter( tracks[search_idx_start:search_idx_end]).enumerate( start=search_idx_start): search_track_num = i + 1 if search_track_num > 5: search_track_num = search_track_num % 5 if search_track_num == 0: search_track_num = 5 try: query = Query.process_input(track.uri, self.local_folder_current_path) if query.is_local: search_list += "`{0}.` **{1}**\n[{2}]\n".format( search_track_num, track.title, LocalPath( track.uri, self.local_folder_current_path).to_string_user(), ) else: search_list += "`{0}.` **[{1}]({2})**\n".format( search_track_num, track.title, track.uri) except AttributeError: track = Query.process_input(track, self.local_folder_current_path) if track.is_local and command != "search": search_list += "`{}.` **{}**\n".format( search_track_num, track.to_string_user()) if track.is_album: folder = True else: search_list += "`{}.` **{}**\n".format( search_track_num, track.to_string_user()) if hasattr(tracks[0], "uri") and hasattr(tracks[0], "track_identifier"): title = _("Tracks Found:") footer = _("search results") elif folder: title = _("Folders Found:") footer = _("local folders") else: title = _("Files Found:") footer = _("local tracks") embed = discord.Embed(colour=await ctx.embed_colour(), title=title, description=search_list) embed.set_footer(text=(_("Page {page_num}/{total_pages}") + " | {num_results} {footer}").format( page_num=page_num, total_pages=search_num_pages, num_results=len(tracks), footer=footer, )) return embed