Esempio n. 1
0
async def cmd_bonkrank(config: Config, match: Match[str]) -> str:
    user = optional_user_arg(match)
    ret = _rank(user, BONKER_RE)
    if ret is None:
        return format_msg(match, f'user not found {esc(user)}')
    else:
        rank, n = ret
        return format_msg(
            match,
            f'{esc(user)} is ranked #{rank}, has bonked others {n} times',
        )
Esempio n. 2
0
async def cmd_bonkedrank(config: Config, match: Match[str]) -> str:
    user = optional_user_arg(match)
    ret = _user_rank_by_line_type(user, BONKED_RE)
    if ret is None:
        return format_msg(match, f'user not found {esc(user)}')
    else:
        rank, n = ret
        return format_msg(
            match,
            f'{esc(user)} is ranked #{rank}, has been bonked {n} times',
        )
Esempio n. 3
0
async def cmd_chatrank(config: Config, match: Match[str]) -> str:
    user = optional_user_arg(match)
    ret = _rank(user, CHAT_LOG_RE)
    if ret is None:
        return format_msg(match, f'user not found {esc(user)}')
    else:
        rank, n = ret
        return format_msg(
            match,
            f'{esc(user)} is ranked #{rank} with {n} messages '
            f'(since {_log_start_date()})',
        )
Esempio n. 4
0
async def cmd_followage(config: Config, match: Match[str]) -> str:
    username = optional_user_arg(match)
    token = config.oauth_token.split(':')[1]

    fetched_users = await fetch_twitch_user(
        config.channel,
        oauth_token=token,
        client_id=config.client_id,
    )
    assert fetched_users is not None
    me, = fetched_users

    fetched_users = await fetch_twitch_user(
        username,
        oauth_token=token,
        client_id=config.client_id,
    )
    if not fetched_users:
        return format_msg(match, f'user {esc(username)} not found!')
    target_user, = fetched_users

    # if streamer wants to check the followage to their own channel
    if me['id'] == target_user['id']:
        return format_msg(
            match,
            f"@{esc(target_user['login'])}, you can't check !followage "
            f'to your own channel.  But I appreciate your curiosity!',
        )

    follow_age_results = await fetch_twitch_user_follows(
        from_id=target_user['id'],
        to_id=me['id'],
        oauth_token=token,
        client_id=config.client_id,
    )
    if not follow_age_results:
        return format_msg(
            match,
            f'{esc(target_user["login"])} is not a follower!',
        )
    follow_age, = follow_age_results

    now = datetime.datetime.utcnow()
    date_of_follow = datetime.datetime.fromisoformat(
        # twitch sends ISO date string with "Z" at the end,
        # which python's fromisoformat method does not like
        follow_age['followed_at'].rstrip('Z'), )
    delta = now - date_of_follow
    return format_msg(
        match,
        f'{esc(follow_age["from_name"])} has been following for '
        f'{esc(humanize.naturaldelta(delta))}!',
    )
Esempio n. 5
0
async def cmd_vimbitsrank(config: Config, match: Match[str]) -> str:
    user = optional_user_arg(match)
    async with aiosqlite.connect('db.db') as db:
        ret = await _user_rank_by_bits(user, db)
        if ret is None:
            return format_msg(match, f'user not found {esc(user)}')
        else:
            rank, n = ret
            return format_msg(
                match,
                f'{esc(user)} is ranked #{rank} with {n} vim bits',
            )
