Skip to content

Leveling

Bases: Extension

Source code in extentions/leveling.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
class Levels(Extension):
    def __init__(self, bot: Client):
        self.bot = bot

    @listen()
    async def on_ready(self):
        self.lvl_cooldown_task.start()

    LevelSettings = SlashCommand(name='leveling_config', default_member_permissions=Permissions.ADMINISTRATOR, description='Manage leveling settings.')

    @LevelSettings.subcommand('ignored_channel', 'add', 'Add a channel to ignored channels.')
    @channel()
    async def LevelingAddIgnoredChannels(self, ctx:InteractionContext, channel: OptionType.CHANNEL=None):
        """/leveling_config ignored_channel add
        Description:
            Add a channel to ignored channels by leveling
        Args:
            channel (OptionType.CHANNEL, optional): Channel to add, defaults to channel command is executed in
        """
        await ctx.defer(ephemeral=True)
        if channel is None:
            channel = ctx.channel
        settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
        if settings is None:
            await db.leveling_settings(guildid=ctx.guild.id).insert()
        ignored_channels = settings.ignored_channels
        if ignored_channels is None:
            ignored_channels = list()
        if channel.id in ignored_channels:
            await ctx.send(f'{channel.mention} is already ignored.', ephemeral=True)
        ignored_channels.append(channel.id)
        await settings.save()
        channel_mentions = [ctx.guild.get_channel(id) for id in ignored_channels]
        channel_mentions = [ch.mention for ch in channel_mentions if ch is not None]
        channel_mentions = ' '.join(channel_mentions)
        embed = Embed(description=f"Channel {channel.mention} set to be ignored.")
        embed.add_field('Ignored Channels', channel_mentions)
        await ctx.send(embed=embed, ephemeral=True)

    @LevelSettings.subcommand('ignored_channel', 'remove', 'Remove a channel from ignored channels.')
    @channel()
    async def LevelingRemoveIgnoredChannels(self, ctx:InteractionContext, channel: OptionType.CHANNEL=None):
        """/leveling_config ignored_channel remove
        Description:
            Remove a channel from ignored channels by leveling
        Args:
            channel (OptionType.CHANNEL, optional): Channel to remove, defaults to channel command is executed in
        """
        await ctx.defer(ephemeral=True)
        if channel is None:
            channel = ctx.channel
        settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
        if settings is None:
            await db.leveling_settings(guildid=ctx.guild.id).insert()
        ignored_channels = settings.ignored_channels
        if ignored_channels is None:
            ignored_channels = list()
        if channel.id not in ignored_channels:
            await ctx.send(f'{channel.mention} is not being ignored by leveling.', ephemeral=True)
        ignored_channels.remove(channel.id)
        await settings.save()
        channel_mentions = [ctx.guild.get_channel(id) for id in ignored_channels]
        channel_mentions = [ch.mention for ch in channel_mentions if ch is not None]
        channel_mentions = ' '.join(channel_mentions)
        embed = Embed(description=f"Channel {channel.mention} removed from ignored channels.")
        embed.add_field('Ignored Channels', channel_mentions)
        await ctx.send(embed=embed, ephemeral=True)

    @LevelSettings.subcommand('ignored_role', 'add', 'Make a role to be ignored by leveling.')
    @role()
    async def LevelingAddIgnoredRoles(self, ctx:InteractionContext, role: OptionType.ROLE):
        """/leveling_config ignored_role add
        Description:
            Add a role to be ignored by leveling
        Args:
            role (OptionType.ROLE): Role to add
        """
        await ctx.defer(ephemeral=True)
        settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
        if settings is None:
            await db.leveling_settings(guildid=ctx.guild.id).insert()
        ignored_roles = settings.ignored_roles
        if ignored_roles is None:
            ignored_roles = list()
        if role.id in ignored_roles:
            await ctx.send(f'{role.mention} is already ignored.', ephemeral=True)
        ignored_roles.append(role.id)
        await settings.save()
        role_mentions = [ctx.guild.get_role(id) for id in ignored_roles]
        role_mentions = [r.mention for r in role_mentions if r is not None]
        role_mentions = ' '.join(role_mentions)
        embed = Embed(description=f"Role {role.mention} was added to roles ignored by leveling.")
        embed.add_field('Ignored Roles', role_mentions)
        await ctx.send(embed=embed, ephemeral=True)

    @LevelSettings.subcommand('ignored_role', 'remove', 'Remove a role from ignored roles.')
    @role()
    async def LevelingRemoveIgnoredRoles(self, ctx:InteractionContext, role: OptionType.ROLE):
        """/leveling_config ignored_role remove
        Description:
            Remove a role from ignored by leveling
        Args:
            role (OptionType.ROLE): Role to remove
        """
        await ctx.defer(ephemeral=True)
        settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
        if settings is None:
            await db.leveling_settings(guildid=ctx.guild.id).insert()
        ignored_roles = settings.ignored_roles
        if ignored_roles is None:
            ignored_roles = list()
        if role.id not in ignored_roles:
            await ctx.send(f'{role.mention} is not being ignored by leveling.', ephemeral=True)
        ignored_roles.remove(role.id)
        await settings.save()
        role_mentions = [ctx.guild.get_role(id) for id in ignored_roles]
        role_mentions = [r.mention for r in role_mentions if r is not None]
        role_mentions = ' '.join(role_mentions)
        embed = Embed(description=f"Role {role.mention} was removed from roles ignored by leveling.")
        embed.add_field('Ignored Roles', role_mentions)
        await ctx.send(embed=embed, ephemeral=True)

    @LevelSettings.subcommand('ignored_member', 'add', 'Make a member to be ignored by leveling.')
    @user()
    async def LevelingAddIgnoredMember(self, ctx:InteractionContext, user: OptionType.USER):
        """/leveling_config ignored_member add
        Description:
            Make a member to be ignored by leveling.
        Args:
            user (OptionType.USER): User to add
        """
        await ctx.defer(ephemeral=True)
        settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
        if settings is None:
            await db.leveling_settings(guildid=ctx.guild.id).insert()
        ignored_users = settings.ignored_users
        if ignored_users is None:
            ignored_users = list()
        if user.id in ignored_users:
            await ctx.send(f'{user}|{user.id} is already ignored.', ephemeral=True)
        ignored_users.append(user.id)
        await settings.save()
        users = [ctx.guild.get_member(id) for id in ignored_users]
        users = [f'{r}({r.id})' for r in users if r is not None]
        users = ' '.join(users)
        embed = Embed(description=f"Member {user}({user.id}) was added to members ignored by leveling.")
        embed.add_field('Ignored Members', users)
        await ctx.send(embed=embed, ephemeral=True)

    @LevelSettings.subcommand('ignored_member', 'remove', 'Remove a member from ignored members.')
    @user()
    async def LevelingRemoveIgnoredMember(self, ctx:InteractionContext, user: OptionType.USER):
        """/leveling_config ignored_member remove
        Description:
            Remove a member from ignored members for leveling

        Args:
            user (OptionType.USER): User to ignore
        """
        await ctx.defer(ephemeral=True)
        settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
        if settings is None:
           await db.leveling_settings(guildid=ctx.guild.id).insert()
        ignored_users = settings.ignored_users
        if ignored_users is None:
            ignored_users = list()
        if user.id not in ignored_users:
            await ctx.send(f'{user}|{user.id} is not being ignored by leveling.', ephemeral=True)
        ignored_users.remove(user.id)
        await settings.save()
        users = [ctx.guild.get_member(id) for id in ignored_users]
        users = [f'{r}({r.id})' for r in users if r is not None]
        users = ' '.join(users)
        embed = Embed(description=f"Member {user}({user.id}) was removed from members ignored by leveling.")
        embed.add_field('Ignored Members', users)
        await ctx.send(embed=embed, ephemeral=True)

    @listen()
    async def on_xp_gain(self, event: MessageCreate):
        message = event.message # get message object from event
        user = message.author # get user object from message
        if user.bot: # if user is a bot, return
            return

        settings = await db.leveling_settings.find_one({'guildid':message.guild.id}) # get leveling settings for guild
        if not user.has_permission(Permissions.ADMINISTRATOR): # if user is not an admin
            if settings.ignored_users is not None: # if ignored users list is not empty
                if user.id in settings.ignored_users: # if user is in ignored users list, return
                    return
            if settings.ignored_roles is not None: # if ignored roles list is not empty
                if any(role for role in user.roles if role.id in settings.ignored_roles): # if user has an ignored role, return
                    return
        if settings.ignored_channels is not None: # if ignored channels list is not empty
            if message.channel.id in settings.ignored_channels: # if message is in ignored channel, return
                return

        cooldown = await db.levelwait.find_one({'guildid':message.guild.id, 'user':message.author.id}) # check if user is on cooldown
        if cooldown: # if user is on cooldown, return
            return
        await db.levelwait(guildid=message.guild.id, user=message.author.id, starttime=datetime.utcnow(), endtime=(datetime.utcnow() + timedelta(seconds=60))).insert() # add user to cooldown list

        member = await db.leveling.find_one({'guildid':message.guild.id, 'memberid':message.author.id}) # get leveling data for user
        if not member: # if user has no leveling data, create new data and return
            await db.leveling(guildid=message.guild.id, memberid=message.author.id, total_xp=0, level=0, xp_to_next_level=0, messages=1).insert()
            return

        level_stats = await db.levelingStats.find_one( {'level':member.level}) # get leveling stats for user's current level

        if member.xp_to_next_level is None: # if user has no xp to next level, set to 0
            xptnl = 0
        else:
            xptnl = member.xp_to_next_level
        xp_to_give = random.randint(15, 25) # generate random xp to give
        member.total_xp = member.total_xp+xp_to_give # add xp to user's total xp
        if member.messages is None: # if user has no message count, set to 0
            member.messages = 0
        member.messages = member.messages+1 # increment user's message count
        xp = xptnl + xp_to_give # calculate total xp

        if xp >= level_stats.xp_to_next_level: # if user has enough xp to level up
            member.level = member.level+1 # increment user's level
            member.xp_to_next_level = 0 # reset user's xp to next level
            roles = await db.leveling_roles.find_one({'guildid':message.guild.id, 'level':member.level}) # get roles associated with user's new level
            if roles: # if roles exist for user's new level
                role = message.guild.get_role(roles.roleid) # get role object
                if (role is not None) and (role not in message.author.roles): # if role exists and user does not already have role
                    await message.author.add_role(role, f'[Melody][LEVELUP]Added a role assoiciated with level {member.level}') # add role to user and send message
        else:
            member.xp_to_next_level = xp # set user's xp to next level to calculated value
        await member.save() # save user's leveling data

        # Find all leveling roles for the current guild that have a level requirement less than or equal to the member's level
        level_roles = db.leveling_roles.find({"guildid":message.guild.id, 'level':{'$lte':member.level}})

        # Create an empty list to store the role IDs of the leveling roles the member is eligible for
        roles = []

        # Loop through the leveling roles the member is eligible for and append their role IDs to the roles list
        async for role in level_roles:
            roles.append(role.roleid)

        # If the member is eligible for any leveling roles, loop through the role IDs and add the corresponding roles to the member's roles if they don't already have them
        if level_roles != []:
            for role_id in roles:
                role = message.guild.get_role(role_id)
                if role not in message.author.roles:
                    await message.author.add_role(role)

        # If the member's display name is None or different from their current display name, update their display name and save the changes
        if (member.display_name is None) or (member.display_name != message.author.display_name):
            member.display_name = message.author.display_name
            await member.save()

    @Task.create(IntervalTrigger(seconds=5))
    async def lvl_cooldown_task(self):
        level_wait = db.levelwait.find({'endtime':{'$lte':datetime.utcnow()}})
        async for instance in level_wait:
            mem = int(f'{instance.guildid}{instance.user}')
            if mem not in cooldown_task_ongoing:
                cooldown_task_ongoing.append(mem)
                await instance.delete()
                cooldown_task_ongoing.remove(mem)

    leveling = SlashCommand(name='leveling', description='add/remove leveling roles', default_member_permissions=Permissions.MANAGE_ROLES)

    @leveling.subcommand(sub_cmd_name='addrole', sub_cmd_description="allow's me to create leveling roles")
    @role()
    @role_level()
    async def leveling_add_role(self, ctx:InteractionContext, role: OptionType.ROLE, role_level:str):
        """/leveling addrole
        Description:
            Create level roles

        Args:
            role (OptionType.ROLE): Role
            role_level (str): Role level number, has to be more than 0 and less than 1000
        """
        await ctx.defer()
        if (int(role_level) < 1) or (int(role_level) > 1000):
            await ctx.send('role level has to be more than 0 and less than 1000')
            return

        check = await db.leveling_roles.find_one({'guildid':ctx.guild_id, 'roleid':role.id})
        if check is None:
            await db.leveling_roles(guildid=ctx.guild_id, roleid=role.id, level=int(role_level)).insert()
            await ctx.send(embed=Embed(color=0xffcc50, description=f'Leveling role {role.mention} assigned to level {role_level}'))
        else:
            await ctx.send(embed=Embed(color=0xDD2222, description=f':x: Leveling role {role.mention} is already assigned to level {check.level}'))

        for member in ctx.guild.members:
            mem = await db.leveling.find_one({'guildid':ctx.guild.id, 'memberid':member.id})
            level_roles = db.leveling_roles.find({"guildid":ctx.guild_id, 'level':{'$lte':mem.level}})
            roles = []
            async for role in level_roles:
                roles.append(role.roleid)
            if level_roles != []:
                for role_id in roles:
                    role = ctx.guild.get_role(role_id)
                    if role not in member.roles:
                        await member.add_role(role)

    @slash_command(name='ranklist', description="leveling roles list")
    async def ranks_list(self, ctx:InteractionContext):
        """/ranklist
        Description:
            List leveling roles
        """
        await ctx.defer()
        from interactions.ext.paginators import Paginator
        def chunks(l, n):
            n = max(1, n)
            return (l[i:i+n] for i in range(0, len(l), n))

        def mlis(lst, s, e):
            nc = list(chunks(lst, 20))
            mc = ''
            for testlist in nc[s:e]:
                for m in testlist:
                    mc = mc + m
            return mc

        def newpage(title, level, role):
            embed = Embed(title=title,
            color=0xffcc50)
            embed.add_field(name='Level', value=level, inline=True)
            embed.add_field(name='Role', value=role, inline=True)
            return embed


        levels = db.leveling_roles.find({"guildid":ctx.guild_id})
        level = []
        roles = []
        async for l in levels:
            level.append(f'{l.level}\n')
            lvlrole = find_role(ctx, l.roleid)
            if lvlrole is None:
                roles.append('[ROLE NOT FOUND]\n')
            else:
                roles.append(f"{lvlrole.mention}\n")

        if (level or roles) == []:
            embed = Embed(description=f"There are no ranks for {ctx.guild.name}.",
                        color=0xffcc50)
            await ctx.send(embed=embed)
            return

        s = -1
        e = 0
        embedcount = 1
        nc = list(chunks(level, 20))

        embeds = []
        while embedcount <= len(nc):
            s = s+1
            e = e+1
            embeds.append(newpage(f'List of ranks for {ctx.guild.name}', mlis(level, s, e), mlis(roles, s, e)))
            embedcount = embedcount+1

        paginator = Paginator(
            client=self.bot, 
            pages=embeds,
            timeout_interval=80,
            show_select_menu=False)
        await paginator.send(ctx)

    @leveling.subcommand(sub_cmd_name='removerole', sub_cmd_description="allow's me to remove leveling roles")
    @role()
    async def leveling_remove_role(self, ctx:InteractionContext, role: OptionType.ROLE=None):
        """/leveling removerole

        Args:
            role (OptionType.ROLE, optional): Role to remove from leveling.
        """
        await ctx.defer()
        if role is None:
            await ctx.send('you have to include a role')
            return

        check = await db.leveling_roles.find_one({'guildid':ctx.guild.id, 'roleid':role.id})
        if check is None:
            await ctx.send(embed=Embed(color=0xDD2222, description=f':x: Leveling role {role.mention} is not assigned to a level'))
        else:
            await ctx.send(embed=Embed(color=0xffcc50, description=f'Leveling role {role.mention} removed from level {check.level}'))
            await check.delete()

        for member in ctx.guild.members:
            if role in member.roles:
                await member.remove_role(role)

    @slash_command(name='rank', description='check your leveling statistics')
    @member()
    async def newrank(self, ctx: SlashContext, member:OptionType.USER=None):
        """/rank
        Description:
            Generates leveling info card

        Args:
            member (OptionType.USER, optional): Member, defaults to member executing the command
        """
        await ctx.defer() # defer the response to avoid timeout
        if member is None: # if no member parameter is provided, use the author of the command
            member = ctx.author

        levels = await db.leveling.find_one({'guildid':ctx.guild_id, 'memberid':member.id}) # get the leveling data for the member
        if levels is None: # if no leveling data is found, send a message and return
            await ctx.send("You don't have any xp yet. You can start having conversations with people to gain xp.", ephemeral=True)
            return

        level_stats = await db.levelingStats.find_one({'level':levels.level}) # get the leveling stats for the member's current level
        if (levels.display_name is None) or (levels.display_name != member.display_name): # if the member's display name has changed, update it in the database
            levels.display_name = member.display_name
            await levels.save()

        def getPercent(first, second): # helper function to calculate percentage
            return first / second * 100
        percent = getPercent(levels.xp_to_next_level,level_stats.xp_to_next_level) # calculate the percentage of xp towards the next level
        def findx(percentage): # helper function to calculate the progress bar width
            if percentage == 0:
                return 1
            return 550/(100/percentage)

        if member.guild_avatar is not None: # get the member's avatar url
            avatarurl = f'{member.guild_avatar.url}.png'
        else:
            avatarurl = f'{member.avatar.url}.png'

        def round(im): # helper function to round the member's avatar
            im = im.resize((210*16,210*16), resample=Image.ANTIALIAS)
            mask = Image.new("L", im.size, 0)
            draw = ImageDraw.Draw(mask)
            draw.ellipse((0,0)+im.size, fill=255)
            out = ImageOps.fit(im, mask.size, centering=(0,0))
            out.putalpha(mask)
            image = out.resize((210,210), resample=Image.ANTIALIAS).convert("RGBA")
            return image

        IW, IH = (956, 435) # set the image width and height
        aspect_ratio = IW/IH

        if levels.lc_background is not None: # get the background image for the level card
            try:
                background = Image.open(requests.get(f'{levels.lc_background}', stream=True).raw).crop((0,0,IW,IH)).convert("RGBA")
            except:
                background = Image.open(requests.get('https://i.imgur.com/4yzKbQo.png', stream=True).raw).convert("RGBA")
        else:
            background = Image.open(requests.get('https://i.imgur.com/4yzKbQo.png', stream=True).raw).convert("RGBA")

        overlay = Image.open(requests.get('https://i.imgur.com/fsuIzSv.png', stream=True).raw).convert("RGBA") # add an overlay to the background
        background.paste(overlay, (0, 0), overlay)

        try:
            pfp = Image.open(requests.get(avatarurl, stream=True).raw).resize((230,230)).convert("RGBA") # get the member's avatar image
        except:
            pfp = Image.open(requests.get('https://cdn.discordapp.com/embed/avatars/1.png', stream=True).raw).resize((230,230)).convert("RGBA")
        pfp = round(pfp) # round the member's avatar
        background.paste(pfp, (78, 115), pfp) # paste the avatar onto the background

        def draw_progress_bar(fx): # helper function to draw the progress bar
            rad = 115
            im = Image.open(requests.get('https://i.imgur.com/sRseF8Y.png', stream=True).raw).convert('RGBA')
            im = im.resize((fx*16,30*16), resample=Image.ANTIALIAS)
            circle = Image.new('L', (rad * 2, rad * 2), 0)
            draw = ImageDraw.Draw(circle)
            draw.ellipse((0, 0, rad * 2, rad * 2), fill=255)
            alpha = Image.new('L', im.size, 255)
            w, h = im.size
            alpha.paste(circle.crop((0, 0, rad, rad)), (0, 0))
            alpha.paste(circle.crop((0, rad, rad, rad * 2)), (0, h - rad))
            alpha.paste(circle.crop((rad, 0, rad * 2, rad)), (w - rad, 0))
            alpha.paste(circle.crop((rad, rad, rad * 2, rad * 2)), (w - rad, h - rad))
            im.putalpha(alpha)
            im = im.resize((fx,30), resample=Image.ANTIALIAS)
            return im

        fx = findx(int(percent)) # calculate the progress bar width
        progress_bar = draw_progress_bar(int(fx)) # draw the progress bar
        background.paste(progress_bar, (330, 370), progress_bar) # paste the progress bar onto the background

        def rectangle_fill(im): # helper function to fill the background with a rectangle
            mask = Image.new("L", im.size, 0)
            draw = ImageDraw.Draw(mask)
            draw.rectangle((0,0)+im.size, fill=255)
            out = ImageOps.fit(im, mask.size, centering=(0,0))
            out.putalpha(mask)
            return out

        background = rectangle_fill(background) # fill the background with a rectangle

        font = ImageFont.truetype('Everson-Mono-Bold.ttf', 45) # set the font for the text

        I1 = ImageDraw.Draw(background)

        lvlfont = ImageFont.truetype('Everson-Mono-Bold.ttf', 45)
        I1.text((73,352), f'LVL:{levels.level}', font=lvlfont, stroke_width=2, stroke_fill=(30, 27, 26), fill=(255, 255, 255)) # add the member's level to the level card

        lvlmsg = f'XP: {levels.xp_to_next_level}/{level_stats.xp_to_next_level}\nTotal XP: {levels.total_xp}\nMessages: {levels.messages}' # create the level message
        I1.text((341,110), lvlmsg, font=font, stroke_width=2, stroke_fill=(30, 27, 26), fill=(255, 255, 255)) # add the level message to the level card

        namefont = ImageFont.truetype('Everson-Mono-Bold.ttf', 50)
        name = f'{member.display_name}'
        if len(name) > 27: # shorten the member's name if it is too long
            name = name[:-4]

        I1.text((73,28), name, font=namefont, stroke_width=2, stroke_fill=(30, 27, 26), fill=(255, 255, 255)) # add the member's name to the level card
        background.save(f'levelcard_{member.id}.png') # save the level card as a file
        await ctx.send(file=f"levelcard_{member.id}.png") # send the level card as a file
        os.remove(f'levelcard_{member.id}.png') # remove the level card file

    @slash_command(name='leaderboard', description='check the servers leveling leaderboard')
    async def leaderboard(self, ctx: InteractionContext):
        """/leaderboard
        Description:
            Sends a button to the leaderboard for the server the command is executed in.
        """
        components = Button(
            style=ButtonStyle.URL,
            label="Click Me!",
            url=f"https://www.beni2am.space/melody/leaderboard/{ctx.guild_id}/",
        )
        await ctx.send("A button to the web leaderboard!", components=components)

