Skip to content

Commit 9b20fbc

Browse files
committed
LastSeen command and fix annoying TwitchIO erros (maybe).
1 parent f47c266 commit 9b20fbc

File tree

13 files changed

+398
-13
lines changed

13 files changed

+398
-13
lines changed

bot/cogs/lastseen/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# -*- coding: utf-8 -*-
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# -*- coding: utf-8 -*-
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import annotations
3+
4+
import datetime
5+
import random
6+
from typing import TYPE_CHECKING
7+
8+
from bot.ext import Context, Response, commands
9+
from bot.models import User
10+
from bot.utils import StringTools
11+
12+
from .translations import Translations
13+
14+
if TYPE_CHECKING:
15+
from bot.bot import Gorenmu
16+
17+
18+
class LastSeenCmd(commands.CustomComponent):
19+
def __init__(self, bot: Gorenmu) -> None:
20+
self.bot = bot
21+
self.translations: Translations = Translations(bot)
22+
self.StringTools: StringTools = StringTools()
23+
24+
cooldown_rate = 3
25+
cooldown_per = 10
26+
cooldown_key = commands.BucketType.user
27+
28+
async def component_command_error(self, payload: commands.CommandErrorPayload) -> bool | None: ...
29+
30+
@commands.Component.guard()
31+
def guards_component(self, ctx: commands.Context) -> bool: # NOQA
32+
return True
33+
34+
@commands.command(name="lastseen", aliases=["ls"])
35+
async def lastseen(self, ctx: Context, *, args="") -> Response:
36+
if not args:
37+
args = ctx.channel.name
38+
name = self.StringTools.str2name(args)
39+
if name == ctx.bot.bot_nick.lower():
40+
return self.translations.LastSeen.bot(ctx)
41+
elif name == ctx.author.name.lower():
42+
return self.translations.LastSeen.author(ctx)
43+
elif not (user := await User.get_or_none(name=name)):
44+
return self.translations.Exceptions.user_not_found_name(ctx, name)
45+
elif not user.mention:
46+
offset = random.randint(10, 61)
47+
offset = user.updated_at - datetime.timedelta(minutes=offset)
48+
time = self.translations.SupportTools.TimeTools.Humanize(ctx).naturaltime(offset)
49+
return self.translations.LastSeen.not_authorized(ctx, name + self.StringTools.inv_char(), time)
50+
else:
51+
time = self.translations.SupportTools.TimeTools.Humanize(ctx).updated_a_time(user.updated_at)
52+
return self.translations.LastSeen.last_seen(ctx, name, user.channel, user.content, time)
53+
54+
55+
async def setup(bot: Gorenmu) -> None:
56+
await bot.add_component(LastSeenCmd(bot))
57+
58+
59+
async def teardown(bot: Gorenmu) -> None: ... # NOQA
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import annotations
3+
4+
from typing import TYPE_CHECKING
5+
6+
from bot.ext import Admonitions, CommandExemples, Response, TBase, TranslationBase
7+
8+
if TYPE_CHECKING:
9+
from bot.bot import Gorenmu
10+
from bot.ext import Context
11+
12+
13+
class Translations(TranslationBase):
14+
def __init__(self, bot: Gorenmu) -> None:
15+
super().__init__(bot)
16+
self.populate_subclasses()
17+
18+
class LastSeen(TBase):
19+
def __init__(self):
20+
super().__init__()
21+
22+
def bot(self, ctx: Context) -> Response:
23+
response = Response(ctx=ctx, success=False, handle=None, response_list=None)
24+
with self.lang_dict.once(self._cname):
25+
self.lang_dict.add_with("en", "I am everywhere, at every moment...")
26+
self.lang_dict.add_with(["pt_br", "pt"], "eu estou em todos os lugares, a todo momento...")
27+
return response.format_response(self._untangle_str(ctx, self._cname))
28+
29+
def author(self, ctx: Context) -> Response:
30+
response = Response(ctx=ctx, success=False, handle=None, response_list=None)
31+
with self.lang_dict.once(self._cname):
32+
self.lang_dict.add_with("en", "you were last seen here ☝️")
33+
self.lang_dict.add_with(["pt_br", "pt"], "você foi visto pela última vez aqui ☝️")
34+
return response.format_response(self._untangle_str(ctx, self._cname))
35+
36+
def not_found(self, ctx: Context, name) -> Response:
37+
response = Response(ctx=ctx, success=False, handle=None, response_list=None)
38+
with self.lang_dict.once(self._cname):
39+
self.lang_dict.add_with("en", "@{} has not been registered yet.")
40+
self.lang_dict.add_with(["pt_br", "pt"], "@{} ainda não foi registrado.")
41+
return response.format_response(self._untangle_str(ctx, self._cname), name)
42+
43+
def not_authorized(self, ctx: Context, name, delta) -> Response:
44+
response = Response(ctx=ctx, success=True, handle=None, response_list=None)
45+
with self.lang_dict.once(self._cname):
46+
self.lang_dict.add_with("en", "@{} was last seen {}")
47+
self.lang_dict.add_with(["pt_br", "pt"], "@{} foi visto ultima vez {}")
48+
return response.format_response(self._untangle_str(ctx, self._cname), name, delta)
49+
50+
def last_seen(self, ctx: Context, name, channel, content, delta) -> Response:
51+
response = Response(ctx=ctx, success=True, handle=None, response_list=None)
52+
with self.lang_dict.once(self._cname):
53+
self.lang_dict.add_with("en", "@{} was last seen in @{}: {} ({})")
54+
self.lang_dict.add_with(["pt_br", "pt"], "@{} foi visto em @{} pela última vez: {} ({})")
55+
return response.format_response(self._untangle_str(ctx, self._cname), name, channel, content, delta)
56+
57+
def deco_helper(self, ctx: Context, *args, **kwargs) -> str:
58+
with self.lang_dict.once(self._cname):
59+
self.lang_dict.add_with("en", "Used to see the last time a user was online.")
60+
self.lang_dict.add_with(["pt_br", "pt"], "Usado para ver a ultima vez que um usuário esteve online.")
61+
return self._untangle_str(ctx, self._cname)
62+
63+
def deco_usage(self, ctx: Context, prefix: str = None, *args, **kwargs) -> str:
64+
with self.lang_dict.once(self._cname):
65+
self.lang_dict.add_with("en", "Usage: {}lastseen (user)")
66+
self.lang_dict.add_with(["pt_br", "pt"], "Uso: {}lastseen (usuário)")
67+
return self._untangle_str(ctx, self._cname).format(prefix)
68+
69+
# region Hide.
70+
def deco_description(self, ctx: Context, *args, **kwargs) -> str:
71+
with self.lang_dict.once(self._cname):
72+
self.lang_dict.add_with("en", "Command used to check when a user was last seen in a chat.")
73+
self.lang_dict.add_with(
74+
["pt_br", "pt"],
75+
"Comando utilizado para verificar quando foi a ultima vez que um usuário foi visto em um chat.",
76+
)
77+
return self._untangle_str(ctx, self._cname)
78+
79+
def deco_commands(self, ctx: Context, *args, **kwargs) -> CommandExemples:
80+
with self.lang_dict.once(self._cname):
81+
self.lang_dict.add_with(
82+
"en",
83+
CommandExemples(
84+
[
85+
{
86+
"args": "",
87+
"response": "@channel_name was last seen in @other_channel: "
88+
"Message content (1 day ago)",
89+
},
90+
{
91+
"args": "user_nick",
92+
"response": "@user_nick was last seen in @other_channel: "
93+
"Message content (2 days ago)",
94+
},
95+
]
96+
),
97+
)
98+
self.lang_dict.add_with(
99+
["pt_br", "pt"],
100+
CommandExemples(
101+
[
102+
{
103+
"args": "",
104+
"response": "@nome_do_canal foi visto em @outro_canal pela última vez: "
105+
"Conteúdo da mensagem (ha 1 dia)",
106+
},
107+
{
108+
"args": "user_nick",
109+
"response": "@user_nick foi visto em @outro_canal pela última vez: "
110+
"Conteúdo da mensagem (ha 2 dia)",
111+
},
112+
]
113+
),
114+
)
115+
return self._untangle_commands(ctx, self._cname)
116+
117+
def deco_admonitions(self, ctx: Context, *args, **kwargs) -> Admonitions:
118+
with self.lang_dict.once(self._cname):
119+
self.lang_dict.add_with(
120+
"en",
121+
Admonitions(
122+
[
123+
{
124+
"admonition_type": "info",
125+
"position": "top",
126+
"title": "User Mention",
127+
"message": "If the user has disabled mentions, the content of the last "
128+
"message and exact time will not be shown.",
129+
}
130+
]
131+
),
132+
)
133+
self.lang_dict.add_with(
134+
["pt_br", "pt"],
135+
Admonitions(
136+
[
137+
{
138+
"admonition_type": "info",
139+
"position": "top",
140+
"title": "User Mention",
141+
"message": "Caso o usuário tenha desabilitado a menção o conteúdo da ultima "
142+
"mensagem e exato tempo não sera mostrado.",
143+
}
144+
]
145+
),
146+
)
147+
return self._untangle_admonitions(ctx, self._cname)
148+
149+
# endregion
150+
151+
LastSeen: LastSeen

