Ejemplo n.º 1
0
    async def send_event(self, event: Event, subscription: Subscription):
        channel = self.get_channel(subscription.channel_id)
        if not channel:
            # Ignore channels we no longer have access to (e.g. deleted / power outage).
            return

        while True:
            try:
                await channel.send(content=format_link(event),
                                   embed=await
                                   format_embed(event,
                                                skip_timeago_if_recent=True))
                break
            except Forbidden:
                break  # In case we're subscribed to a channel we don't have access to.
            except ClientOSError as ex:
                log_err(
                    f"WARNING | Encountered ClientOSError \"{ex}\" when sending to channel \"{channel}\", retrying..."
                )
            except HTTPException as ex:
                if ex.text.startswith("5"):
                    # 500-type codes are server-related (i.e. on Discord's end) and can be safely ignored.
                    # Commonly "503: Service Unavailable" and "504: Gateway Time-out".
                    log_err(
                        f"WARNING | Encountered HTTPException \"{ex}\" when sending to channel \"{channel}\", retrying..."
                    )
Ejemplo n.º 2
0
    def parse_event_json(self,
                         event_json: object,
                         user_jsons: object = None) -> Event:
        """Returns a BeatmapsetEvent reflecting the given event json object.
        Ignores any event with an incomplete context (e.g. deleted beatmaps).

        Requests user names from the api unless supplied with the corresponding user json from the discussion page."""
        if not event_json:
            # Seems to occur when the respective beatmapset has been deleted. However, it's there when
            # viewing the page source manually for some reason, regardless of login status.
            log_err(
                "WARNING | An event is missing; the beatmapset was probably deleted."
            )
            return None

        try:
            # Scrape object data
            _type = event_json["message_type"]
            time = timestamp.from_string(event_json["created_at"])

            beatmapset_id = event_json["beatmapset_id"]
            discussion_id = event_json["starting_post"][
                "beatmap_discussion_id"]

            user_id = event_json["user_id"]
            # The user name is either provided by a user json from the discussion page, or queried through the api.
            user_json = self.__lookup_user_json(user_id, user_jsons)
            user_name = user_json["username"] if user_json else None

            content = event_json["starting_post"]["message"]

            difficulty = event_json["beatmap"][
                "version"] if "beatmap" in event_json and "version" in event_json[
                    "beatmap"] else None
            tab = None
            if event_json["timestamp"] is not None: tab = "timeline"
            elif difficulty: tab = "general"
            else: tab = "generalAll"

            # Reconstruct objects
            beatmapset = Beatmapset(beatmapset_id)
            user = User(user_id, user_name) if user_id is not None else None
            # TODO: This portion is missing handling for replies, see the other method.
            # Still unclear which message_type replies use; will need to find out if/when replies get json formats.
            discussion = Discussion(
                discussion_id, beatmapset, user, content, tab,
                difficulty) if discussion_id is not None else None
        except DeletedContextError as err:
            log_err(err)
        else:
            return Event(_type=_type,
                         time=time,
                         beatmapset=beatmapset,
                         discussion=discussion,
                         user=user,
                         content=content)

        return None
Ejemplo n.º 3
0
async def loop(client: Client, reader: Reader) -> None:
    """Updates the client activity ("Playing" indicator), and status (Online indicator), every minute."""
    try:
        while True:
            await client.change_presence(activity=get_activity(client, reader),
                                         status=get_status(client, reader))
            # Presence updates are ratelimited at 1 update / 15s.
            await asyncio.sleep(60)
    except Exception as ex:
        log_err(f"WARNING | Discord presence raised \"{ex}\"")
Ejemplo n.º 4
0
 async def respond(self, response: str, embed: Embed=None) -> bool:
     """Sends a message in the channel where the command was called, with the given response text and embed, if any.
     Handles Forbidden and HTTPException errors by logging. Returns whether a response was successfully sent."""
     # `\u200b` is a zero-width space. This prevents any kind of mention/highlight.
     response = response.replace("@", "@\u200b")
     try:
         await self.context.channel.send(response, embed=embed)
         self.response = response
         self.response_embed = embed
         return True
     except Forbidden:
         log_err(f"Lacking permissions to write \"{response}\" in {self.context.channel}.")
     
     return False
Ejemplo n.º 5
0
def try_request(request_url: str, **kwargs) -> Response:
    """Requests a response object and returns it if successful, otherwise None is returned.
    If the website is in cloudflare IUAM mode, we also return None."""
    response = None

    try:
        response = requests.get(request_url, **kwargs)
    except requests.exceptions.ConnectionError:
        log_err(
            f"WARNING | ConnectionError was raised on GET \"{request_url}\"")
        return None

    if "<title>Just a moment...</title>" in response.text:
        log_err("WARNING | CloudFlare IUAM is active")
        return None

    return response