LevelingAddIgnoredChannels(ctx, channel=None) async

/leveling_config ignored_channel add

Description

Add a channel to ignored channels by leveling

Parameters:

Name Type Description Default
channel OptionType.CHANNEL

Channel to add, defaults to channel command is executed in

None
Source code in extentions/leveling.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@LevelSettings.subcommand('ignored_channel', 'add', 'Add a channel to ignored channels.')
@channel()
async def LevelingAddIgnoredChannels(self, ctx:InteractionContext, channel: OptionType.CHANNEL=None):
    """/leveling_config ignored_channel add
    Description:
        Add a channel to ignored channels by leveling
    Args:
        channel (OptionType.CHANNEL, optional): Channel to add, defaults to channel command is executed in
    """
    await ctx.defer(ephemeral=True)
    if channel is None:
        channel = ctx.channel
    settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
    if settings is None:
        await db.leveling_settings(guildid=ctx.guild.id).insert()
    ignored_channels = settings.ignored_channels
    if ignored_channels is None:
        ignored_channels = list()
    if channel.id in ignored_channels:
        await ctx.send(f'{channel.mention} is already ignored.', ephemeral=True)
    ignored_channels.append(channel.id)
    await settings.save()
    channel_mentions = [ctx.guild.get_channel(id) for id in ignored_channels]
    channel_mentions = [ch.mention for ch in channel_mentions if ch is not None]
    channel_mentions = ' '.join(channel_mentions)
    embed = Embed(description=f"Channel {channel.mention} set to be ignored.")
    embed.add_field('Ignored Channels', channel_mentions)
    await ctx.send(embed=embed, ephemeral=True)

