Exemple #1
0
    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)
Exemple #2
0
    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)
Exemple #3
0
    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
Exemple #4
0
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]
Exemple #5
0
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)
Exemple #6
0
    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)
Exemple #7
0
    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)
Exemple #8
0
    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)
Exemple #10
0
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():