예제 #1
0
async def fetch_geoloc_web(ip: IPAddress) -> Optional[Geolocation]:
    """Fetch geolocation data based on ip (using ip-api)."""
    url = f"http://ip-api.com/line/{ip}"

    async with http.get(url) as resp:
        if not resp or resp.status != 200:
            log("Failed to get geoloc data: request failed.", Ansi.LRED)
            return

        status, *lines = (await resp.text()).split("\n")

        if status != "success":
            err_msg = lines[0]
            if err_msg == "invalid query":
                err_msg += f" ({url})"

            log(f"Failed to get geoloc data: {err_msg}.", Ansi.LRED)
            return

    acronym = lines[1].lower()

    return {
        "latitude": float(lines[6]),
        "longitude": float(lines[7]),
        "country": {
            "acronym": acronym,
            "numeric": country_codes[acronym],
        },
    }
예제 #2
0
async def donor_expiry() -> list[Coroutine]:
    """Add new donation ranks & enqueue tasks to remove current ones."""
    # TODO: this system can get quite a bit better; rather than just
    # removing, it should rather update with the new perks (potentially
    # a different tier, enqueued after their current perks).

    async def rm_donor(userid: int, when: int):
        if (delta := when - time.time()) >= 0:
            await asyncio.sleep(delta)

        p = await glob.players.get_ensure(id=userid)

        # TODO: perhaps make a `revoke_donor` method?
        await p.remove_privs(Privileges.Donator)
        await glob.db.execute(
            'UPDATE users '
            'SET donor_end = 0 '
            'WHERE id = %s',
            [p.id]
        )

        if p.online:
            p.enqueue(packets.notification('Your supporter status has expired.'))

        log(f"{p}'s supporter status has expired.", Ansi.LMAGENTA)
예제 #3
0
파일: utils.py 프로젝트: Varkaria/gulag
def ensure_local_services_are_running() -> int:
    """Ensure all required services (mysql, redis) are running."""
    # NOTE: if you have any problems with this, please contact me
    # @cmyui#0425/[email protected]. i'm interested in knowing
    # how people are using the software so that i can keep it
    # in mind while developing new features & refactoring.

    if settings.DB_DSN.hostname in ("localhost", "127.0.0.1", None):
        # sql server running locally, make sure it's running
        for service in ("mysqld", "mariadb"):
            if os.path.exists(f"/var/run/{service}/{service}.pid"):
                break
        else:
            # not found, try pgrep
            pgrep_exit_code = subprocess.call(
                ["pgrep", "mysqld"],
                stdout=subprocess.DEVNULL,
            )
            if pgrep_exit_code != 0:
                log("Unable to connect to mysql server.", Ansi.LRED)
                return 1

    if not os.path.exists("/var/run/redis/redis-server.pid"):
        log("Unable to connect to redis server.", Ansi.LRED)
        return 1

    return 0
예제 #4
0
파일: utils.py 프로젝트: Varkaria/gulag
def _download_achievement_images_osu(achievements_path: Path) -> bool:
    """Download all used achievement images (one by one, from osu!)."""
    achs: list[str] = []

    for res in ("", "@2x"):
        for gm in ("osu", "taiko", "fruits", "mania"):
            # only osu!std has 9 & 10 star pass/fc medals.
            for n in range(1, 1 + (10 if gm == "osu" else 8)):
                achs.append(f"{gm}-skill-pass-{n}{res}.png")
                achs.append(f"{gm}-skill-fc-{n}{res}.png")

        for n in (500, 750, 1000, 2000):
            achs.append(f"osu-combo-{n}{res}.png")

    log("Downloading achievement images from osu!.", Ansi.LCYAN)

    for ach in achs:
        resp = requests.get(f"https://assets.ppy.sh/medals/client/{ach}")
        if resp.status_code != 200:
            return False

        log(f"Saving achievement: {ach}", Ansi.LCYAN)
        (achievements_path / ach).write_bytes(resp.content)

    return True