LevelingRemoveIgnoredChannels(ctx, channel=None) async

/leveling_config ignored_channel remove

Description

Remove a channel from ignored channels by leveling

Parameters:

Name Type Description Default
channel OptionType.CHANNEL

Channel to remove, defaults to channel command is executed in

None
Source code in extentions/leveling.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@LevelSettings.subcommand('ignored_channel', 'remove', 'Remove a channel from ignored channels.')
@channel()
async def LevelingRemoveIgnoredChannels(self, ctx:InteractionContext, channel: OptionType.CHANNEL=None):
    """/leveling_config ignored_channel remove
    Description:
        Remove a channel from ignored channels by leveling
    Args:
        channel (OptionType.CHANNEL, optional): Channel to remove, defaults to channel command is executed in
    """
    await ctx.defer(ephemeral=True)
    if channel is None:
        channel = ctx.channel
    settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
    if settings is None:
        await db.leveling_settings(guildid=ctx.guild.id).insert()
    ignored_channels = settings.ignored_channels
    if ignored_channels is None:
        ignored_channels = list()
    if channel.id not in ignored_channels:
        await ctx.send(f'{channel.mention} is not being ignored by leveling.', ephemeral=True)
    ignored_channels.remove(channel.id)
    await settings.save()
    channel_mentions = [ctx.guild.get_channel(id) for id in ignored_channels]
    channel_mentions = [ch.mention for ch in channel_mentions if ch is not None]
    channel_mentions = ' '.join(channel_mentions)
    embed = Embed(description=f"Channel {channel.mention} removed from ignored channels.")
    embed.add_field('Ignored Channels', channel_mentions)
    await ctx.send(embed=embed, ephemeral=True)

