def handle_in_session(self, user_id, reset): """ When a user issues commands, we want to show up-to-date info even if there is no "voice_state_update" """ # after data recovery we should have a sensible start channel record last_record = self.get_last_record(user_id, ["start channel"]) cur_time = utilities.get_time() last_record_time = last_record.creation_time if last_record else cur_time rank_categories = utilities.get_rank_categories(string=False) rank_categories_val = list(rank_categories.values()) string_rank_categories = list( utilities.get_rank_categories(string=True).values()) in_session_names = [ "in_session_" + str(in_session) for in_session in string_rank_categories[0] ] category_key_names = string_rank_categories[ 0] + string_rank_categories[1:] in_session_incrs = [] for in_session, in_session_name in zip(rank_categories_val[0], in_session_names): in_session_time = self.redis_client.hget(in_session_name, user_id) in_session_time = float(in_session_time) if in_session_time else 0 base_time = max(last_record_time, in_session) incr = utilities.timedelta_to_hours(cur_time - base_time) - in_session_time # Max necessary since an enter channel (or other voice status change) update/sync might be called earlier than the exit one incr = max(incr, 0) in_session_incrs.append(incr) new_val = 0 if reset else incr + in_session_time self.redis_client.hset(in_session_name, user_id, new_val) # standard incr is what gets used for monthly and weekly. In other words, official incr is one of the sets of stats in_session_std_time_name = f"in_session_std" in_session_std_time = self.redis_client.hget(in_session_std_time_name, user_id) in_session_std_time = float( in_session_std_time) if in_session_std_time else 0 std_incr = utilities.timedelta_to_hours( cur_time - last_record_time) - in_session_std_time neg_msg = f"std_incr Negative: {std_incr}\n" if std_incr < 0 else "" std_incr = max(std_incr, 0) in_session_std_time = 0 if reset else std_incr + in_session_std_time self.redis_client.hset(in_session_std_time_name, user_id, in_session_std_time) monthly_now, all_time_now = utilities.increment_studytime( category_key_names, self.redis_client, user_id, in_session_incrs=in_session_incrs, std_incr=std_incr) log_msg = f'{utilities.get_time()}\n{neg_msg}monthly_now: {monthly_now}\nall_time_now: {all_time_now}\nincr: {std_incr}\ncur_time: {cur_time}\nlast_record_time: {last_record_time}\npast_in_session_time: {in_session_std_time}\nuser_id: {user_id}' self.data_change_logger.info(log_msg)
def handle_in_session(self, user_id, reset): # after data recovery we should have a sensible start channel record last_record = self.get_last_record(user_id, ["start channel"]) cur_time = utilities.get_time() last_record_time = last_record.creation_time if last_record else cur_time rank_categories = utilities.get_rank_categories(string=False) rank_categories_val = list(rank_categories.values()) string_rank_categories = list( utilities.get_rank_categories(string=True).values()) in_session_names = [ "in_session_" + str(in_session) for in_session in string_rank_categories[0] ] category_key_names = string_rank_categories[ 0] + string_rank_categories[1:] in_session_incrs = [] std_incr = None # TODO optimize: most values here will be the same and this is the bottleneck for in_session, in_session_name in zip(rank_categories_val[0], in_session_names): past_in_session_time = self.redis_client.hget( in_session_name, user_id) past_in_session_time = float( past_in_session_time) if past_in_session_time else 0 base_time = max(last_record_time, in_session) incr = utilities.timedelta_to_hours( cur_time - base_time) - past_in_session_time if in_session_name[-8:] == str( utilities.config["business"]["update_time"]) + ":00:00": std_incr = incr prev_std_incr_name = "daily_" + str(in_session - timedelta(days=1)) prev_std_incr = self.redis_client.hget( "in_session_" + prev_std_incr_name, user_id) prev_std_incr = float(prev_std_incr) if prev_std_incr else 0 if prev_std_incr: std_incr += prev_std_incr self.redis_client.hset("in_session_" + prev_std_incr_name, user_id, 0) in_session_incrs.append(incr) new_val = 0 if reset else incr + past_in_session_time self.redis_client.hset(in_session_name, user_id, new_val) utilities.increment_studytime(category_key_names, self.redis_client, user_id, in_session_incrs=in_session_incrs, std_incr=std_incr)
async def update_roles(self, user: discord.Member): user_id = user.id rank_categories = utilities.get_rank_categories() hours_cur_month = await utilities.get_redis_score( self.redis_client, rank_categories["monthly"], user_id) if not hours_cur_month: hours_cur_month = 0 pre_role, cur_role, next_role, time_to_next_role = utilities.get_role_status( self.role_name_to_info, hours_cur_month) # not fetching the actual role to save an api call role_to_add_id = int(cur_role["mention"][3:-1]) if cur_role else None roles_to_remove = { role_obj for role_name, role_obj in self.role_name_to_obj.items() if role_name in utilities.role_names } user_roles = user.roles roles_to_remove = { role for role in user_roles if role in roles_to_remove and role.id != role_to_add_id } if roles_to_remove: await user.remove_roles(*roles_to_remove, atomic=False) if cur_role and cur_role["mention"]: # assuming the mention format will stay the same role_to_add = discord.utils.get(user.guild.roles, id=role_to_add_id) if role_to_add not in user_roles: await user.add_roles(role_to_add, atomic=False) return cur_role, next_role, time_to_next_role
async def main(): google_client = gaio.AsyncioGspreadClientManager(get_creds) sheets = await get_sheet(google_client) sheet = sheets[0] sheet2 = sheets[1] names = utilities.get_rank_categories(flatten=True) print(names) all_time = pair_data(sheet.range("J2:K" + str(sheet.row_count)), 2, "all_time") df_all_time = pd.DataFrame(all_time[1:], columns=all_time[0]) monthly = pair_data(sheet.range("C2:D" + str(sheet.row_count)), 2, "monthly") df_monthly = pd.DataFrame(monthly[1:], columns=monthly[0]) weekly = pair_data(sheet.range("Q2:R" + str(sheet.row_count)), 2, "weekly") df_weekly = pd.DataFrame(weekly[1:], columns=weekly[0]) daily = pair_data(sheet.range("X2:Y" + str(sheet.row_count)), 2, "daily") df_daily = pd.DataFrame(daily[1:], columns=daily[0]) streaks = pair_data(sheet2.range("A3:C" + str(sheet2.row_count)), 3, "current_streak", "longest_streak") df_streaks = pd.DataFrame(streaks[1:], columns=streaks[0]) return [df_all_time, df_monthly, df_weekly, df_daily, df_streaks]
def insert_sorted_set(): filter_time_fn_li = [utilities.get_day_start, utilities.get_week_start, utilities.get_month_start, utilities.get_earliest_start] category_key_names = utilities.get_rank_categories(flatten=True) for (category_key_name, sorted_set_name), filter_time_fn in zip(category_key_names.items(), filter_time_fn_li): if category_key_name not in dictionary: print(f"{category_key_name} missing") continue to_insert = dictionary[category_key_name] # TODO handle it smarter in fetch_all for k, v in to_insert.items(): if type(v) != int and type(v) != float: to_insert[k] = locale.atoi(v) to_insert[k] /= 60 redis_client.zadd(sorted_set_name, to_insert)
async def p(self, ctx, user: discord.Member = None): """ Displays your role placement for this month (use '~help p' to see more) examples: '~p' To specify a user examples: '~p @chooseyourfriend' """ # if the user has not specified someone else if not user: user = ctx.author await self.update_stats(ctx, user) name = f"{user.name} #{user.discriminator}" user_id = user.id rank_categories = utilities.get_rank_categories() hours_cur_month = await utilities.get_redis_score( self.redis_client, rank_categories["monthly"], user_id) if not hours_cur_month: hours_cur_month = 0 role, next_role, time_to_next_role = utilities.get_role_status( self.role_name_to_obj, hours_cur_month) # TODO update user roles text = f""" **User:** ``{name}``\n __Study role__ ({utilities.get_time().strftime("%B")}) **Current study role:** {role["mention"] if role else "No Role"} **Next study role:** {next_role["mention"] if next_role else "``👑 Highest rank reached``"} **Role rank:** ``{'👑 ' if role and utilities.role_names.index(role["name"]) + 1 == {len(utilities.role_settings)} else ''}{utilities.role_names.index(role["name"]) + 1 if role else '0'}/{len(utilities.role_settings)}`` """ if time_to_next_role: text += f"**Role promotion in:** ``{(str(time_to_next_role) + 'h')}``" emb = discord.Embed(title=utilities.config["embed_titles"]["p"], description=text) await ctx.send(embed=emb)
async def me(self, ctx, timepoint=None, user: discord.Member = None): """ Displays statistics for your studytime (use '~help me' to see more) By default the daily time is last 24 hours, but you can specify a start time (in the last 24 hours) Currently, the available starting points are hours. If we include half past hours, '~me 10:14' will become '~me 10:30' To specify a starting time, use any of the following formats "%H:%M", "%H:%m", "%h:%M", "%h:%m", "%H", "%h" examples: '~me 9' or '~me 9pm' To specify a user, use examples: '~me 9 @chooseyourfriend' or '~me - @chooseyourfriend' Note the weekly time resets on Monday GMT+0 5pm and the monthly time 1st day of the month 5pm """ """ # Regarding timezone # user input on command, input on DB: use user time to get UTC time & display user time # user input on command, not input on DB: use UTC time - prompt to input timezone # no user input on command, input on DB: past 24 hours - display user time # no user input on command, no input on DB: past 24 hours - prompt to input timezone """ if not user: user = ctx.author user_id = user.id await self.update_stats(user) timepoint, display_timezone, display_timepoint = await utilities.get_user_timeinfo( ctx, user, timepoint) rank_categories = utilities.get_rank_categories() name = user.name + "#" + user.discriminator user_sql_obj = self.sqlalchemy_session.query(User).filter( User.id == user_id).first() stats = await utilities.get_user_stats(self.redis_client, user_id, timepoint=timepoint) average_per_day = utilities.round_num( stats[rank_categories["monthly"]]["study_time"] / utilities.get_num_days_this_month()) currentStreak = user_sql_obj.current_streak if user_sql_obj else 0 longestStreak = user_sql_obj.longest_streak if user_sql_obj else 0 currentStreak = str(currentStreak) + " day" + ( "s" if currentStreak != 1 else "") longestStreak = str(longestStreak) + " day" + ( "s" if longestStreak != 1 else "") num_dec = int( os.getenv(("test_" if os.getenv("mode") == "test" else "") + "display_num_decimal")) width = 5 + num_dec text = f""" ```css {utilities.config["embed_titles"]["me"]}``` ```glsl Timeframe {" " * (num_dec - 1)}Hours Place Daily: {stats[timepoint]["study_time"]:{width}.{num_dec}f}h #{stats[str(timepoint)]["rank"]} Weekly: {stats[rank_categories["weekly"]]["study_time"]:{width}.{num_dec}f}h #{stats[rank_categories["weekly"]]["rank"]} Monthly: {stats[rank_categories["monthly"]]["study_time"]:{width}.{num_dec}f}h #{stats[rank_categories["monthly"]]["rank"]} All-time: {stats[rank_categories["all_time"]]["study_time"]:{width}.{num_dec}f}h #{stats[rank_categories["all_time"]]["rank"]} Average/day ({utilities.get_month()}): {average_per_day} h Current study streak: {currentStreak} Longest study streak: {longestStreak} ``` """ emb = discord.Embed(description=text) foot = name # Add Fancy decoration for supporter_role # user.roles is a list if self.supporter_role in [role.id for role in user.roles]: foot = "⭐ " + foot emb.set_footer(text=foot, icon_url=user.avatar_url) await ctx.send( f"**Daily starts tracking at {display_timezone} {display_timepoint}**" ) await ctx.send(embed=emb) await ctx.send( f"**Visit <https://app.studytogether.com/users/{user_id}> for more details.**" ) await self.update_roles(user)
async def lb(self, ctx, timepoint=None, page: int = -1, user: discord.Member = None): """ Displays statistics for people with similar studytime (use '~help lb' to see more) By default the ranking is monthly, you can specify a start time (in the last 24 hours). Currently, the available starting points are hours. If we include half past hours, '~lb 10:14' will become '~lb 10:30' To specify a starting time, use any of the following formats "%H:%M", "%H:%m", "%h:%M", "%h:%m", "%H", "%h" examples: '~lb 9' or '~lb 9pm' To specify a page, specify the page number where each page has 10 members; use '-' as a placeholder to get monthly ranking examples: '~lb 9 2' or '~lb - 3' To specify a time and a user, use '-1' as a placeholder for page examples: '~lb 9 -1 @chooseyourfriend' To specify a user, also use '-' as a placeholder to get monthly ranking examples: '~lb - -1 @chooseyourfriend' Note the weekly time resets on Monday GMT+0 5pm and the monthly time 1st day of the month 5pm """ # TODO implement all-time text = "" # if the user has not specified someone else if not user: user = ctx.author await self.update_stats(user) if timepoint and timepoint != "-": timepoint, display_timezone, display_timepoint = await utilities.get_user_timeinfo( ctx, user, timepoint) text = f"(From {display_timezone} {display_timepoint})\n" # No timepoint or using placeholder else: timepoint = utilities.get_rank_categories()["monthly"] # No timepoint or using placeholder if not page or page == -1: user_id = user.id leaderboard = await self.get_neighbor_stats(timepoint, user_id) else: if page < 1: await ctx.send("Invalid page number.") return end = page * 10 start = end - 10 leaderboard = await self.get_info_from_leaderboard( timepoint, start, end) num_dec = int( os.getenv(("test_" if os.getenv("mode") == "test" else "") + "display_num_decimal")) width = 5 + num_dec for person in leaderboard: name = (await self.get_discord_name(person["discord_user_id"]))[:40] style = "**" if user and person[ "discord_user_id"] == user.id else "" text += f'`{(person["rank"] or 0):>5}.` {style}{person["study_time"]:{width}.{num_dec}f} h {name}{style}\n' lb_embed = discord.Embed( title= f'{utilities.config["embed_titles"]["lb"]} ({utilities.get_month()})', description=text) lb_embed.set_footer( text=f"Type ~help lb to see how to go to other pages") await ctx.send(embed=lb_embed) await self.update_roles(user)
async def on_ready(self): # called after the bot initializes # fetch initial api info await self.fetch() # start updating the roles print("Updating roles...", flush=True) # get the list of users from sql users = self.sqlalchemy_session.query(User).all() # get the users monthly hours from redis monthly_session_name = utilities.get_rank_categories()["monthly"] users_monthly_hours = self.redis_client.zrange(monthly_session_name, 0, -1, withscores=True) # create a dict of users with their monthly study hours set to 0 user_dict = {user.id: 0 for user in users} # update each user's hours based on redis for user_monthly_hours in users_monthly_hours: user_dict[user_monthly_hours[0]] = user_monthly_hours[1] # turn the dictionary into a list of [user_id, hours_studied_this_month] user_list = [] for key, value in user_dict.items(): user_list.append([key, value]) # # write the user_list to a file for debugging # with open("onlyUpdatedTest.json", "w") as f: # f.write(json.dumps(user_list)) # get the roles and reverse them roles = list(self.role_names.values()) roles.reverse() # task for processing the user_list and updating each users roles accordingly def the_task(self, user_list, roles): count = 0 countAddedRoles = 0 countRemovedRoles = 0 toUpdate = { } # {discord.Member: {"add": [discord.Role], "remove": [discord.Role]} } # for each user in the list for user in user_list: count += 1 # if the number of entries isn't two, then there is an error if len(user) != 2: print("Invalid user tuple in user_list") continue # get the member from the discord api by id m = self.client.get_guild(utilities.get_guildID()).get_member( int(user[0])) # if user doesn't exist (potentially they left the server), continue if not m: continue # get the user's hours from redis hours = user[1] # for each role, # remove roles that the user should no longer hold # add roles that the user now holds for r in roles: # min_ and max_ are the bounds for the role of interest min_ = float(r["hours"].split("-")[0]) max_ = float(r["hours"].split("-")[1]) if min_ <= hours < max_ or (hours >= 350 and r["id"] == 676158518956654612): if not m.guild.get_role(r["id"]) in m.roles: # if user hours are inside the bounds for this role, and the user doesn't already have this role # store that the role should be added to this user in the `toUpdate` object if m not in toUpdate: toUpdate[m] = {"add": [], "remove": []} toUpdate[m]["add"].append(m.guild.get_role( r["id"])) countAddedRoles += 1 else: if m.guild.get_role(r["id"]) in m.roles: # if user hours are outside the bounds for this role, and the user has this role # store that the role should be removed from this user in the `toUpdate` object if m not in toUpdate: toUpdate[m] = {"add": [], "remove": []} toUpdate[m]["remove"].append( m.guild.get_role(r["id"]) ) # await m.remove_roles(m.guild.get_role(r["id"])) countRemovedRoles += 1 # return the dict storing role update information return toUpdate try: # try updating each users roles print("Starting processing", flush=True) func = partial(the_task, self, user_list, roles) # get the update dictionary toUpdate = await self.client.loop.run_in_executor(None, func) count = 0 numPendingUpdates = len(toUpdate) # apply the updates # this can take a while because of api rate limiting for (k, v) in toUpdate.items(): if k is not None: print( f"{count} / {numPendingUpdates}. Updating roles of: " + k.name, flush=True) print(v, flush=True) await k.add_roles(*v["add"], reason="New rank") await k.remove_roles(*v["remove"], reason="New rank") else: print("Bug member is none") count += 1 # print(f"Added {len(v['add'])} roles and removed {len(v['remove'])} roles.") print("FINISHED", len(toUpdate), flush=True) except Exception as e: print("Error", flush=True) print(e, flush=True)
from sqlalchemy.orm import sessionmaker import utilities from models import * load_dotenv("dev.env") database_name = os.getenv("database") engine = utilities.get_engine() Session = sessionmaker(bind=engine) sqlalchemy_session = Session() redis_client = utilities.get_redis_client() df = pd.read_csv("user_files/user_stats.csv", index_col="id") df = df[~df.index.duplicated(keep='first')] df.fillna(0, inplace=True) daily_name = utilities.get_rank_categories(flatten=True)["daily"] # df[daily_name] = 0 df["current_streak"] = df["current_streak"].astype(int) df["longest_streak"] = df["longest_streak"].astype(int) dictionary = df.to_dict() def insert_df(): user_df = df[["current_streak", "longest_streak"]] user_df["id"] = user_df.index.astype(int) user_df.to_sql('user', con=engine, if_exists="append", index=False) sqlalchemy_session.commit() def insert_sorted_set():