예제 #5
0
파일: utils.py 프로젝트: Varkaria/gulag
    def _excepthook(
        type_: Type[BaseException],
        value: BaseException,
        traceback: types.TracebackType,
    ):
        if type_ is KeyboardInterrupt:
            print("\33[2K\r", end="Aborted startup.")
            return
        elif type_ is AttributeError and value.args[0].startswith(
                "module 'config' has no attribute", ):
            attr_name = value.args[0][34:-1]
            log(
                "gulag's config has been updated, and has "
                f"added a new `{attr_name}` attribute.",
                Ansi.LMAGENTA,
            )
            log(
                "Please refer to it's value & example in "
                "ext/config.sample.py for additional info.",
                Ansi.LCYAN,
            )
            return

        printc(
            f"gulag v{settings.VERSION} ran into an issue before starting up :(",
            Ansi.RED,
        )
        real_excepthook(type_, value, traceback)  # type: ignore
예제 #6
0
파일: init_api.py 프로젝트: Varkaria/gulag
    async def on_startup() -> None:
        app.state.loop = asyncio.get_running_loop()

        if os.geteuid() == 0:
            log(
                "Running the server with root privileges is not recommended.",
                Ansi.LRED,
            )

        app.state.services.http = aiohttp.ClientSession(
            json_serialize=lambda x: orjson.dumps(x).decode(), )
        await app.state.services.database.connect()
        await app.state.services.redis.initialize()

        if app.state.services.datadog is not None:
            app.state.services.datadog.start(
                flush_in_thread=True,
                flush_interval=15,
            )
            app.state.services.datadog.gauge("gulag.online_players", 0)

        await app.state.services.run_sql_migrations()

        async with app.state.services.database.connection() as db_conn:
            await collections.initialize_ram_caches(db_conn)

        await app.bg_loops.initialize_housekeeping_tasks()
예제 #7
0
def point_of_interest():
    """Leave a pseudo-breakpoint somewhere to ask the user if
       they could pls submit their stacktrace to cmyui <3."""

    # TODO: fix this, circular import thing
    #ver_str = f'Running gulag v{glob.version!r} | cmyui_pkg v{cmyui.__version__}'
    #printc(ver_str, Ansi.LBLUE)

    for fi in inspect.stack()[1:]:
        if fi.function == '_run':
            # go all the way up to server start func
            break

        file = Path(fi.filename)

        # print line num, index, func name & locals for each frame.
        log('[{function}() @ {fname} L{lineno}:{index}] {frame.f_locals}'.
            format(**fi._asdict(), fname=file.name))

    msg_str = '\n'.join((
        "Hey! If you're seeing this, osu! just did something pretty strange,",
        "and the gulag devs have left a breakpoint here. We'd really appreciate ",
        "if you could screenshot the data above, and send it to cmyui, either via ",
        "Discord (cmyui#0425), or by email ([email protected]). Thanks! 😳😳😳"
    ))

    printc(msg_str, Ansi.LRED)
    input('To close this menu & unfreeze, simply hit the enter key.')
예제 #8
0
    async def maps_from_sql(self, db_cursor: aiomysql.DictCursor) -> None:
        """Retrieve all maps from sql to populate `self.maps`."""
        await db_cursor.execute(
            "SELECT map_id, mods, slot FROM tourney_pool_maps WHERE pool_id = %s",
            [self.id],
        )

        async for row in db_cursor:
            map_id = row["map_id"]
            bmap = await Beatmap.from_bid(map_id)

            if not bmap:
                # map not found? remove it from the
                # pool and log this incident to console.
                # NOTE: it's intentional that this removes
                # it from not only this pool, but all pools.
                # TODO: perhaps discord webhook?
                log(f"Removing {map_id} from pool {self.name} (not found).",
                    Ansi.LRED)

                await db_cursor.execute(
                    "DELETE FROM tourney_pool_maps WHERE map_id = %s",
                    [map_id],
                )
                continue

            key: tuple[Mods, int] = (Mods(row["mods"]), row["slot"])
            self.maps[key] = bmap