LevelingAddIgnoredRoles(ctx, role) async

/leveling_config ignored_role add

Description

Add a role to be ignored by leveling

Parameters:

Name Type Description Default
role OptionType.ROLE

Role to add

required
Source code in extentions/leveling.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@LevelSettings.subcommand('ignored_role', 'add', 'Make a role to be ignored by leveling.')
@role()
async def LevelingAddIgnoredRoles(self, ctx:InteractionContext, role: OptionType.ROLE):
    """/leveling_config ignored_role add
    Description:
        Add a role to be ignored by leveling
    Args:
        role (OptionType.ROLE): Role to add
    """
    await ctx.defer(ephemeral=True)
    settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
    if settings is None:
        await db.leveling_settings(guildid=ctx.guild.id).insert()
    ignored_roles = settings.ignored_roles
    if ignored_roles is None:
        ignored_roles = list()
    if role.id in ignored_roles:
        await ctx.send(f'{role.mention} is already ignored.', ephemeral=True)
    ignored_roles.append(role.id)
    await settings.save()
    role_mentions = [ctx.guild.get_role(id) for id in ignored_roles]
    role_mentions = [r.mention for r in role_mentions if r is not None]
    role_mentions = ' '.join(role_mentions)
    embed = Embed(description=f"Role {role.mention} was added to roles ignored by leveling.")
    embed.add_field('Ignored Roles', role_mentions)
    await ctx.send(embed=embed, ephemeral=True)