Esempio n. 6
0
async def cmd_chatplot(config: Config, match: Match[str]) -> str:
    user = optional_user_arg(match).lower()

    min_date = datetime.date.fromisoformat(_log_start_date())
    x: list[int] = []
    y = []

    for filename in sorted(os.listdir('logs')):
        if filename == f'{datetime.date.today()}.log':
            continue

        filename_date = datetime.date.fromisoformat(filename.split('.')[0])

        full_filename = os.path.join('logs', filename)
        counts = _counts_per_file(full_filename, CHAT_LOG_RE)
        if x or counts[user]:
            x.append((filename_date - min_date).days)
            y.append(counts[user])

    if len(x) < 2:
        return format_msg(
            match, f'sorry {esc(user)}, need at least 2 days of data',
        )

    m, c = lin_regr(x, y)

    chart = {
        'type': 'scatter',
        'data': {
            'datasets': [
                {
                    'label': 'chats',
                    'data': [
                        {'x': x_i, 'y': y_i}
                        for x_i, y_i in zip(x, y)
                        if y_i
                    ],
                },
                {
                    'label': 'trend',
                    'type': 'line',
                    'fill': False,
                    'pointRadius': 0,
                    'data': [
                        {'x': x[0], 'y': m * x[0] + c},
                        {'x': x[-1], 'y': m * x[-1] + c},
                    ],
                },
            ],
        },
        'options': {
            'scales': {
                'xAxes': [{'ticks': {'callback': 'CALLBACK'}}],
                'yAxes': [{'ticks': {'beginAtZero': True, 'min': 0}}],
            },
            'title': {
                'display': True,
                'text': f"{user}'s chat in twitch.tv/{config.channel}",
            },
        },
    }

    callback = (
        'x=>{'
        f'y=new Date({str(min_date)!r});'
        'y.setDate(x+y.getDate());return y.toISOString().slice(0,10)'
        '}'
    )
    data = json.dumps(chart, separators=(',', ':'))
    data = data.replace('"CALLBACK"', callback)

    post_data = {'chart': data}
    request = urllib.request.Request(
        'https://quickchart.io/chart/create',
        method='POST',
        data=json.dumps(post_data).encode(),
        headers={'Content-Type': 'application/json'},
    )
    resp = urllib.request.urlopen(request)
    contents = json.load(resp)
    return format_msg(match, f'{esc(user)}: {contents["url"]}')
Esempio n. 7
0
async def cmd_followage(config: Config, match: Match[str]) -> str:
    username = optional_user_arg(match)

    me = await fetch_twitch_user(
        config.channel,
        oauth_token=config.oauth_token_token,
        client_id=config.client_id,
    )
    assert me is not None

    target_user = await fetch_twitch_user(
        username,
        oauth_token=config.oauth_token_token,
        client_id=config.client_id,
    )
    if target_user is None:
        return format_msg(match, f'user {esc(username)} not found!')

    # if streamer wants to check the followage to their own channel
    if me['id'] == target_user['id']:
        return format_msg(
            match,
            f"@{esc(target_user['login'])}, you can't check !followage "
            f'to your own channel.  But I appreciate your curiosity!',
        )

    follow_age_results = await fetch_twitch_user_follows(
        from_id=target_user['id'],
        to_id=me['id'],
        oauth_token=config.oauth_token_token,
        client_id=config.client_id,
    )
    if not follow_age_results:
        return format_msg(
            match,
            f'{esc(target_user["login"])} is not a follower!',
        )
    follow_age, = follow_age_results

    now = datetime.datetime.utcnow()
    date_of_follow = datetime.datetime.fromisoformat(
        # twitch sends ISO date string with "Z" at the end,
        # which python's fromisoformat method does not like
        follow_age['followed_at'].rstrip('Z'),
    )
    delta = now - date_of_follow

    if delta <= datetime.timedelta(days=2):
        # using 2 days because precisedelta returns "1 days"
        humanize_string = humanize.naturaldelta(delta)
    else:
        humanize_string = humanize.precisedelta(
            delta,
            minimum_unit='days',
            format='%d',
        )

        # odd month outputs are broken because mod 30.5
        humanize_string = humanize_string.replace(' 1 days', ' 1 day')
        humanize_string = humanize_string.replace(' and 0 days', '')
    return format_msg(
        match,
        f'{esc(follow_age["from_name"])} has been following for '
        f'{esc(humanize_string)}!',
    )
