0

I've been coding a discord bot for a while now and I wanted to make a voice activity tracking.

The problem is, there's almost no documentation about that on the net. I used this as base but I changed a lot of code in it to make it "per server". I have at least 2 issues.

The 1st one is because of datetime, for exemple, if a user join a voice channel at 11:45 and leave it at 00:45, the result will be

"guild_id": {
    "user_id": "-1 day, ..."
}

because the operation is 11 - (~25).

My seconde issue is also because of datetime,

if the time while a user as been in a vocal is more than 24hours, i'll get this kind of error:

click to see the error

Here's my code :

    @commands.Cog.listener()
    async def on_voice_state_update(self, member, before, after):
        with open('data/voice_leaderboard.json', 'r') as file:
            voice_data = json.load(file)
            new_user = str(member.id)
            guild_id = str(member.guild.id)
            
        # Update existing user
        if new_user in voice_data[guild_id]:
            voice_leave_time = datetime.datetime.now().time().strftime('%H:%M:%S')
            voice_join_time = voice_data[guild_id][new_user]

            calculate_time = (datetime.datetime.strptime(voice_leave_time, '%H:%M:%S') - datetime.datetime.strptime(voice_join_time, '%H:%M:%S'))

            voice_data[guild_id][new_user] = str(calculate_time)

            with open('data/voice_leaderboard.json', 'w') as update_user_data:
                json.dump(voice_data, update_user_data, indent=4)

        # Add new user
        else:
            if member.bot:
                return
            else:
                new_voice_join_time = datetime.datetime.utcnow().strftime('%H:%M:%S')
                voice_data[guild_id][new_user] = new_voice_join_time

            with open('data/voice_leaderboard.json', 'w') as new_user_data:
                json.dump(voice_data, new_user_data, indent=4)

and here's a part of the json file :