LevelingRemoveIgnoredRoles(ctx, role) async

/leveling_config ignored_role remove

Description

Remove a role from ignored by leveling

Parameters:

Name Type Description Default
role OptionType.ROLE

Role to remove

required
Source code in extentions/leveling.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
@LevelSettings.subcommand('ignored_role', 'remove', 'Remove a role from ignored roles.')
@role()
async def LevelingRemoveIgnoredRoles(self, ctx:InteractionContext, role: OptionType.ROLE):
    """/leveling_config ignored_role remove
    Description:
        Remove a role from ignored by leveling
    Args:
        role (OptionType.ROLE): Role to remove
    """
    await ctx.defer(ephemeral=True)
    settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
    if settings is None:
        await db.leveling_settings(guildid=ctx.guild.id).insert()
    ignored_roles = settings.ignored_roles
    if ignored_roles is None:
        ignored_roles = list()
    if role.id not in ignored_roles:
        await ctx.send(f'{role.mention} is not being ignored by leveling.', ephemeral=True)
    ignored_roles.remove(role.id)
    await settings.save()
    role_mentions = [ctx.guild.get_role(id) for id in ignored_roles]
    role_mentions = [r.mention for r in role_mentions if r is not None]
    role_mentions = ' '.join(role_mentions)
    embed = Embed(description=f"Role {role.mention} was removed from roles ignored by leveling.")
    embed.add_field('Ignored Roles', role_mentions)
    await ctx.send(embed=embed, ephemeral=True)