bot/handlers/lifecycle_handler.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ async def close(self):
4343
self.bot.MarkovTask.cancel()
4444
with contextlib.suppress(asyncio.CancelledError):
4545
await self.bot.MarkovTask
46-
await super(type(self.bot), self.bot).close()
46+
if not self.bot.mock:
47+
await super(type(self.bot), self.bot).close()
4748

4849
async def event_ready(self):
4950
if not self.bot.mock:

tests/helpers/fake_db_data.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,10 @@ async def create_fake_db(bot: Gorenmu):
6666
cookie.stocked = 93 * num
6767
await cookie.save()
6868
await cookie.new_cooldown()
69-
...
69+
70+
ctx = MockContext("no_mention_user", 1234512341234, "channelname", 123456, bot)
71+
ctx.bot.channels["channelname"] = channel
72+
user = await User.create_or_update(ctx)
73+
user.saved_color = "FF4500"
74+
user.mention = False
75+
await user.save()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# -*- coding: utf-8 -*-
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# -*- coding: utf-8 -*-
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import pytest
4+
import pytest_asyncio
5+
6+
from bot.cogs.lastseen.command.lastseen import LastSeenCmd
7+
from tests.helpers.mock_classes import MockContext
8+
9+
from .test_params import Params
10+
11+
12+
@pytest_asyncio.fixture
13+
async def interact(mock_bot):
14+
return LastSeenCmd(bot=mock_bot)
15+
16+
17+
@pytest.mark.asyncio
18+
@pytest.mark.parametrize("lang, helper, usage", Params.decorators)
19+
async def test_decorators(interact, mock_context: MockContext, lang: str, helper: str, usage: str):
20+
await mock_context.prepare_context(lang)
21+
mock_context.Asserter.assert_string(
22+
interact.translations.LastSeen.deco_usage(mock_context, "+"), usage, strict=True
23+
)
24+
mock_context.Asserter.assert_string(
25+
interact.translations.LastSeen.deco_helper(mock_context, "+"), helper, strict=True
26+
)
27+
28+
29+
async def base_lastseen(
30+
interact,
31+
mock_context: MockContext,
32+
lang: str,
33+
content: str,
34+
expected: str = None,
35+
re_expected: str = None,
36+
success: bool = False,
37+
):
38+
await mock_context.prepare_context(lang)
39+
response: Response = await interact.lastseen._callback(interact, mock_context, args=content) # NOQA
40+
mock_context.Asserter.assert_string(
41+
response.response_string, expected=expected, re_expected=re_expected, strict=True
42+
)
43+
mock_context.Asserter.assert_boolean(response.success, success)
44+
45+
46+
@pytest.mark.asyncio
47+
@pytest.mark.parametrize("lang, expected", Params.bot)
48+
async def test_lastseen_bot(interact, mock_context: MockContext, lang: str, expected: str):
49+
await base_lastseen(interact, mock_context, lang=lang, content="bot_name", expected=expected, success=False)
50+
51+
52+
@pytest.mark.asyncio
53+
@pytest.mark.parametrize("lang, expected", Params.author)
54+
async def test_lastseen_author(interact: LastSeenCmd, mock_context: MockContext, lang: str, expected: str) -> None:
55+
await base_lastseen(
56+
interact=interact, mock_context=mock_context, lang=lang, content="username", expected=expected, success=False
57+
)
58+
59+
60+
@pytest.mark.asyncio
61+
@pytest.mark.parametrize("lang, expected", Params.not_found)
62+
async def test_lastseen_not_found(interact: LastSeenCmd, mock_context: MockContext, lang: str, expected: str) -> None:
63+
await base_lastseen(
64+
interact=interact,
65+
mock_context=mock_context,
66+
lang=lang,
67+
content="nonexistent_user",
68+
expected=expected,
69+
success=False,
70+
)
71+
72+
73+
@pytest.mark.asyncio
74+
@pytest.mark.parametrize("lang, expected", Params.not_authorized)
75+
async def test_lastseen_not_authorized(
76+
interact: LastSeenCmd, mock_context: MockContext, lang: str, expected: str
77+
) -> None:
78+
await base_lastseen(
79+
interact=interact,
80+
mock_context=mock_context,
81+
lang=lang,
82+
content="no_mention_user",
83+
re_expected=expected,
84+
success=True,
85+
)
86+
87+
88+
@pytest.mark.asyncio
89+
@pytest.mark.parametrize("lang, expected", Params.last_seen)
90+
async def test_lastseen_success(interact: LastSeenCmd, mock_context: MockContext, lang: str, expected: str) -> None:
91+
await base_lastseen(
92+
interact=interact,
93+
mock_context=mock_context,
94+
lang=lang,
95+
content="status_user_50",
96+
re_expected=expected,
97+
success=True,
98+
)

0 commit comments

Comments
 (0)