{
    "749948728248631297": {
        "437265873401544705": "0:06:49"
    },

I don't have any idea of how I could fix those 2 issues exept maybe by using discord.js but that's not what I want so if anyone have any idea of how I can do, please help me

gwendal
  • 17
  • 3
  • 2
    Please limit your questions to a single, answerable inquiry per [ask]. Questions asking two or more distinct questions are considered too broad and are not good fits for the Stack Overflow Q&A format. – esqew Aug 18 '21 at 20:46

2 Answers2

1

Both of the problems are occurring because you store your time data as time only, without including dates.

Including dates would fix the first problem because it now knows that both timestamps are on different dates. In addition, the second problem would be fixed as the timestamp wouldn't exceed 24 hours but instead add another day.

Adding %d/%m/%Y to all of your strptimes and strftimes should fix the problem.

Doing that, your code would look like this:

dateFormat = "%d/%m/%Y %H:%M:%S"
    @commands.Cog.listener()
    async def on_voice_state_update(self, member, before, after):
        with open('data/voice_leaderboard.json', 'r') as file:
            voice_data = json.load(file)
            new_user = str(member.id)
            guild_id = str(member.guild.id)
            
        # Update existing user
        if new_user in voice_data[guild_id]:
            voice_leave_time = datetime.datetime.time().strftime(dateFormat)
            voice_join_time = voice_data[guild_id][new_user]

            calculate_time = (datetime.datetime.strptime(voice_leave_time, dateFormat) - datetime.datetime.strptime(voice_join_time, dateFormat))

            voice_data[guild_id][new_user] = str(calculate_time)

            with open('data/voice_leaderboard.json', 'w') as update_user_data:
                json.dump(voice_data, update_user_data, indent=4)

        # Add new user
        else:
            if member.bot:
                return
            else:
                new_voice_join_time = datetime.datetime.utcnow().strftime(dateFormat)
                voice_data[guild_id][new_user] = new_voice_join_time

            with open('data/voice_leaderboard.json', 'w') as new_user_data:
                json.dump(voice_data, new_user_data, indent=4)
Axiumin_
  • 2,107
  • 2
  • 15
  • 24
0

Both of your issues come from the fact that you store the time in an hour:minute:seconds format, which is completely unnecessary in your use case, as you only need the relative time. To solve your issues you could just use time.time() which gives the time since epoch (maybe round() it to make it nicer to look at). When an user joins the vc you record the joining-time.time(), when they leave you take the leaving-time.time(). The delta between those two is how long they've been in the vc, you can convert that delta to a readable format in few different ways.

Aside from that your program seems to have a few more fundamental problems like:

  • A new user-key(in your dict) only gets created when they join a vc for the very first time as you save the time they spent in the vc under that key after they leave(this leads to the error in the other answer). As you're making a leaderboard I'm assuming you want to keep the total of time spent in a vc, so to solve it there's two options:

    • Make the userid key save a dict, with at least two keys (join time and total time), when the user leaves the channel you check the join time voice_data[guild_id][user]['jointime'] and add the time they were in a vc to voice_data[guild_id][user]['totaltime']
    • Make two dictionaries, one for saving the total time they were in a vc and the other one for saving the join time, doing this is possible, but I don't advice doing it as its gonna make you do way more file operations than option 1.
  • This adds on problem 1, you dont check the type of voice_channel event, and as according to the docs the event gets triggered on: A member joins a vc, A member leaves a vc, A member is muted or deafened by their own accord, A member is muted or deafened by a guild administrator, etc. As you just run the code every time it gets triggered this will make your code completely mess up if something other than 'user joins one vc' 'user leaves that vc' happens (like joining a different vc in the same server from that vc, or a deafen/mute), you should apply some logic that checks the before and after (that the event gives you). They are VoiceStates, you could look at the channel attribute of that class to check what exactly happened before doing your time function. (you would also have to save the channelid in the dictionary I suggested in problem 1.

I know you didn't ask for me to make your code, but it was easier for me to just make it than to give hints on how to do it, sorry. Only note that I completely wrote this up on the fly so you might get syntax errors or errors I didnt expect.

    @commands.Cog.listener()
async def on_voice_state_update(self, member, before, after):
    if member.bot: #checking this before anything else will reduce unneeded file operations etc
        return
    with open('data/voice_leaderboard.json', 'r') as file: 
        voice_data = json.load(file)
    new_user = str(member.id)
    guild_id = str(member.guild.id)
    if new_user not in voice_data[guild_id]: #this adds a new user to the guild dict if they're not in it yet
        voice_data[guild_id][new_user] = {
            "total_time" : 0,
            "join_time" : None} 
    userdata = voice_data[guild_id][new_user] #this is to make the next code clearer, adding things to this dict also adds them to the voice_data dict, it just make the code "cleaner"

    #after making sure the user exists you gotta check if they're joining or leaving a vc(and reject all the other options), plus if they change vc within the same guild it should keep counting. There's multiple ways to do this
    if(before.channel == None): #this is when they join a vc (they werent in one before so they gotta have just joined one)
        join_time = round(time.time())
        user_data["join_time"] = join_time
    elif(before.channel.guild == after.channel.guild): #wrote this to only check if they changed vc within the same guild, but then I realised it can also catch all the mute/deafen events yay.
        break
    elif(str(after.channel.guild.id) != guild_id): #this will check if the channel they're in after the event (we wanna record the time passed if its None or a different guild, both of which will get triggered by this)
        if(userdata["join_time"] == None): break #this will catch errors, if they were to happen
        leave_time = time.time()
        passed_time = leave_time - userdata["join_time"]
        userdata[total_time] += passed_time
        userdata["join_time"] = None #preventive measure against errors
    with open('data/voice_leaderboard.json', 'w') as update_user_data:
        json.dump(voice_data, update_user_data, indent=4)
      

On a last note, file operations are blocking, if you're gonna be doing a lot of them you should look into making them asynchronous too (I solved that problem by not doing file operation with my bot, making the dicts of it global and only saving them on exit)

FierySpectre
  • 642
  • 3
  • 9
  • Hello ! I understood your answer but I'm pretty sure I'm not able to do it alone. I don't want you to make it for me but i'd like a little bit more help please – gwendal Aug 21 '21 at 03:19
  • Hi, I added some code to my answer (i kinda just made it, sorry if you wanted just a few hints) – FierySpectre Aug 21 '21 at 08:46
  • I got one last error when I disconnect: elif(before.channel.guild == after.channel.guild): AttributeError: 'NoneType' object has no attribute 'guild' – gwendal Aug 22 '21 at 20:06