LevelingAddIgnoredMember(ctx, user) async

/leveling_config ignored_member add

Description

Make a member to be ignored by leveling.

Parameters:

Name Type Description Default
user OptionType.USER

User to add

required
Source code in extentions/leveling.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
@LevelSettings.subcommand('ignored_member', 'add', 'Make a member to be ignored by leveling.')
@user()
async def LevelingAddIgnoredMember(self, ctx:InteractionContext, user: OptionType.USER):
    """/leveling_config ignored_member add
    Description:
        Make a member to be ignored by leveling.
    Args:
        user (OptionType.USER): User to add
    """
    await ctx.defer(ephemeral=True)
    settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
    if settings is None:
        await db.leveling_settings(guildid=ctx.guild.id).insert()
    ignored_users = settings.ignored_users
    if ignored_users is None:
        ignored_users = list()
    if user.id in ignored_users:
        await ctx.send(f'{user}|{user.id} is already ignored.', ephemeral=True)
    ignored_users.append(user.id)
    await settings.save()
    users = [ctx.guild.get_member(id) for id in ignored_users]
    users = [f'{r}({r.id})' for r in users if r is not None]
    users = ' '.join(users)
    embed = Embed(description=f"Member {user}({user.id}) was added to members ignored by leveling.")
    embed.add_field('Ignored Members', users)
    await ctx.send(embed=embed, ephemeral=True)

LevelingRemoveIgnoredMember(ctx, user) async

/leveling_config ignored_member remove

Description

Remove a member from ignored members for leveling

Parameters:

Name Type Description Default
user OptionType.USER

User to ignore

required
Source code in extentions/leveling.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@LevelSettings.subcommand('ignored_member', 'remove', 'Remove a member from ignored members.')
@user()
async def LevelingRemoveIgnoredMember(self, ctx:InteractionContext, user: OptionType.USER):
    """/leveling_config ignored_member remove
    Description:
        Remove a member from ignored members for leveling

    Args:
        user (OptionType.USER): User to ignore
    """
    await ctx.defer(ephemeral=True)
    settings = await db.leveling_settings.find_one({"guildid":ctx.guild.id})
    if settings is None:
       await db.leveling_settings(guildid=ctx.guild.id).insert()
    ignored_users = settings.ignored_users
    if ignored_users is None:
        ignored_users = list()
    if user.id not in ignored_users:
        await ctx.send(f'{user}|{user.id} is not being ignored by leveling.', ephemeral=True)
    ignored_users.remove(user.id)
    await settings.save()
    users = [ctx.guild.get_member(id) for id in ignored_users]
    users = [f'{r}({r.id})' for r in users if r is not None]
    users = ' '.join(users)
    embed = Embed(description=f"Member {user}({user.id}) was removed from members ignored by leveling.")
    embed.add_field('Ignored Members', users)
    await ctx.send(embed=embed, ephemeral=True)

leveling_add_role(ctx, role, role_level) async

/leveling addrole

Description

Create level roles

Parameters:

Name Type Description Default
role OptionType.ROLE

Role

required
role_level str

Role level number, has to be more than 0 and less than 1000