Ejemplo n.º 6
0
    def parse_event(self, event: Tag) -> Event:
        """Returns a BeatmapsetEvent reflecting the given event html Tag object.
        Ignores any event with an incomplete context (e.g. deleted beatmaps)."""
        try:
            # Scrape object data
            _type = self.parse_event_type(event)
            time = self.parse_event_time(event)

            link = self.parse_event_link(event)
            beatmapset_id = self.parse_id_from_beatmapset_link(link)
            discussion_id = self.parse_id_from_discussion_link(link)

            user_id = self.parse_event_author_id(event)
            user_name = self.parse_event_author_name(event)

            content = self.parse_discussion_message(event)

            # Reconstruct objects
            beatmapset = Beatmapset(beatmapset_id)
            user = User(user_id, user_name) if user_id is not None else None
            if _type == "reply":
                # Replies should look up the discussion they are posted on.
                discussion = Discussion(
                    discussion_id,
                    beatmapset) if discussion_id is not None else None
            else:
                tab = self.parse_discussion_tab(event)
                difficulty = self.parse_discussion_diff(event)

                discussion = Discussion(
                    discussion_id, beatmapset, user, content, tab,
                    difficulty) if discussion_id is not None else None
        except DeletedContextError as err:
            log_err(err)
        else:
            return Event(_type=_type,
                         time=time,
                         beatmapset=beatmapset,
                         discussion=discussion,
                         user=user,
                         content=content)

        return None
Ejemplo n.º 7
0
def try_request(request_url: str, method: str = "GET", **kwargs) -> Response:
    """Requests a response object and returns it if successful, otherwise None is returned.
    If the website is in cloudflare IUAM mode, we also return None."""
    response = None

    log(f"GET {request_url}", postfix="requests")

    try:
        response = requests.request(method, request_url, **kwargs)
    except requests.exceptions.ConnectionError:
        log_err(
            f"WARNING | ConnectionError was raised on GET \"{request_url}\"")
        return None
    except requests.exceptions.ReadTimeout:
        log_err(f"WARNING | ReadTimeout was raised on GET \"{request_url}\"")
        return None

    if "<title>Just a moment...</title>" in response.text:
        log_err("WARNING | CloudFlare IUAM is active")
        return None

    log(f"RECEIVED {response.status_code}: {response.reason}",
        postfix="requests")

    return response
Ejemplo n.º 8
0
    def parse_event_json(self,
                         event_json: object,
                         user_jsons: object = None) -> Event:
        """Returns a BeatmapsetEvent reflecting the given event json object.
        Ignores any event with an incomplete context (e.g. deleted beatmaps).

        Requests user names from the api unless supplied with the json-users."""
        if not event_json:
            # Seems to occur when the respective beatmapset has been deleted.
            log_err(
                "WARNING | An event is missing; the beatmapset was probably deleted."
            )
            return None

        try:
            # Scrape object data
            _type = event_json["type"]
            time = timestamp.from_string(event_json["created_at"])

            if "beatmapset" not in event_json or not event_json["beatmapset"]:
                raise DeletedContextError(
                    "No beatmapset was found in this event. It was likely deleted."
                )

            beatmapset_id = event_json["beatmapset"]["id"]
            discussion_id = event_json["discussion"][
                "id"] if "discussion" in event_json and event_json[
                    "discussion"] else None

            user_id = event_json["user_id"] if "user_id" in event_json else None
            user_json = self.__lookup_user_json(user_id, user_jsons)
            user_name = user_json["username"] if user_json else None

            content = None
            if _type in [types.LANGUAGE_EDIT, types.GENRE_EDIT]:
                # Language/genre edits always have "old" and "new" fields, which no other type has.
                old = event_json["comment"]["old"]
                new = event_json["comment"]["new"]
                content = f"{old} -> {new}"

            if _type in [types.UNLOVE]:
                # E.g. "Mapper has asked for it to be removed from Loved".
                content = event_json["comment"]["reason"]

            # Reconstruct objects
            beatmapset = Beatmapset(beatmapset_id)
            user = User(user_id, user_name) if user_id is not None else None
            discussion = Discussion(
                discussion_id,
                beatmapset) if discussion_id is not None else None
        except DeletedContextError as err:
            log_err(err)
        else:
            return Event(_type=_type,
                         time=time,
                         beatmapset=beatmapset,
                         discussion=discussion,
                         user=user,
                         content=content)

        return None