예제 #9
0
파일: beatmap.py 프로젝트: Varkaria/gulag
async def ensure_local_osu_file(
    osu_file_path: Path,
    bmap_id: int,
    bmap_md5: str,
) -> bool:
    """Ensure we have the latest .osu file locally,
    downloading it from the osu!api if required."""
    if (not osu_file_path.exists() or
            hashlib.md5(osu_file_path.read_bytes()).hexdigest() != bmap_md5):
        # need to get the file from the osu!api
        if settings.DEBUG:
            log(f"Doing osu!api (.osu file) request {bmap_id}", Ansi.LMAGENTA)

        url = f"https://old.ppy.sh/osu/{bmap_id}"
        async with app.state.services.http.get(url) as resp:
            if resp.status != 200:
                if 400 <= resp.status < 500:
                    # client error, report this to cmyui
                    stacktrace = app.utils.get_appropriate_stacktrace()
                    await app.state.services.log_strange_occurrence(stacktrace)
                return False

            osu_file_path.write_bytes(await resp.read())

    return True
예제 #10
0
async def _remove_expired_donation_privileges(interval: int) -> None:
    """Remove donation privileges from users with expired sessions."""
    while True:
        if glob.app.debug:
            log("Removing expired donation privileges.", Ansi.LMAGENTA)

        expired_donors = await glob.db.fetchall(
            "SELECT id FROM users "
            "WHERE donor_end <= UNIX_TIMESTAMP() "
            "AND priv & 48",  # 48 = Supporter | Premium
            _dict=False,
        )

        for expired_donor_id in expired_donors:
            p = await glob.players.from_cache_or_sql(id=expired_donor_id)

            # TODO: perhaps make a `revoke_donor` method?
            await p.remove_privs(Privileges.DONATOR)
            await glob.db.execute(
                "UPDATE users SET donor_end = 0 WHERE id = %s",
                [p.id],
            )

            if p.online:
                p.enqueue(
                    packets.notification("Your supporter status has expired."))

            log(f"{p}'s supporter status has expired.", Ansi.LMAGENTA)

        await asyncio.sleep(interval)
예제 #11
0
파일: misc.py 프로젝트: cmyui/gulag
def _download_achievement_images_osu(achievements_path: Path) -> bool:
    """Download all used achievement images (one by one, from osu!)."""
    achs = []

    for res in ('', '@2x'):
        for gm in ('osu', 'taiko', 'fruits', 'mania'):
            # only osu!std has 9 & 10 star pass/fc medals.
            for n in range(1, 1 + (10 if gm == 'osu' else 8)):
                achs.append(f'{gm}-skill-pass-{n}{res}.png')
                achs.append(f'{gm}-skill-fc-{n}{res}.png')

        for n in (500, 750, 1000, 2000):
            achs.append(f'osu-combo-{n}{res}.png')

    log('Downloading achievement images from osu!.', Ansi.LCYAN)

    for ach in achs:
        r = requests.get(f'https://assets.ppy.sh/medals/client/{ach}')
        if r.status_code != 200:
            return False

        log(f'Saving achievement: {ach}', Ansi.LCYAN)
        (achievements_path / ach).write_bytes(r.content)

    return True
예제 #12
0
파일: beatmap.py 프로젝트: cmyui/gulag
async def ensure_local_osu_file(
    osu_file_path: Path,
    bmap_id: int, bmap_md5: str
) -> bool:
    """Ensure we have the latest .osu file locally,
       downloading it from the osu!api if required."""
    if (
        not osu_file_path.exists() or
        hashlib.md5(osu_file_path.read_bytes()).hexdigest() != bmap_md5
    ):
        # need to get the file from the osu!api
        if glob.app.debug:
            log(f'Doing osu!api (.osu file) request {bmap_id}', Ansi.LMAGENTA)

        url = f'https://old.ppy.sh/osu/{bmap_id}'
        async with glob.http.get(url) as r:
            if not r or r.status != 200:
                # temporary logging, not sure how possible this is
                stacktrace = utils.misc.get_appropriate_stacktrace()
                await utils.misc.log_strange_occurrence(stacktrace)
                return False

            osu_file_path.write_bytes(await r.read())

    return True