required
Source code in extentions/leveling.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
@leveling.subcommand(sub_cmd_name='addrole', sub_cmd_description="allow's me to create leveling roles")
@role()
@role_level()
async def leveling_add_role(self, ctx:InteractionContext, role: OptionType.ROLE, role_level:str):
    """/leveling addrole
    Description:
        Create level roles

    Args:
        role (OptionType.ROLE): Role
        role_level (str): Role level number, has to be more than 0 and less than 1000
    """
    await ctx.defer()
    if (int(role_level) < 1) or (int(role_level) > 1000):
        await ctx.send('role level has to be more than 0 and less than 1000')
        return

    check = await db.leveling_roles.find_one({'guildid':ctx.guild_id, 'roleid':role.id})
    if check is None:
        await db.leveling_roles(guildid=ctx.guild_id, roleid=role.id, level=int(role_level)).insert()
        await ctx.send(embed=Embed(color=0xffcc50, description=f'Leveling role {role.mention} assigned to level {role_level}'))
    else:
        await ctx.send(embed=Embed(color=0xDD2222, description=f':x: Leveling role {role.mention} is already assigned to level {check.level}'))

    for member in ctx.guild.members:
        mem = await db.leveling.find_one({'guildid':ctx.guild.id, 'memberid':member.id})
        level_roles = db.leveling_roles.find({"guildid":ctx.guild_id, 'level':{'$lte':mem.level}})
        roles = []
        async for role in level_roles:
            roles.append(role.roleid)
        if level_roles != []:
            for role_id in roles:
                role = ctx.guild.get_role(role_id)
                if role not in member.roles:
                    await member.add_role(role)

ranks_list(ctx) async

/ranklist

Description

List leveling roles

Source code in extentions/leveling.py
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
@slash_command(name='ranklist', description="leveling roles list")
async def ranks_list(self, ctx:InteractionContext):
    """/ranklist
    Description:
        List leveling roles
    """
    await ctx.defer()
    from interactions.ext.paginators import Paginator
    def chunks(l, n):
        n = max(1, n)
        return (l[i:i+n] for i in range(0, len(l), n))

    def mlis(lst, s, e):
        nc = list(chunks(lst, 20))
        mc = ''
        for testlist in nc[s:e]:
            for m in testlist:
                mc = mc + m
        return mc

    def newpage(title, level, role):
        embed = Embed(title=title,
        color=0xffcc50)
        embed.add_field(name='Level', value=level, inline=True)
        embed.add_field(name='Role', value=role, inline=True)
        return embed


    levels = db.leveling_roles.find({"guildid":ctx.guild_id})
    level = []
    roles = []
    async for l in levels:
        level.append(f'{l.level}\n')
        lvlrole = find_role(ctx, l.roleid)
        if lvlrole is None:
            roles.append('[ROLE NOT FOUND]\n')
        else:
            roles.append(f"{lvlrole.mention}\n")

    if (level or roles) == []:
        embed = Embed(description=f"There are no ranks for {ctx.guild.name}.",
                    color=0xffcc50)
        await ctx.send(embed=embed)
        return

    s = -1
    e = 0
    embedcount = 1
    nc = list(chunks(level, 20))

    embeds = []
    while embedcount <= len(nc):
        s = s+1
        e = e+1
        embeds.append(newpage(f'List of ranks for {ctx.guild.name}', mlis(level, s, e), mlis(roles, s, e)))
        embedcount = embedcount+1

    paginator = Paginator(
        client=self.bot, 
        pages=embeds,
        timeout_interval=80,
        show_select_menu=False)
    await paginator.send(ctx)

leveling_remove_role(ctx, role=None) async

/leveling removerole

Parameters:

Name Type Description Default
role OptionType.ROLE

Role to remove from leveling.

None
Source code in extentions/leveling.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
@leveling.subcommand(sub_cmd_name='removerole', sub_cmd_description="allow's me to remove leveling roles")
@role()
async def leveling_remove_role(self, ctx:InteractionContext, role: OptionType.ROLE=None):
    """/leveling removerole

    Args:
        role (OptionType.ROLE, optional): Role to remove from leveling.
    """
    await ctx.defer()
    if role is None:
        await ctx.send('you have to include a role')
        return

    check = await db.leveling_roles.find_one({'guildid':ctx.guild.id, 'roleid':role.id})
    if check is None:
        await ctx.send(embed=Embed(color=0xDD2222, description=f':x: Leveling role {role.mention} is not assigned to a level'))
    else:
        await ctx.send(embed=Embed(color=0xffcc50, description=f'Leveling role {role.mention} removed from level {check.level}'))
        await check.delete()

    for member in ctx.guild.members:
        if role in member.roles:
            await member.remove_role(role)

newrank(ctx, member=None) async

/rank

Description

Generates leveling info card

Parameters:

Name Type Description Default
member OptionType.USER

Member, defaults to member executing the command