Esempio n. 8
0
async def cmd_chatplot(config: Config, match: Match[str]) -> str:
    user_list = optional_user_arg(match).lower().split()
    user_list = [_alias(user.lstrip('@')) for user in user_list]
    user_list = list(dict.fromkeys(user_list))

    if len(user_list) > 2:
        return format_msg(match, 'sorry, can only compare 2 users')

    min_date = datetime.date.fromisoformat(_log_start_date())
    comp_users: dict[str, dict[str, list[int]]]
    comp_users = collections.defaultdict(lambda: {'x': [], 'y': []})
    for filename in sorted(os.listdir('logs')):
        if filename == f'{datetime.date.today()}.log':
            continue

        filename_date = datetime.date.fromisoformat(filename.split('.')[0])

        full_filename = os.path.join('logs', filename)
        counts = _counts_per_file(full_filename, CHAT_LOG_RE)
        for user in user_list:
            if counts[user]:
                comp_users[user]['x'].append((filename_date - min_date).days)
                comp_users[user]['y'].append(counts[user])

    # create the datasets (scatter and trend line) for all users to compare
    PLOT_COLORS = ('#00a3ce', '#fab040')
    datasets: list[dict[str, Any]] = []
    for user, color in zip(user_list, PLOT_COLORS):
        if len(comp_users[user]['x']) < 2:
            if len(user_list) > 1:
                return format_msg(
                    match,
                    'sorry, all users need at least 2 days of data',
                )
            else:
                return format_msg(
                    match,
                    f'sorry {esc(user)}, need at least 2 days of data',
                )

        point_data = {
            'label': f"{user}'s chats",
            'borderColor': color,
            # add alpha to the point fill color
            'backgroundColor': f'{color}69',
            'data': [
                {'x': x_i, 'y': y_i}
                for x_i, y_i in
                zip(comp_users[user]['x'], comp_users[user]['y'])
                if y_i
            ],
        }
        m, c = lin_regr(comp_users[user]['x'], comp_users[user]['y'])
        trend_data = {
            'borderColor': color,
            'type': 'line',
            'fill': False,
            'pointRadius': 0,
            'data': [
                {
                    'x': comp_users[user]['x'][0],
                    'y': m * comp_users[user]['x'][0] + c,
                },
                {
                    'x': comp_users[user]['x'][-1],
                    'y': m * comp_users[user]['x'][-1] + c,
                },
            ],
        }
        datasets.append(point_data)
        datasets.append(trend_data)

    # generate title checking if we are comparing users
    if len(user_list) > 1:
        title_user = "******".join(user_list)
        title_user = f"{title_user}'s"
    else:
        title_user = f"{user_list[0]}'s"

    chart = {
        'type': 'scatter',
        'data': {
            'datasets': datasets,
        },
        'options': {
            'scales': {
                'xAxes': [{'ticks': {'callback': 'CALLBACK'}}],
                'yAxes': [{'ticks': {'beginAtZero': True, 'min': 0}}],
            },
            'title': {
                'display': True,
                'text': f'{title_user} chat in twitch.tv/{config.channel}',
            },
            'legend': {
                'labels': {'filter': 'FILTER'},
            },
        },
    }

    callback = (
        'x=>{'
        f'y=new Date({str(min_date)!r});'
        'y.setDate(x+y.getDate());return y.toISOString().slice(0,10)'
        '}'
    )
    # https://github.com/chartjs/Chart.js/issues/3189#issuecomment-528362213
    filter = (
        '(legendItem, chartData)=>{'
        '  return (chartData.datasets[legendItem.datasetIndex].label);'
        '}'
    )
    data = json.dumps(chart, separators=(',', ':'))
    data = data.replace('"CALLBACK"', callback)
    data = data.replace('"FILTER"', filter)

    post_data = {'chart': data}
    request = urllib.request.Request(
        'https://quickchart.io/chart/create',
        method='POST',
        data=json.dumps(post_data).encode(),
        headers={'Content-Type': 'application/json'},
    )
    resp = urllib.request.urlopen(request)
    contents = json.load(resp)
    user_esc = [esc(user) for user in user_list]
    if len(user_list) > 1:
        return format_msg(
            match,
            f'comparing {", ".join(user_esc)}: {contents["url"]}',
        )
    else:
        return format_msg(match, f'{esc(user_esc[0])}: {contents["url"]}')