예제 #13
0
    async def maps_from_sql(self, db_conn: databases.core.Connection) -> None:
        """Retrieve all maps from sql to populate `self.maps`."""
        for row in await db_conn.fetch_all(
                "SELECT map_id, mods, slot FROM tourney_pool_maps WHERE pool_id = :pool_id",
            {"pool_id": self.id},
        ):
            map_id = row["map_id"]
            bmap = await Beatmap.from_bid(map_id)

            if not bmap:
                # map not found? remove it from the
                # pool and log this incident to console.
                # NOTE: it's intentional that this removes
                # it from not only this pool, but all pools.
                # TODO: perhaps discord webhook?
                log(f"Removing {map_id} from pool {self.name} (not found).",
                    Ansi.LRED)

                await db_conn.execute(
                    "DELETE FROM tourney_pool_maps WHERE map_id = :map_id",
                    {"map_id": map_id},
                )
                continue

            key: tuple[Mods, int] = (Mods(row["mods"]), row["slot"])
            self.maps[key] = bmap
예제 #14
0
파일: util.py 프로젝트: Yo-ru/Sekai
async def auth_osu_api(bot) -> None:
    try:
        bot.osu = OssapiV2(client_id=config.osu.get("id"), client_secret=config.osu.get("secret"))
        log("Authorized with the osu!api!", Ansi.LGREEN)
    except:
        bot.unload_extension("cogs.osu")
        log("Failed to authorize with the osu!api! (Unloaded cogs.osu!)", Ansi.LRED)
예제 #15
0
    async def dispatch(
        self,
        request: Request,
        call_next: RequestResponseEndpoint,
    ) -> Response:
        start_time = time.perf_counter_ns()
        response = await call_next(request)
        end_time = time.perf_counter_ns()

        time_elapsed = end_time - start_time

        # TODO: add metric to datadog

        col = (
            Ansi.LGREEN
            if 200 <= response.status_code < 300
            else Ansi.LYELLOW
            if 300 <= response.status_code < 400
            else Ansi.LRED
        )

        url = f"{request.headers['host']}{request['path']}"

        log(f"[{request.method}] {response.status_code} {url}", col, end=" | ")
        printc(f"Request took: {magnitude_fmt_time(time_elapsed)}", Ansi.LBLUE)

        response.headers["process-time"] = str(round(time_elapsed) / 1e6)
        return response
예제 #16
0
파일: misc.py 프로젝트: cmyui/gulag
async def fetch_geoloc_web(ip: IPAddress) -> dict[str, Union[str, float]]:
    """Fetch geolocation data based on ip (using ip-api)."""
    if not glob.has_internet:  # requires internet connection
        return

    url = f'http://ip-api.com/line/{ip}'

    async with glob.http.get(url) as resp:
        if not resp or resp.status != 200:
            log('Failed to get geoloc data: request failed.', Ansi.LRED)
            return

        status, *lines = (await resp.text()).split('\n')

        if status != 'success':
            err_msg = lines[0]
            if err_msg == 'invalid query':
                err_msg += f' ({url})'

            log(f'Failed to get geoloc data: {err_msg}.', Ansi.LRED)
            return

    acronym = lines[1].lower()

    return {
        'latitude': float(lines[6]),
        'longitude': float(lines[7]),
        'country': {
            'acronym': acronym,
            'numeric': country_codes[acronym]
        }
    }