None
Source code in extentions/leveling.py
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
@slash_command(name='rank', description='check your leveling statistics')
@member()
async def newrank(self, ctx: SlashContext, member:OptionType.USER=None):
    """/rank
    Description:
        Generates leveling info card

    Args:
        member (OptionType.USER, optional): Member, defaults to member executing the command
    """
    await ctx.defer() # defer the response to avoid timeout
    if member is None: # if no member parameter is provided, use the author of the command
        member = ctx.author

    levels = await db.leveling.find_one({'guildid':ctx.guild_id, 'memberid':member.id}) # get the leveling data for the member
    if levels is None: # if no leveling data is found, send a message and return
        await ctx.send("You don't have any xp yet. You can start having conversations with people to gain xp.", ephemeral=True)
        return

    level_stats = await db.levelingStats.find_one({'level':levels.level}) # get the leveling stats for the member's current level
    if (levels.display_name is None) or (levels.display_name != member.display_name): # if the member's display name has changed, update it in the database
        levels.display_name = member.display_name
        await levels.save()

    def getPercent(first, second): # helper function to calculate percentage
        return first / second * 100
    percent = getPercent(levels.xp_to_next_level,level_stats.xp_to_next_level) # calculate the percentage of xp towards the next level
    def findx(percentage): # helper function to calculate the progress bar width
        if percentage == 0:
            return 1
        return 550/(100/percentage)

    if member.guild_avatar is not None: # get the member's avatar url
        avatarurl = f'{member.guild_avatar.url}.png'
    else:
        avatarurl = f'{member.avatar.url}.png'

    def round(im): # helper function to round the member's avatar
        im = im.resize((210*16,210*16), resample=Image.ANTIALIAS)
        mask = Image.new("L", im.size, 0)
        draw = ImageDraw.Draw(mask)
        draw.ellipse((0,0)+im.size, fill=255)
        out = ImageOps.fit(im, mask.size, centering=(0,0))
        out.putalpha(mask)
        image = out.resize((210,210), resample=Image.ANTIALIAS).convert("RGBA")
        return image

    IW, IH = (956, 435) # set the image width and height
    aspect_ratio = IW/IH

    if levels.lc_background is not None: # get the background image for the level card
        try:
            background = Image.open(requests.get(f'{levels.lc_background}', stream=True).raw).crop((0,0,IW,IH)).convert("RGBA")
        except:
            background = Image.open(requests.get('https://i.imgur.com/4yzKbQo.png', stream=True).raw).convert("RGBA")
    else:
        background = Image.open(requests.get('https://i.imgur.com/4yzKbQo.png', stream=True).raw).convert("RGBA")

    overlay = Image.open(requests.get('https://i.imgur.com/fsuIzSv.png', stream=True).raw).convert("RGBA") # add an overlay to the background
    background.paste(overlay, (0, 0), overlay)

    try:
        pfp = Image.open(requests.get(avatarurl, stream=True).raw).resize((230,230)).convert("RGBA") # get the member's avatar image
    except:
        pfp = Image.open(requests.get('https://cdn.discordapp.com/embed/avatars/1.png', stream=True).raw).resize((230,230)).convert("RGBA")
    pfp = round(pfp) # round the member's avatar
    background.paste(pfp, (78, 115), pfp) # paste the avatar onto the background

    def draw_progress_bar(fx): # helper function to draw the progress bar
        rad = 115
        im = Image.open(requests.get('https://i.imgur.com/sRseF8Y.png', stream=True).raw).convert('RGBA')
        im = im.resize((fx*16,30*16), resample=Image.ANTIALIAS)
        circle = Image.new('L', (rad * 2, rad * 2), 0)
        draw = ImageDraw.Draw(circle)
        draw.ellipse((0, 0, rad * 2, rad * 2), fill=255)
        alpha = Image.new('L', im.size, 255)
        w, h = im.size
        alpha.paste(circle.crop((0, 0, rad, rad)), (0, 0))
        alpha.paste(circle.crop((0, rad, rad, rad * 2)), (0, h - rad))
        alpha.paste(circle.crop((rad, 0, rad * 2, rad)), (w - rad, 0))
        alpha.paste(circle.crop((rad, rad, rad * 2, rad * 2)), (w - rad, h - rad))
        im.putalpha(alpha)
        im = im.resize((fx,30), resample=Image.ANTIALIAS)
        return im

    fx = findx(int(percent)) # calculate the progress bar width
    progress_bar = draw_progress_bar(int(fx)) # draw the progress bar
    background.paste(progress_bar, (330, 370), progress_bar) # paste the progress bar onto the background

    def rectangle_fill(im): # helper function to fill the background with a rectangle
        mask = Image.new("L", im.size, 0)
        draw = ImageDraw.Draw(mask)
        draw.rectangle((0,0)+im.size, fill=255)
        out = ImageOps.fit(im, mask.size, centering=(0,0))
        out.putalpha(mask)
        return out

    background = rectangle_fill(background) # fill the background with a rectangle

    font = ImageFont.truetype('Everson-Mono-Bold.ttf', 45) # set the font for the text

    I1 = ImageDraw.Draw(background)

    lvlfont = ImageFont.truetype('Everson-Mono-Bold.ttf', 45)
    I1.text((73,352), f'LVL:{levels.level}', font=lvlfont, stroke_width=2, stroke_fill=(30, 27, 26), fill=(255, 255, 255)) # add the member's level to the level card

    lvlmsg = f'XP: {levels.xp_to_next_level}/{level_stats.xp_to_next_level}\nTotal XP: {levels.total_xp}\nMessages: {levels.messages}' # create the level message
    I1.text((341,110), lvlmsg, font=font, stroke_width=2, stroke_fill=(30, 27, 26), fill=(255, 255, 255)) # add the level message to the level card

    namefont = ImageFont.truetype('Everson-Mono-Bold.ttf', 50)
    name = f'{member.display_name}'
    if len(name) > 27: # shorten the member's name if it is too long
        name = name[:-4]

    I1.text((73,28), name, font=namefont, stroke_width=2, stroke_fill=(30, 27, 26), fill=(255, 255, 255)) # add the member's name to the level card
    background.save(f'levelcard_{member.id}.png') # save the level card as a file
    await ctx.send(file=f"levelcard_{member.id}.png") # send the level card as a file
    os.remove(f'levelcard_{member.id}.png') # remove the level card file

leaderboard(ctx) async

/leaderboard

Description

Sends a button to the leaderboard for the server the command is executed in.

Source code in extentions/leveling.py
536
537
538
539
540
541
542
543
544
545
546
547
@slash_command(name='leaderboard', description='check the servers leveling leaderboard')
async def leaderboard(self, ctx: InteractionContext):
    """/leaderboard
    Description:
        Sends a button to the leaderboard for the server the command is executed in.
    """
    components = Button(
        style=ButtonStyle.URL,
        label="Click Me!",
        url=f"https://www.beni2am.space/melody/leaderboard/{ctx.guild_id}/",
    )
    await ctx.send("A button to the web leaderboard!", components=components)