import warnings
from typing import Union, Type
from httpx._config import Proxy as httpxProxy
from .constants import LIKES_ARE_PRIVATE_NOW_WARNING
from .utils import (find_objects, AuthRequired, get_user_from_typehead, get_tweet_id, check_translation_lang,
                    is_tweet_protected, async_list)
from .types import (Proxy, TweetComments, UserTweets, Search, User, Tweet, Trends, Community, CommunityTweets,
                    CommunityMembers, UserFollowers, UserFollowings, TweetHistory, UserMedia, GifSearch,
                    ShortUser, TypeHeadSearch, TweetTranslate, AudioSpace, UserHighlights, UserLikes, Places,
                    UserSubscribers, UserCommunities, Broadcast, LiveStreamPayload)
from .exceptions import *
from .session import Session, MemorySession, FileSession
from .http import Request
from .captcha.base import BaseCaptchaSolver
from .filters import TweetCommentFilters


class BotMethods:
    LOGIN_URL = "https://api.x.com/1.1/onboarding/task.json?flow_name=login"

    def __init__(
            self,
            session_name: Union[str, Session],
            proxy: Union[httpxProxy, Proxy, str] = None,
            captcha_solver: Type[BaseCaptchaSolver] = None,
            **httpx_kwargs
    ):
        """
        Constructor of the Twitter Public class

        :param: session_name: (`str`, `Session`) This is the name of the session which will be saved and can be loaded later
        :param: proxy: (`ProxyTypes` or `None`) Provide the proxy you want to use while making a request
        :param: captcha_solver: (`BaseCaptchaSolver`) Provide the instance of captcha solver class
                                which has two mandatory methods named `unlock`, `__call__`.
                                - both mandatory methods should accept at least one argument
        """

        self._login_url = self.LOGIN_URL
        self._username = None
        self._password = None
        self._extra = None
        self._login_flow = None
        self._login_flow_state = None
        self._last_json = {}
        self._cached_users = {}
        self._proxy = str(proxy) if isinstance(proxy, Proxy) else proxy
        self._event_builders = []
        self._captcha_solver = None

        if isinstance(session_name, MemorySession):
            self.session = session_name(self)
        elif isinstance(session_name, FileSession):
            self.session = session_name
        else:
            self.session = FileSession(self, session_name)

        if captcha_solver:
            if not hasattr(captcha_solver, "unlock"):
                raise AttributeError("captcha_solver instance '{}' doesn't have 'unlock' method".format(type(captcha_solver)))
            elif "__call__" not in dir(captcha_solver):
                raise AttributeError("captcha_solver instance '{}' doesn't have '__call__' method".format(type(captcha_solver)))

            # Captcha Solver is broken
            # self._captcha_solver = captcha_solver(self, self._proxy)

        self.cookies = None
        self.logged_in = False
        self.is_user_authorized = False
        self.request = self.http = Request(self, max_retries=10, proxy=self._proxy, captcha_solver=captcha_solver, **httpx_kwargs)
        self.user = None
    
    async def get_user_info(self, username: Union[str, int, list] = None):
        """
        Get the User Info of the specified username

        :param: username: (`str` | `int` | List[`str`, `int`]) username or user_id to get information of

        :return: .types.twDataTypes.User
        """

        if not username and self.user is not None:
            username = self.user.username

        if isinstance(username, list):
            for i in username:
                if not str(i).isdigit():
                    raise ValueError("Only Accept List of User IDs.")

        if isinstance(username, list) or str(username).isdigit() or isinstance(username, int):
            usernames = [username] if not isinstance(username, list) else username
            users_raw = await self.request.get_users_by_rest_id(usernames)
            users = find_objects(users_raw, "users", None, recursive=False, none_value=[])

            parsed_users = []
            for user in users:
                try:
                    this_user = User(self, user)
                    parsed_users.append(this_user)
                    self._cached_users[str(this_user.username).lower()] = this_user.id
                except Exception as e:
                    warnings.warn(f"UsersByRestId Error: {str(e)}")

            if len(usernames) == 1 and len(parsed_users) > 0:
                return parsed_users[0]
            elif len(usernames) == 1 and len(parsed_users) == 0:
                return None
            elif len(usernames) > 1 and len(parsed_users) == 0:
                return []
            else:
                return parsed_users
        else:
            user_raw = await self.request.get_user(username)
            user = User(self, user_raw)
            self._cached_users[str(username).lower()] = user.id
            return user

    @property
    def user_id(self) -> int:
        """
        Get the user unique twitter id of authenticated user

        :return: int
        """
        return self.user.id if self.user else None

    @property
    def cache(self):
        return self._cached_users
    
    async def get_user_id(self, username: str):
        return await self._get_user_id(username)

    async def _get_user_id(self, username):
        if not username:
            username = self.me

        if isinstance(username, (User, ShortUser)):
            user_id = username.id
        elif isinstance(username, int) or (isinstance(username, str) and str(username).isdigit()):
            user_id = username
        elif self._cached_users.get(username.lower()):
            user_id = self._cached_users[username.lower()]
        else:
            user = None
            try:
                all_users = await self.typehead_user_search(username)
                user = await get_user_from_typehead(username, all_users)
            except TwitterError as e:
                if str(e.__class__.__name__) == "AuthenticationRequired" or "[34]" in str(e):
                    # We can only get user using `typehead_user_search` when authenticated or if user is not suspended
                    pass

            if not user:
                user = await self.get_user_info(username)

            if user:
                user_id = user.id
            else:
                raise UserNotFound()
        return user_id

    async def get_tweets(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            replies: bool = False,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ) -> UserTweets:
        """
         Get the tweets from a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: replies: (`boolean`) get the replied tweets of the user too
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: .types.usertweet.UserTweets
        """

        user_id = await self._get_user_id(username)
        userTweets = UserTweets(user_id, self, pages, replies, wait_time, cursor)
        return await async_list(userTweets)

    async def iter_tweets(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            replies: bool = False,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):

        """
         Generator for getting the tweets from a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: replies: (`boolean`) get the replied tweets of the user too
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: (.types.usertweet.UserTweets, list[.types.twDataTypes.Tweet])
        """

        user_id = await self._get_user_id(username)

        userTweets = UserTweets(user_id, self, pages, replies, wait_time, cursor)

        async for result_tuple in userTweets.generator():
            yield result_tuple

    async def get_user_highlights(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            replies: bool = False,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):
        """
         Get the tweets from a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: replies: (`boolean`) get the replied tweets of the user too
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: .types.usertweet.userHighlights
        """

        user_id = await self._get_user_id(username)

        userHighlights = UserHighlights(user_id, self, pages, replies, wait_time, cursor)

        return await async_list(userHighlights)

    async def iter_user_highlights(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            replies: bool = False,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):

        """
         Generator for getting the tweets from a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: replies: (`boolean`) get the replied tweets of the user too
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: (.types.usertweet.UserHighlights, list[.types.twDataTypes.Tweet])
        """

        user_id = await self._get_user_id(username)

        userHighlights = UserHighlights(user_id, self, pages, replies, wait_time, cursor)

        async for result_tuple in userHighlights.generator():
            yield result_tuple

    async def get_user_likes(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            replies: bool = False,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):
        """
         Get the liked tweets of a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: replies: (`boolean`) get the replied tweets of the user too
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: .types.usertweet.userLikes
        """

        user_id = await self._get_user_id(username)
        if str(user_id) != str(self.me.id):
            warnings.warn(LIKES_ARE_PRIVATE_NOW_WARNING)

        userLikes = UserLikes(user_id, self, pages, replies, wait_time, cursor)

        return await async_list(userLikes)

    async def iter_user_likes(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            replies: bool = False,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):

        """
         Generator for getting the liked tweets of a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: replies: (`boolean`) get the replied tweets of the user too
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: (.types.usertweet.UserLikes, list[.types.twDataTypes.Tweet])
        """

        user_id = await self._get_user_id(username)
        if str(user_id) != str(self.me.id):
            warnings.warn(LIKES_ARE_PRIVATE_NOW_WARNING)

        userLikes = UserLikes(user_id, self, pages, replies, wait_time, cursor)

        async for result_tuple in userLikes.generator():
            yield result_tuple

    @AuthRequired
    async def get_user_media(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ) -> UserMedia:
        """
         Get the media from a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: .types.usertweet.UserMedia
        """

        user_id = await self._get_user_id(username)

        userMedia = UserMedia(user_id, self, pages, wait_time, cursor)

        return await async_list(userMedia)

    @AuthRequired
    async def iter_user_media(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):

        """
         Generator for getting the media from a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: (.types.usertweet.UserMedia, list[.types.twDataTypes.Tweet])
        """

        user_id = await self._get_user_id(username)

        userMedia = UserMedia(user_id, self, pages, wait_time, cursor)

        async for result_tuple in userMedia.generator():
            yield result_tuple

    @AuthRequired
    async def get_trends(self):
        """
        Get the Trends from you locale

        :return: list of .types.twDataTypes.Trends
        """
        trends = []
        response = await self.request.get_trends()

        entries = find_objects(response, "addEntries", None)
        if not entries or len(entries) == 0:
            return trends

        for entry in entries['entries']:
            if str(entry['entryId']) == "trends":
                for item in entry['content']['timelineModule']['items']:
                    trends.append(Trends(self, item))

        return trends

    @AuthRequired
    async def search(
            self,
            keyword: str,
            pages: int = 1,
            filter_: str = None,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ) -> Search:

        """
        Search for a keyword or hashtag on Twitter

        :param: keyword: (`str`) The keyword which is supposed to be searched
        :param: pages: (`int`) The number of pages to get
        :param: filter_: (
           `str`| `filters.SearchFilters.Users()`| `filters.SearchFilters.Latest()` | `filters.SearchFilters.Photos()` | `filters.SearchFilters.Videos()`
        )
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: (`str`) Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)
        :return: .types.search.Search
        """

        search = Search(keyword, self, pages, filter_, wait_time, cursor)

        return await async_list(search)

    @AuthRequired
    async def iter_search(
            self,
            keyword: str,
            pages: int = 1,
            filter_: str = None,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):
        """
        Search for a keyword or hashtag on Twitter

        :param: keyword: (`str`) The keyword which is supposed to be searched
        :param: pages: (`int`) The number of pages to get
        :param: filter_: (
           `str`| `filters.SearchFilters.Users()`| `filters.SearchFilters.Latest()` | `filters.SearchFilters.Photos()` | `filters.SearchFilters.Videos()`
        )
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: (`str`) Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)


        :return: (.types.search.Search, list[.types.twDataTypes.Tweet])
        """

        search = Search(keyword, self, pages, filter_, wait_time, cursor)

        async for result_tuple in search.generator():
            yield result_tuple

    @AuthRequired
    async def typehead_user_search(self, keyword):
        type_head_search = TypeHeadSearch(self, keyword, "users")
        await type_head_search.get_results()
        return type_head_search

    @AuthRequired
    async def get_audio_space(self, space_id: Union[str, Tweet]) -> AudioSpace:
        """

        :param: space_id: ID of the Audio Space , or the Tweet Object that Space Audio is part of.
        :return: .types.twDataTypes.AudioSpace
        """

        if isinstance(space_id, Tweet):
            space_id = space_id.audio_space_id

        space = await self.request.get_audio_space(space_id)

        if not find_objects(space, "metadata", None):
            raise AudioSpaceNotFound(404, "BadRequest", response=space)

        return AudioSpace(self, space)

    @AuthRequired
    async def get_community(self, community_id):
        """

        :param: community_id: ID of the community to get
        :return:
        """

        response = await self.request.get_community(community_id)
        return Community(self, response)

    @AuthRequired
    async def get_user_communities(self, user_id=None):
        """
        Get Communities of a specific user is member of

        :param user_id: User Id of the User whom to get communities of
        :return:.types.community.UserCommunities
        """

        user_id = await self.get_user_id(user_id)

        userCommunities = UserCommunities(self, user_id)
        return await async_list(userCommunities)

    @AuthRequired
    async def iter_community_tweets(
            self,
            community_id: Union[str, int, Community],
            pages: int = 1,
            filter_: str = None,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):

        """
         Generator for getting the tweets from a community

        :param: community_id: (`str` | `int` | `Community`) ID of the community whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: filter_: (`str`) Filter the Tweets
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: (.types.community.CommunityTweets, list[.types.twDataTypes.Tweet])
        """

        if isinstance(community_id, Community):
            community_id = community_id.id

        communityTweets = CommunityTweets(community_id, self, pages, filter_, wait_time, cursor)

        async for result_tuple in communityTweets.generator():
            yield result_tuple

    @AuthRequired
    async def get_community_tweets(
            self,
            community_id: Union[str, int, Community],
            pages: int = 1,
            filter_: str = None,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):

        """
         Getting the tweets from a community

        :param: community_id: (`str` | `int` | `Community`) ID of the community whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: filter_: (`str`) Filter the Tweets
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: .types.community.CommunityTweets
        """

        if isinstance(community_id, Community):
            community_id = community_id.id

        communityTweets = CommunityTweets(community_id, self, pages, filter_, wait_time, cursor)

        return await async_list(communityTweets)

    @AuthRequired
    async def get_community_members(
            self,
            community_id: Union[str, int, Community],
            pages: int = 1,
            filter_: str = None,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):
        """
         Getting the Members from a community

        :param: community_id: (`str` | `int` | `Community`) ID of the community whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: filter_: (`str`) Filter the Members
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: .types.community.CommunityMembers
        """

        if isinstance(community_id, Community):
            community_id = community_id.id

        communityTweets = CommunityMembers(community_id, self, pages, filter_, wait_time, cursor)

        return await async_list(communityTweets)

    @AuthRequired
    async def iter_community_members(
            self,
            community_id: Union[str, int, Community],
            pages: int = 1,
            filter_: str = None,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):
        """
         Getting the Members from a community

        :param: community_id: (`str` | `int` | `Community`) ID of the community whom to get the tweets of
        :param: pages: (`int`) number of pages to be scraped
        :param: filter_: (`str`) Filter the Members
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: (.types.community.CommunityMembers, [.types.twDataTypes.User])
        """

        if isinstance(community_id, Community):
            community_id = community_id.id

        communityTweets = CommunityMembers(community_id, self, pages, filter_, wait_time, cursor)

        async for result_tuple in communityTweets.generator():
            yield result_tuple

    @AuthRequired
    async def get_user_followers(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ) -> UserFollowers:
        """
         Get the followers of a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the followers of
        :param: pages: (`int`) number of pages to be scraped
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: .types.follow.UserFollowers
        """

        user_id = await self._get_user_id(username)

        userFollowers = UserFollowers(user_id, self, pages, wait_time, cursor)

        return await async_list(userFollowers)

    @AuthRequired
    async def iter_user_followers(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):

        """
         Generator for getting the followers from a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the followers of
        :param: pages: (`int`) number of pages to be scraped
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: (.types.follow.UserFollowers, list[.types.twDataTypes.User])
        """

        user_id = await self._get_user_id(username)

        userFollowers = UserFollowers(user_id, self, pages, wait_time, cursor)

        async for result_tuple in userFollowers.generator():
            yield result_tuple

    @AuthRequired
    async def get_user_followings(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ) -> UserFollowings:
        """
         Get the Followings of a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the followings of
        :param: pages: (`int`) number of pages to be scraped
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: .types.follow.UserFollowings
        """

        user_id = await self._get_user_id(username)

        userFollowings = UserFollowings(user_id, self, pages, wait_time, cursor)

        return await async_list(userFollowings)

    @AuthRequired
    async def iter_user_followings(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):

        """
         Generator for getting the followings from a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the followings of
        :param: pages: (`int`) number of pages to be scraped
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: (.types.follow.UserFollowings, list[.types.twDataTypes.User])
        """

        user_id = await self._get_user_id(username)

        userFollowings = UserFollowings(user_id, self, pages, wait_time, cursor)

        async for result_tuple in userFollowings.generator():
            yield result_tuple

    @AuthRequired
    async def get_user_subscribers(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ) -> UserSubscribers:
        """
         Get the Subscribers of a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the followings of
        :param: pages: (`int`) number of pages to be scraped
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: .types.follow.UserSubscribers
        """

        user_id = await self._get_user_id(username)

        userSubscribers = UserSubscribers(user_id, self, pages, wait_time, cursor)

        return await async_list(userSubscribers)

    @AuthRequired
    async def iter_user_subscribers(
            self,
            username: Union[str, int, User],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None
    ):

        """
         Generator for getting the Subscribers from a user

        :param: username: (`str` | `int` | `User`) username of the user whom to get the followings of
        :param: pages: (`int`) number of pages to be scraped
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)

        :return: (.types.follow.UserSubscribers, list[.types.twDataTypes.User])
        """

        user_id = await self._get_user_id(username)

        userSubscribers = UserSubscribers(user_id, self, pages, wait_time, cursor)

        async for result_tuple in userSubscribers.generator():
            yield result_tuple

    @AuthRequired
    async def get_tweet_comments(
            self,
            tweet_id: Union[int, str, Tweet],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None,
            get_hidden: bool = False,
            filter_: str = TweetCommentFilters.Relevant
    ):
        """

        :param: tweet_id: Tweet ID or the Tweet Object of which the Comments to get
        :param: pages: (`int`) The number of pages to get
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: (`str`) Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)
        :param: get_hidden: (`bool`) get the hidden comments (most likely offensive comments)
        :param: filter_: (`str`) Filter Tweet Comments
        :return: .types.usertweet.TweetComments
        """

        tweetId = get_tweet_id(tweet_id)

        comments = TweetComments(tweetId, self, get_hidden, filter_, pages, wait_time, cursor)
        return await async_list(comments)

    @AuthRequired
    async def iter_tweet_comments(
            self,
            tweet_id: Union[str, Tweet],
            pages: int = 1,
            wait_time: Union[int, list, tuple] = 2,
            cursor: str = None,
            get_hidden: bool = False,
            filter_: str = TweetCommentFilters.Relevant
    ):
        """

        :param: tweet_id: Tweet ID or the Tweet Object of which the Likes to get
        :param: pages: (`int`) The number of pages to get
        :param: wait_time: (`int`, `list`, `tuple`) seconds to wait between multiple requests
        :param: cursor: (`str`) Pagination cursor if you want to get the pages from that cursor up-to (This cursor is different from actual API cursor)
        :param: get_hidden: (`bool`) get the hidden comments (most likely offensive comments)
        :param: filter_: (`str`) Filter Tweet Comments

        :return: (.types.usertweet.TweetComments, list[.types.twDataTypes.ConversationThread])
        """

        tweetId = get_tweet_id(tweet_id)

        comments = TweetComments(tweetId, self, get_hidden, filter_, pages, wait_time, cursor)

        async for result_tuple in comments.generator():
            yield result_tuple

    @AuthRequired
    async def tweet_edit_history(self, identifier) -> TweetHistory:
        """
        Get Edit History of a Tweet

        :param identifier: (`str`) The unique identifier of the tweet , either the `Tweet id` or `Tweet Link`

        :return: .types.usertweet.TweetHistory
        """

        tweetId = get_tweet_id(identifier)
        tweet_history = TweetHistory(tweetId, self)
        await tweet_history.get_history()
        return tweet_history

    async def tweet_detail(self, identifier: str) -> Tweet:
        """
        Get Detail of a single tweet

        :param: identifier: (`str`) The unique identifier of the tweet , either the `Tweet id` or `Tweet Link`

        :return: .types.twDataTypes.Tweet
        """

        tweetId = get_tweet_id(identifier)

        response = await self.request.get_tweet_detail(tweetId)

        if self.user is None:
            if find_objects(response, "tweetResult", None):
                return Tweet(self, response, response)
        else:
            _tweet_before = []
            entries = find_objects(response, "type", "TimelineAddEntries")

            if not entries or len(entries) == 0:
                raise InvalidTweetIdentifier(response=response)

            for entry in entries['entries']:
                if str(entry['entryId']).split("-")[0] == "tweet":
                    # ignore these protected tweets that are not what we are looking for
                    # otherwise it will throw exception
                    if not (is_tweet_protected(entry) and str(entry['entryId'].split("-")[1]) != str(tweetId)):
                        tweet = Tweet(self, entry, response)

                        if str(tweet.id) == str(tweetId):
                            tweet.threads.extend(_tweet_before)
                            return tweet
                        else:
                            _tweet_before.append(tweet)

        raise InvalidTweetIdentifier(response=response)

    async def translate_tweet(self, tweet_id: Union[str, int, Tweet], language: str):
        """
            Translate Tweet in another Language

            :param tweet_id: Tweet ID of the Tweet to be translated
            :param language: In which Language you want to translate the Tweet (see tweety.filters.Language)
            :return: .types.twDataTypes.TweetTranslate
        """

        tweetId = get_tweet_id(tweet_id)
        language = check_translation_lang(language)
        response = await self.request.get_tweet_translation(tweetId, language)
        return TweetTranslate(self, response)

    async def search_gifs(self, search_term, pages=1, cursor=None, wait_time=2):
        search = GifSearch(search_term, self, pages, cursor, wait_time)
        return await async_list(search)

    async def iter_search_gifs(self, search_term, pages=1, cursor=None, wait_time=2):
        search = GifSearch(search_term, self, pages, cursor, wait_time)
        async for result_tuple in search.generator():
            yield result_tuple

    async def search_place(self, lat=None, long=None, search_term=None):
        """
        Search Place either using `search_term` , or `latitude` and `longitude`

        :param lat: Latitude of Place
        :param long: Longitude of Place
        :param search_term: Search Term of Place
        :return: .type.places.Places
        """

        places = Places(self, lat, long, search_term)
        await places.get_page()
        return places

    async def get_stream(self, media_key):
        if isinstance(media_key, (Broadcast, AudioSpace)):
            media_key = media_key.media_key

        response = await self.http.get_stream(media_key)
        return LiveStreamPayload(self, response)