예제 #17
0
파일: bg_loops.py 프로젝트: Varkaria/gulag
async def _remove_expired_donation_privileges(interval: int) -> None:
    """Remove donation privileges from users with expired sessions."""
    while True:
        if settings.DEBUG:
            log("Removing expired donation privileges.", Ansi.LMAGENTA)

        expired_donors = await app.state.services.database.fetch_all(
            "SELECT id FROM users "
            "WHERE donor_end <= UNIX_TIMESTAMP() "
            "AND priv & 48",  # 48 = Supporter | Premium
        )

        for expired_donor in expired_donors:
            p = await app.state.sessions.players.from_cache_or_sql(
                id=expired_donor["id"], )

            assert p is not None

            # TODO: perhaps make a `revoke_donor` method?
            await p.remove_privs(Privileges.DONATOR)
            await app.state.services.database.execute(
                "UPDATE users SET donor_end = 0 WHERE id = :id",
                {"id": p.id},
            )

            if p.online:
                p.enqueue(
                    app.packets.notification(
                        "Your supporter status has expired."), )

            log(f"{p}'s supporter status has expired.", Ansi.LMAGENTA)

        await asyncio.sleep(interval)
예제 #18
0
    async def maps_from_sql(self) -> None:
        """Retrieve all maps from sql to populate `self.maps`."""
        query = ('SELECT map_id, mods, slot '
                 'FROM tourney_pool_maps '
                 'WHERE pool_id = %s')

        for row in await glob.db.fetchall(query, [self.id]):
            map_id = row['map_id']
            bmap = await Beatmap.from_bid(map_id)

            if not bmap:
                # map not found? remove it from the
                # pool and log this incident to console.
                # NOTE: it's intentional that this removes
                # it from not only this pool, but all pools.
                # TODO: perhaps discord webhook?
                log(f'Removing {map_id} from pool {self.name} (not found).', Ansi.LRED)

                await glob.db.execute(
                    'DELETE FROM tourney_pool_maps '
                    'WHERE map_id = %s',
                    [map_id]
                )
                continue

            key = (Mods(row['mods']), row['slot'])
            self.maps[key] = bmap
예제 #19
0
파일: util.py 프로젝트: Yo-ru/Sekai
async def mysql_connect(bot, cred: dict) -> None:
    try:
        bot.db = AsyncSQLPool()
        await bot.db.connect(cred)
        log("Connected to MySQL!", Ansi.LGREEN)
    except:
        log("Failed to connect to MySQL!", Ansi.LRED)
        exit(1) # NOTE: Moé loses a lot of functionality without mysql; stop execution
예제 #20
0
    def append(self, p: Player) -> None:
        """Append `p` to the list."""
        if p in self:
            if settings.DEBUG:
                log(f"{p} double-added to global player list?")
            return

        super().append(p)
예제 #21
0
    def remove(self, p: Player) -> None:
        """Remove `p` from the list."""
        if p not in self:
            if settings.DEBUG:
                log(f"{p} removed from player list when not online?")
            return

        super().remove(p)
예제 #22
0
파일: collections.py 프로젝트: cmyui/gulag
    def remove(self, p: Player) -> None:
        """Remove `p` from the list."""
        if p not in self:
            if glob.app.debug:
                log(f'{p} removed from player list when not online?')
            return

        super().remove(p)
예제 #23
0
파일: collections.py 프로젝트: cmyui/gulag
    def append(self, p: Player) -> None:
        """Append `p` to the list."""
        if p in self:
            if glob.app.debug:
                log(f'{p} double-added to global player list?')
            return

        super().append(p)
예제 #24
0
파일: util.py 프로젝트: Yo-ru/Sekai
async def create_client_session(bot) -> None:
    try:
        bot.request = aiohttp.ClientSession(json_serialize=orjson.dumps)
        log("Client Session created!", Ansi.LGREEN)
    except:
        log("Failed to get the Client Session!", Ansi.LRED)
        bot.db.close() # safely close db connection
        exit(1) # NOTE: Moé loses a lot of functionality without the client session; stop execution
예제 #25
0
파일: beatmap.py 프로젝트: cmyui/Asahi
    async def from_api(cls, md5: str) -> Optional['Beatmap']:
        api = 'https://old.ppy.sh/api/get_beatmaps'
        params = {'k': glob.config.api_key, 'h': md5}

        async with glob.web.get(api, params=params) as resp:
            if resp.status != 200 or not resp:
                return # request failed, map prob doesnt exist

            data = await resp.json()
            if not data:
                return

            bmap = data[0] # i hate this idea but o well

        self = cls()
        self.id = int(bmap['beatmap_id'])
        self.sid = int(bmap['beatmapset_id'])
        self.md5 = md5

        self.bpm = float(bmap['bpm'])
        self.cs = float(bmap['diff_size'])
        self.ar = float(bmap['diff_approach'])
        self.od = float(bmap['diff_overall'])
        self.hp = float(bmap['diff_drain'])
        self.sr = float(bmap['difficultyrating'])
        self.mode = osuModes(int(bmap['mode']))

        self.artist = bmap['artist']
        self.title = bmap['title']
        self.diff = bmap['version']
        self.mapper = bmap['creator']

        self.status = mapStatuses.from_api(int(bmap['approved']))
        self.update = dt.strptime(bmap['last_update'], '%Y-%m-%d %H:%M:%S').timestamp()

        self.nc = time.time()
        e = await glob.db.fetchrow('SELECT frozen, status, `update` FROM maps WHERE id = %s', [self.id])

        if e:
            if self.update > e['update']:
                if e['frozen'] and self.status != e['status']:
                    self.status = e['status']
                    self.frozen = e['frozen'] == 1
                    self.lb = None # status has changed, lets reset lb cache in case

                await self.save()
            else:
                pass
        else:
            self.frozen = False # don't freeze by default, we can override if someone manually edits the map status
            await self.save()

        glob.cache['maps'][md5] = self # cache the map now we have it from api & saved in sql

        await cls.cache_set(self.sid)

        log(f'Retrieved Set ID {self.sid} from osu!api', Ansi.LCYAN)
        return self
예제 #26
0
파일: collections.py 프로젝트: cmyui/gulag
    def remove(self, m: Match) -> None:
        """Remove `m` from the list."""
        for i, _m in enumerate(self):
            if m is _m:
                self[i] = None
                break

        if glob.app.debug:
            log(f'{m} removed from matches list.')
예제 #27
0
    def remove(self, m: Match) -> None:
        """Remove `m` from the list."""
        for i, _m in enumerate(self):
            if m is _m:
                self[i] = None
                break

        if settings.DEBUG:
            log(f"{m} removed from matches list.")
예제 #28
0
 async def prepare(self, db_conn: databases.core.Connection) -> None:
     """Fetch data from sql & return; preparing to run the server."""
     log("Fetching clans from sql.", Ansi.LCYAN)
     for row in await db_conn.fetch_all("SELECT * FROM clans"):
         row = dict(row)  # make a mutable copy
         row["owner_id"] = row.pop("owner")
         clan = Clan(**row)
         await clan.members_from_sql(db_conn)
         self.append(clan)
예제 #29
0
파일: bancho.py 프로젝트: cmyui/Asahi
async def send_pm(user: Player, p: bytes) -> None:
    d = reader.handle_packet(p, (('msg', osuTypes.message),))

    msg = d['msg'].msg
    tarname = d['msg'].tarname

    if not (target := await glob.players.get(name=tarname)):
        log(f'{user.name} tried to send message to offline user {tarname}', Ansi.LRED)
        return
예제 #30
0
파일: bancho.py 프로젝트: cmyui/Asahi
async def friend_remove(user: Player, p: bytes) -> None:
    tar = (reader.handle_packet(p, (('uid', osuTypes.i32),)))['uid']

    if tar not in user.friends:
        return

    user.friends.remove(tar)
    await glob.db.execute('DELETE FROM friends WHERE user1 = %s AND user2 = %s', [user.id, tar])

    log(f"{user.name} removed UID {tar} from their friends list.", Ansi.LCYAN)