test(client): high-level methods and types tests
This commit is contained in:
parent
1b6e41a709
commit
42c3b2c809
13 changed files with 1100 additions and 110 deletions
|
@ -8,7 +8,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install .config/husky",
|
"prepare": "husky install .config/husky",
|
||||||
"postinstall": "node scripts/validate-deps-versions.mjs",
|
"postinstall": "node scripts/validate-deps-versions.mjs",
|
||||||
"test": "vitest --config .config/vite.ts run && pnpm run -r test",
|
"test": "vitest --config .config/vite.mts run && pnpm run -r test",
|
||||||
"test:dev": "vitest --config .config/vite.mts watch",
|
"test:dev": "vitest --config .config/vite.mts watch",
|
||||||
"test:ui": "vitest --config .config/vite.mts --ui",
|
"test:ui": "vitest --config .config/vite.mts --ui",
|
||||||
"test:coverage": "vitest --config .config/vite.mts run --coverage",
|
"test:coverage": "vitest --config .config/vite.mts run --coverage",
|
||||||
|
|
37
packages/client/src/methods/users/get-users.test.ts
Normal file
37
packages/client/src/methods/users/get-users.test.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { Long } from '@mtcute/core'
|
||||||
|
import { assertTypeIs } from '@mtcute/core/utils.js'
|
||||||
|
import { createStub, StubTelegramClient } from '@mtcute/test'
|
||||||
|
|
||||||
|
import { User } from '../../types/index.js'
|
||||||
|
import { getUsers } from './get-users.js'
|
||||||
|
|
||||||
|
describe('getUsers', () => {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
client.respondWith('users.getUsers', ({ id }) =>
|
||||||
|
id.map((it) => {
|
||||||
|
assertTypeIs('', it, 'inputUser')
|
||||||
|
|
||||||
|
if (it.userId === 1) return { _: 'userEmpty', id: 1 }
|
||||||
|
|
||||||
|
return createStub('user', { id: it.userId, accessHash: Long.ZERO })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it('should return users returned by users.getUsers', async () => {
|
||||||
|
expect(await getUsers(client, [123, 456])).toEqual([
|
||||||
|
new User(createStub('user', { id: 123, accessHash: Long.ZERO })),
|
||||||
|
new User(createStub('user', { id: 456, accessHash: Long.ZERO })),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work for one user', async () => {
|
||||||
|
expect(await getUsers(client, 123)).toEqual([new User(createStub('user', { id: 123, accessHash: Long.ZERO }))])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ignore userEmpty', async () => {
|
||||||
|
expect(await getUsers(client, 1)).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
315
packages/client/src/methods/users/resolve-peer.test.ts
Normal file
315
packages/client/src/methods/users/resolve-peer.test.ts
Normal file
|
@ -0,0 +1,315 @@
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { Long } from '@mtcute/core'
|
||||||
|
import { createStub, StubTelegramClient } from '@mtcute/test'
|
||||||
|
|
||||||
|
import { Chat, MtPeerNotFoundError, User } from '../../types/index.js'
|
||||||
|
import { getAuthState } from '../auth/_state.js'
|
||||||
|
import { resolvePeer } from './resolve-peer.js'
|
||||||
|
|
||||||
|
describe('resolvePeer', () => {
|
||||||
|
it('should extract input peer from User/Chat', async () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const chat = new Chat(
|
||||||
|
createStub('channel', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(await resolvePeer(StubTelegramClient.offline(), user)).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
expect(await resolvePeer(StubTelegramClient.offline(), chat)).toEqual({
|
||||||
|
_: 'inputPeerChannel',
|
||||||
|
channelId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should extract input peer from tl objects', async () => {
|
||||||
|
const user = createStub('inputPeerUser', {
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await resolvePeer(StubTelegramClient.offline(), user)).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return inputPeerSelf for me/self', async () => {
|
||||||
|
expect(await resolvePeer(StubTelegramClient.offline(), 'me')).toEqual({ _: 'inputPeerSelf' })
|
||||||
|
expect(await resolvePeer(StubTelegramClient.offline(), 'self')).toEqual({ _: 'inputPeerSelf' })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resolving by id', () => {
|
||||||
|
describe('users', () => {
|
||||||
|
it('should first try checking in storage', async () => {
|
||||||
|
const client = StubTelegramClient.offline()
|
||||||
|
|
||||||
|
await client.registerPeers(
|
||||||
|
createStub('user', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, 123)
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return user with zero hash if not in storage', async () => {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, 123)
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.ZERO,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return user with zero hash for bots', async () => {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
getAuthState(client).isBot = true
|
||||||
|
|
||||||
|
await client.registerPeers(
|
||||||
|
createStub('user', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, 123)
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.ZERO,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('channels', () => {
|
||||||
|
it('should first try checking in storage', async () => {
|
||||||
|
const client = StubTelegramClient.offline()
|
||||||
|
|
||||||
|
await client.registerPeers(
|
||||||
|
createStub('channel', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, -1000000000123)
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerChannel',
|
||||||
|
channelId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return channel with zero hash if not in storage', async () => {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, -1000000000123)
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerChannel',
|
||||||
|
channelId: 123,
|
||||||
|
accessHash: Long.ZERO,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('chats', () => {
|
||||||
|
it('should always return zero hash', async () => {
|
||||||
|
const client = StubTelegramClient.offline()
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, -123)
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerChat',
|
||||||
|
chatId: 123,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should accept Peer objects', async () => {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
await client.registerPeers(
|
||||||
|
createStub('user', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, { _: 'peerUser', userId: 123 })
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resolving by phone number', () => {
|
||||||
|
it('should first try checking in storage', async () => {
|
||||||
|
const client = StubTelegramClient.offline()
|
||||||
|
|
||||||
|
await client.registerPeers(
|
||||||
|
createStub('user', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
phone: '123456789',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, '+123456789')
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call contacts.resolvePhone if not in storage', async () => {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
const resolvePhoneFn = client.respondWith(
|
||||||
|
'contacts.resolvePhone',
|
||||||
|
vi.fn().mockReturnValue({
|
||||||
|
_: 'contacts.resolvedPeer',
|
||||||
|
peer: {
|
||||||
|
_: 'peerUser',
|
||||||
|
userId: 123,
|
||||||
|
},
|
||||||
|
users: [
|
||||||
|
createStub('user', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, '+123456789')
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
expect(resolvePhoneFn).toHaveBeenCalledWith({
|
||||||
|
_: 'contacts.resolvePhone',
|
||||||
|
phone: '123456789',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty response from contacts.resolvePhone', async () => {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
const resolvePhoneFn = vi.fn().mockReturnValue({
|
||||||
|
_: 'contacts.resolvedPeer',
|
||||||
|
peer: {
|
||||||
|
_: 'peerUser',
|
||||||
|
userId: 123,
|
||||||
|
},
|
||||||
|
users: [],
|
||||||
|
})
|
||||||
|
client.respondWith('contacts.resolvePhone', resolvePhoneFn)
|
||||||
|
|
||||||
|
await expect(() => resolvePeer(client, '+123456789')).rejects.toThrow(MtPeerNotFoundError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('resolving by username', () => {
|
||||||
|
it('should first try checking in storage', async () => {
|
||||||
|
const client = StubTelegramClient.offline()
|
||||||
|
|
||||||
|
await client.registerPeers(
|
||||||
|
createStub('user', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
username: 'test',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, 'test')
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call contacts.resolveUsername if not in storage', async () => {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
const resolveUsernameFn = vi.fn().mockReturnValue({
|
||||||
|
_: 'contacts.resolvedPeer',
|
||||||
|
peer: {
|
||||||
|
_: 'peerChannel',
|
||||||
|
channelId: 123,
|
||||||
|
},
|
||||||
|
chats: [
|
||||||
|
createStub('channel', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
client.respondWith('contacts.resolveUsername', resolveUsernameFn)
|
||||||
|
|
||||||
|
const resolved = await resolvePeer(client, 'test')
|
||||||
|
|
||||||
|
expect(resolved).toEqual({
|
||||||
|
_: 'inputPeerChannel',
|
||||||
|
channelId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
expect(resolveUsernameFn).toHaveBeenCalledWith({
|
||||||
|
_: 'contacts.resolveUsername',
|
||||||
|
username: 'test',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty response from contacts.resolveUsername', async () => {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
const resolveUsernameFn = vi.fn().mockReturnValue({
|
||||||
|
_: 'contacts.resolvedPeer',
|
||||||
|
peer: {
|
||||||
|
_: 'peerChannel',
|
||||||
|
channelId: 123,
|
||||||
|
},
|
||||||
|
chats: [],
|
||||||
|
})
|
||||||
|
client.respondWith('contacts.resolveUsername', resolveUsernameFn)
|
||||||
|
|
||||||
|
await expect(() => resolvePeer(client, 'test')).rejects.toThrow(MtPeerNotFoundError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { MtPeerNotFoundError } from '../../types/errors.js'
|
import { MtPeerNotFoundError } from '../../types/errors.js'
|
||||||
import { InputPeerLike } from '../../types/peers/index.js'
|
import { InputPeerLike } from '../../types/peers/index.js'
|
||||||
import { normalizeToInputPeer } from '../../utils/peer-utils.js'
|
import { normalizeToInputPeer } from '../../utils/peer-utils.js'
|
||||||
|
import { getAuthState } from '../auth/_state.js'
|
||||||
|
|
||||||
// @available=both
|
// @available=both
|
||||||
/**
|
/**
|
||||||
|
@ -18,7 +19,7 @@ import { normalizeToInputPeer } from '../../utils/peer-utils.js'
|
||||||
* Useful when an `InputPeer` is needed in Raw API.
|
* Useful when an `InputPeer` is needed in Raw API.
|
||||||
*
|
*
|
||||||
* @param peerId The peer identifier that you want to extract the `InputPeer` from.
|
* @param peerId The peer identifier that you want to extract the `InputPeer` from.
|
||||||
* @param force Whether to force re-fetch the peer from the server
|
* @param force Whether to force re-fetch the peer from the server (only applicable for usernames and phone numbers)
|
||||||
*/
|
*/
|
||||||
export async function resolvePeer(
|
export async function resolvePeer(
|
||||||
client: BaseTelegramClient,
|
client: BaseTelegramClient,
|
||||||
|
@ -37,7 +38,7 @@ export async function resolvePeer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof peerId === 'number' && !force) {
|
if (typeof peerId === 'number' && !force && !getAuthState(client).isBot) {
|
||||||
const fromStorage = await client.storage.getPeerById(peerId)
|
const fromStorage = await client.storage.getPeerById(peerId)
|
||||||
if (fromStorage) return fromStorage
|
if (fromStorage) return fromStorage
|
||||||
}
|
}
|
||||||
|
@ -123,81 +124,32 @@ export async function resolvePeer(
|
||||||
throw new MtPeerNotFoundError(`Could not find a peer by ${peerId}`)
|
throw new MtPeerNotFoundError(`Could not find a peer by ${peerId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// in some cases, the server allows us to use access_hash=0.
|
||||||
|
// particularly, when we're a bot or we're referencing a user
|
||||||
|
// who we have "seen" recently
|
||||||
|
// if it's not the case, we'll get an `PEER_ID_INVALID` error anyways
|
||||||
const peerType = getBasicPeerType(peerId)
|
const peerType = getBasicPeerType(peerId)
|
||||||
|
|
||||||
// try fetching by id, with access_hash set to 0
|
|
||||||
switch (peerType) {
|
switch (peerType) {
|
||||||
case 'user': {
|
case 'user':
|
||||||
const res = await client.call({
|
return {
|
||||||
_: 'users.getUsers',
|
_: 'inputPeerUser',
|
||||||
id: [
|
userId: peerId,
|
||||||
{
|
accessHash: Long.ZERO,
|
||||||
_: 'inputUser',
|
|
||||||
userId: peerId,
|
|
||||||
accessHash: Long.ZERO,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const found = res.find((it) => it.id === peerId)
|
|
||||||
|
|
||||||
if (found && found._ === 'user') {
|
|
||||||
if (!found.accessHash) {
|
|
||||||
// shouldn't happen? but just in case
|
|
||||||
throw new MtPeerNotFoundError(
|
|
||||||
`Peer (user) with username ${peerId} was found, but it has no access hash`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
_: 'inputPeerUser',
|
|
||||||
userId: found.id,
|
|
||||||
accessHash: found.accessHash,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
case 'chat':
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'chat': {
|
|
||||||
return {
|
return {
|
||||||
_: 'inputPeerChat',
|
_: 'inputPeerChat',
|
||||||
chatId: -peerId,
|
chatId: -peerId,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case 'channel': {
|
case 'channel': {
|
||||||
const id = toggleChannelIdMark(peerId)
|
const id = toggleChannelIdMark(peerId)
|
||||||
|
|
||||||
const res = await client.call({
|
return {
|
||||||
_: 'channels.getChannels',
|
_: 'inputPeerChannel',
|
||||||
id: [
|
channelId: id,
|
||||||
{
|
accessHash: Long.ZERO,
|
||||||
_: 'inputChannel',
|
|
||||||
channelId: id,
|
|
||||||
accessHash: Long.ZERO,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const found = res.chats.find((it) => it.id === id)
|
|
||||||
|
|
||||||
if (found && (found._ === 'channel' || found._ === 'channelForbidden')) {
|
|
||||||
if (!found.accessHash) {
|
|
||||||
// shouldn't happen? but just in case
|
|
||||||
throw new MtPeerNotFoundError(
|
|
||||||
`Peer (channel) with username ${peerId} was found, but it has no access hash`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
_: 'inputPeerChannel',
|
|
||||||
channelId: found.id,
|
|
||||||
accessHash: found.accessHash ?? Long.ZERO,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new MtPeerNotFoundError(`Could not find a peer by ID ${peerId}`)
|
|
||||||
}
|
}
|
||||||
|
|
196
packages/client/src/types/bots/keyboard-builder.test.ts
Normal file
196
packages/client/src/types/bots/keyboard-builder.test.ts
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { BotKeyboardBuilder } from './keyboard-builder.js'
|
||||||
|
|
||||||
|
describe('BotKeyboardBuilder', () => {
|
||||||
|
describe('#push', () => {
|
||||||
|
it('should add buttons', () => {
|
||||||
|
const builder = new BotKeyboardBuilder()
|
||||||
|
|
||||||
|
builder.push(
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
{ _: 'keyboardButton', text: '3' },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(builder.asInline()).toEqual({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
{ _: 'keyboardButton', text: '3' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should wrap long rows buttons', () => {
|
||||||
|
const builder = new BotKeyboardBuilder(3)
|
||||||
|
|
||||||
|
builder.push(
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
{ _: 'keyboardButton', text: '3' },
|
||||||
|
{ _: 'keyboardButton', text: '4' },
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(builder.asInline()).toEqual({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
{ _: 'keyboardButton', text: '3' },
|
||||||
|
],
|
||||||
|
[{ _: 'keyboardButton', text: '4' }],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should always add a new row', () => {
|
||||||
|
const builder = new BotKeyboardBuilder(3)
|
||||||
|
|
||||||
|
builder.push({ _: 'keyboardButton', text: '1' })
|
||||||
|
builder.push({ _: 'keyboardButton', text: '2' })
|
||||||
|
builder.push({ _: 'keyboardButton', text: '3' })
|
||||||
|
|
||||||
|
expect(builder.asInline()).toEqual({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [
|
||||||
|
[{ _: 'keyboardButton', text: '1' }],
|
||||||
|
[{ _: 'keyboardButton', text: '2' }],
|
||||||
|
[{ _: 'keyboardButton', text: '3' }],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should accept functions and falsy values', () => {
|
||||||
|
const builder = new BotKeyboardBuilder(3)
|
||||||
|
|
||||||
|
builder.push({ _: 'keyboardButton', text: '1' })
|
||||||
|
builder.push(() => ({ _: 'keyboardButton', text: '2' }))
|
||||||
|
builder.push(1 > 1 && { _: 'keyboardButton', text: '3' })
|
||||||
|
|
||||||
|
expect(builder.asInline()).toEqual({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [[{ _: 'keyboardButton', text: '1' }], [{ _: 'keyboardButton', text: '2' }]],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#append', () => {
|
||||||
|
it('should append (or wrap) to the last row', () => {
|
||||||
|
const builder = new BotKeyboardBuilder(3)
|
||||||
|
|
||||||
|
builder.append({ _: 'keyboardButton', text: '1' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '2' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '3' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '4' })
|
||||||
|
|
||||||
|
expect(builder.asInline()).toEqual({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
{ _: 'keyboardButton', text: '3' },
|
||||||
|
],
|
||||||
|
[{ _: 'keyboardButton', text: '4' }],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accept functions and falsy values', () => {
|
||||||
|
const builder = new BotKeyboardBuilder(3)
|
||||||
|
|
||||||
|
builder.append({ _: 'keyboardButton', text: '1' })
|
||||||
|
builder.append(() => ({ _: 'keyboardButton', text: '2' }))
|
||||||
|
builder.append(1 > 1 && { _: 'keyboardButton', text: '3' })
|
||||||
|
|
||||||
|
expect(builder.asInline()).toEqual({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should accept custom row size', () => {
|
||||||
|
const builder = new BotKeyboardBuilder(5)
|
||||||
|
|
||||||
|
builder.append({ _: 'keyboardButton', text: '1' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '2' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '3' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '4' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '5' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '6' })
|
||||||
|
|
||||||
|
expect(builder.asInline()).toEqual({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
{ _: 'keyboardButton', text: '3' },
|
||||||
|
{ _: 'keyboardButton', text: '4' },
|
||||||
|
{ _: 'keyboardButton', text: '5' },
|
||||||
|
],
|
||||||
|
[{ _: 'keyboardButton', text: '6' }],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('#row should add entire rows of buttons', () => {
|
||||||
|
const builder = new BotKeyboardBuilder(3)
|
||||||
|
|
||||||
|
builder.row([
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
{ _: 'keyboardButton', text: '3' },
|
||||||
|
{ _: 'keyboardButton', text: '4' },
|
||||||
|
{ _: 'keyboardButton', text: '5' },
|
||||||
|
])
|
||||||
|
builder.append({ _: 'keyboardButton', text: '6' })
|
||||||
|
|
||||||
|
expect(builder.asInline()).toEqual({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
{ _: 'keyboardButton', text: '3' },
|
||||||
|
{ _: 'keyboardButton', text: '4' },
|
||||||
|
{ _: 'keyboardButton', text: '5' },
|
||||||
|
],
|
||||||
|
[{ _: 'keyboardButton', text: '6' }],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support reply keyboards', () => {
|
||||||
|
const builder = new BotKeyboardBuilder(3)
|
||||||
|
|
||||||
|
builder.append({ _: 'keyboardButton', text: '1' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '2' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '3' })
|
||||||
|
builder.append({ _: 'keyboardButton', text: '4' })
|
||||||
|
|
||||||
|
expect(builder.asReply({ resize: true })).toEqual({
|
||||||
|
type: 'reply',
|
||||||
|
resize: true,
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButton', text: '1' },
|
||||||
|
{ _: 'keyboardButton', text: '2' },
|
||||||
|
{ _: 'keyboardButton', text: '3' },
|
||||||
|
],
|
||||||
|
[{ _: 'keyboardButton', text: '4' }],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -72,7 +72,7 @@ export class BotKeyboardBuilder {
|
||||||
this._buttons.length &&
|
this._buttons.length &&
|
||||||
(this.maxRowWidth === null || force || this._buttons[this._buttons.length - 1].length < this.maxRowWidth)
|
(this.maxRowWidth === null || force || this._buttons[this._buttons.length - 1].length < this.maxRowWidth)
|
||||||
) {
|
) {
|
||||||
this._buttons[this._buttons.length - 1].push()
|
this._buttons[this._buttons.length - 1].push(btn)
|
||||||
} else {
|
} else {
|
||||||
this._buttons.push([btn])
|
this._buttons.push([btn])
|
||||||
}
|
}
|
||||||
|
|
167
packages/client/src/types/bots/keyboards.test.ts
Normal file
167
packages/client/src/types/bots/keyboards.test.ts
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { tl } from '@mtcute/core'
|
||||||
|
|
||||||
|
import { BotKeyboard } from './keyboards.js'
|
||||||
|
|
||||||
|
describe('findButton', () => {
|
||||||
|
const kb: tl.TypeKeyboardButton[][] = [
|
||||||
|
[{ _: 'keyboardButton', text: 'aaa' }],
|
||||||
|
[{ _: 'keyboardButton', text: 'a' }],
|
||||||
|
[{ _: 'keyboardButton', text: 'b' }],
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should find buttons by text', () => {
|
||||||
|
expect(BotKeyboard.findButton(kb, 'a')).toEqual({
|
||||||
|
_: 'keyboardButton',
|
||||||
|
text: 'a',
|
||||||
|
})
|
||||||
|
expect(BotKeyboard.findButton(kb, 'c')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should find buttons by predicate', () => {
|
||||||
|
expect(BotKeyboard.findButton(kb, (s) => s._ === 'keyboardButton')).toEqual({
|
||||||
|
_: 'keyboardButton',
|
||||||
|
text: 'aaa',
|
||||||
|
})
|
||||||
|
expect(BotKeyboard.findButton(kb, 'c')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('_convertToTl', () => {
|
||||||
|
it('should convert reply markup', () => {
|
||||||
|
expect(
|
||||||
|
BotKeyboard._convertToTl({
|
||||||
|
type: 'reply',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButton', text: 'a' },
|
||||||
|
{ _: 'keyboardButton', text: 'b' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
resize: true,
|
||||||
|
singleUse: true,
|
||||||
|
selective: true,
|
||||||
|
persistent: true,
|
||||||
|
placeholder: 'whatever',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
_: 'replyKeyboardMarkup',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
_: 'keyboardButtonRow',
|
||||||
|
buttons: [
|
||||||
|
{ _: 'keyboardButton', text: 'a' },
|
||||||
|
{ _: 'keyboardButton', text: 'b' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resize: true,
|
||||||
|
singleUse: true,
|
||||||
|
selective: true,
|
||||||
|
persistent: true,
|
||||||
|
placeholder: 'whatever',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert inline markup', () => {
|
||||||
|
expect(
|
||||||
|
BotKeyboard._convertToTl({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButton', text: 'a' },
|
||||||
|
{ _: 'keyboardButton', text: 'b' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
_: 'replyInlineMarkup',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
_: 'keyboardButtonRow',
|
||||||
|
buttons: [
|
||||||
|
{ _: 'keyboardButton', text: 'a' },
|
||||||
|
{ _: 'keyboardButton', text: 'b' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert reply hide markup', () => {
|
||||||
|
expect(
|
||||||
|
BotKeyboard._convertToTl({
|
||||||
|
type: 'reply_hide',
|
||||||
|
selective: true,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
_: 'replyKeyboardHide',
|
||||||
|
selective: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert force reply markup', () => {
|
||||||
|
expect(
|
||||||
|
BotKeyboard._convertToTl({
|
||||||
|
type: 'force_reply',
|
||||||
|
selective: true,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
_: 'replyKeyboardForceReply',
|
||||||
|
selective: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('webview', () => {
|
||||||
|
it('should replace keyboardButtonWebView with keyboardButtonSimpleWebView for reply keyboards', () => {
|
||||||
|
expect(
|
||||||
|
BotKeyboard._convertToTl({
|
||||||
|
type: 'reply',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButtonWebView', text: 'a', url: 'https://google.com' },
|
||||||
|
{ _: 'keyboardButtonWebView', text: 'b', url: 'https://google.com' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
_: 'replyKeyboardMarkup',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
_: 'keyboardButtonRow',
|
||||||
|
buttons: [
|
||||||
|
{ _: 'keyboardButtonSimpleWebView', text: 'a', url: 'https://google.com' },
|
||||||
|
{ _: 'keyboardButtonSimpleWebView', text: 'b', url: 'https://google.com' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep keyboardButtonWebView for inline keyboards', () => {
|
||||||
|
expect(
|
||||||
|
BotKeyboard._convertToTl({
|
||||||
|
type: 'inline',
|
||||||
|
buttons: [
|
||||||
|
[
|
||||||
|
{ _: 'keyboardButtonWebView', text: 'a', url: 'https://google.com' },
|
||||||
|
{ _: 'keyboardButtonWebView', text: 'b', url: 'https://google.com' },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
_: 'replyInlineMarkup',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
_: 'keyboardButtonRow',
|
||||||
|
buttons: [
|
||||||
|
{ _: 'keyboardButtonWebView', text: 'a', url: 'https://google.com' },
|
||||||
|
{ _: 'keyboardButtonWebView', text: 'b', url: 'https://google.com' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -437,6 +437,8 @@ export namespace BotKeyboard {
|
||||||
resize: obj.resize,
|
resize: obj.resize,
|
||||||
singleUse: obj.singleUse,
|
singleUse: obj.singleUse,
|
||||||
selective: obj.selective,
|
selective: obj.selective,
|
||||||
|
persistent: obj.persistent,
|
||||||
|
placeholder: obj.placeholder,
|
||||||
rows: _2dToRows(obj.buttons, false),
|
rows: _2dToRows(obj.buttons, false),
|
||||||
}
|
}
|
||||||
case 'reply_hide':
|
case 'reply_hide':
|
||||||
|
@ -449,6 +451,7 @@ export namespace BotKeyboard {
|
||||||
_: 'replyKeyboardForceReply',
|
_: 'replyKeyboardForceReply',
|
||||||
singleUse: obj.singleUse,
|
singleUse: obj.singleUse,
|
||||||
selective: obj.selective,
|
selective: obj.selective,
|
||||||
|
placeholder: obj.placeholder,
|
||||||
}
|
}
|
||||||
case 'inline':
|
case 'inline':
|
||||||
return {
|
return {
|
||||||
|
|
86
packages/client/src/types/peers/peers-index.test.ts
Normal file
86
packages/client/src/types/peers/peers-index.test.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { MtArgumentError } from '@mtcute/core/src/index.js'
|
||||||
|
import { createStub } from '@mtcute/test'
|
||||||
|
|
||||||
|
import { PeersIndex } from './peers-index.js'
|
||||||
|
|
||||||
|
describe('PeersIndex', () => {
|
||||||
|
it('should build the index from an object with users/chats fields', () => {
|
||||||
|
const obj = {
|
||||||
|
users: [createStub('user', { id: 1 }), createStub('user', { id: 2 })],
|
||||||
|
chats: [createStub('chat', { id: 1 }), createStub('channel', { id: 2 })],
|
||||||
|
}
|
||||||
|
|
||||||
|
const peers = PeersIndex.from(obj)
|
||||||
|
|
||||||
|
expect(peers.users.size).toBe(2)
|
||||||
|
expect(peers.chats.size).toBe(2)
|
||||||
|
expect(peers.users.get(1)).toBe(obj.users[0])
|
||||||
|
expect(peers.users.get(2)).toBe(obj.users[1])
|
||||||
|
expect(peers.chats.get(1)).toBe(obj.chats[0])
|
||||||
|
expect(peers.chats.get(2)).toBe(obj.chats[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should detect min peers and set hasMin', () => {
|
||||||
|
const obj = {
|
||||||
|
users: [createStub('user', { id: 1 }), createStub('user', { id: 2, min: true })],
|
||||||
|
chats: [createStub('chat', { id: 1 }), createStub('channel', { id: 2, min: true })],
|
||||||
|
}
|
||||||
|
|
||||||
|
const peers = PeersIndex.from(obj)
|
||||||
|
|
||||||
|
expect(peers.hasMin).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#user', () => {
|
||||||
|
it('should find user info by its id', () => {
|
||||||
|
const peers = PeersIndex.from({
|
||||||
|
users: [createStub('user', { id: 1 }), createStub('user', { id: 2 })],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(peers.user(1)).toBe(peers.users.get(1))
|
||||||
|
expect(peers.user(2)).toBe(peers.users.get(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw if user is not found', () => {
|
||||||
|
const peers = PeersIndex.from({
|
||||||
|
users: [createStub('user', { id: 1 }), createStub('user', { id: 2 })],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => peers.user(3)).toThrow(MtArgumentError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#chat', () => {
|
||||||
|
it('should find chat info by its id', () => {
|
||||||
|
const peers = PeersIndex.from({
|
||||||
|
chats: [createStub('chat', { id: 1 }), createStub('channel', { id: 2 })],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(peers.chat(1)).toBe(peers.chats.get(1))
|
||||||
|
expect(peers.chat(2)).toBe(peers.chats.get(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw if chat is not found', () => {
|
||||||
|
const peers = PeersIndex.from({
|
||||||
|
chats: [createStub('chat', { id: 1 }), createStub('channel', { id: 2 })],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(() => peers.chat(3)).toThrow(MtArgumentError)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#get', () => {
|
||||||
|
it('should find peer info by Peer', () => {
|
||||||
|
const peers = PeersIndex.from({
|
||||||
|
users: [createStub('user', { id: 1 }), createStub('user', { id: 2 })],
|
||||||
|
chats: [createStub('chat', { id: 1 }), createStub('channel', { id: 2 })],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(peers.get({ _: 'peerUser', userId: 1 })).toBe(peers.users.get(1))
|
||||||
|
expect(peers.get({ _: 'peerChat', chatId: 1 })).toBe(peers.chats.get(1))
|
||||||
|
expect(peers.get({ _: 'peerChannel', channelId: 2 })).toBe(peers.chats.get(2))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
226
packages/client/src/types/peers/user.test.ts
Normal file
226
packages/client/src/types/peers/user.test.ts
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { Long } from '@mtcute/core'
|
||||||
|
import { createStub } from '@mtcute/test'
|
||||||
|
|
||||||
|
import { MessageEntity } from '../messages/index.js'
|
||||||
|
import { User } from './user.js'
|
||||||
|
|
||||||
|
describe('User', () => {
|
||||||
|
describe('inputPeer', () => {
|
||||||
|
it('should return correct input peer', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.inputPeer).toEqual({
|
||||||
|
_: 'inputPeerUser',
|
||||||
|
userId: 123,
|
||||||
|
accessHash: Long.fromBits(456, 789),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw if user has no access hash', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
id: 123,
|
||||||
|
accessHash: undefined,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(() => user.inputPeer).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('status', () => {
|
||||||
|
it('should correctly handle bot online status', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
bot: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.status).toEqual('bot')
|
||||||
|
expect(user.lastOnline).toBeNull()
|
||||||
|
expect(user.nextOffline).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should correctly handle online status', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
status: {
|
||||||
|
_: 'userStatusOnline',
|
||||||
|
expires: 1000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.status).toEqual('online')
|
||||||
|
expect(user.lastOnline).toBeNull()
|
||||||
|
expect(user.nextOffline).toEqual(new Date(1000_000))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should correctly handle offline status', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
status: {
|
||||||
|
_: 'userStatusOffline',
|
||||||
|
wasOnline: 1000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.status).toEqual('offline')
|
||||||
|
expect(user.lastOnline).toEqual(new Date(1000_000))
|
||||||
|
expect(user.nextOffline).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['userStatusRecently', 'recently'],
|
||||||
|
['userStatusLastWeek', 'within_week'],
|
||||||
|
['userStatusLastMonth', 'within_month'],
|
||||||
|
['userStatusEmpty', 'long_time_ago'],
|
||||||
|
] as const)('should correctly handle %s status', (status, expected) => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
status: {
|
||||||
|
_: status,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.status).toEqual(expected)
|
||||||
|
expect(user.lastOnline).toBeNull()
|
||||||
|
expect(user.nextOffline).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('usernames', () => {
|
||||||
|
it('should handle users with one username', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
username: 'test',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.username).toEqual('test')
|
||||||
|
expect(user.usernames).toEqual([{ _: 'username', username: 'test', active: true }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle users with multiple usernames', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
usernames: [
|
||||||
|
{ _: 'username', username: 'test', active: true },
|
||||||
|
{ _: 'username', username: 'test2', active: false },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.username).toEqual('test')
|
||||||
|
expect(user.usernames).toEqual([
|
||||||
|
{ _: 'username', username: 'test', active: true },
|
||||||
|
{ _: 'username', username: 'test2', active: false },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle users with both username and usernames', () => {
|
||||||
|
// according to docs, this shouldn't ever actually happen,
|
||||||
|
// but just in case. let's just ignore the usernames field
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
username: 'test1',
|
||||||
|
usernames: [
|
||||||
|
{ _: 'username', username: 'test2', active: true },
|
||||||
|
{ _: 'username', username: 'test3', active: false },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.username).toEqual('test1')
|
||||||
|
expect(user.usernames).toEqual([{ _: 'username', username: 'test1', active: true }])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('displayName', () => {
|
||||||
|
it('should work for users without first name', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
firstName: undefined,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.displayName).toEqual('Deleted Account')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work for users with only first name', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
firstName: 'John',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.displayName).toEqual('John')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work for users with first and last name', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.displayName).toEqual('John Doe')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mention', () => {
|
||||||
|
it('should work for users with a username', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
username: 'test',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.mention()).toEqual('@test')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work for users with multiple usernames', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
usernames: [
|
||||||
|
{ _: 'username', username: 'test', active: true },
|
||||||
|
{ _: 'username', username: 'test2', active: false },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.mention()).toEqual('@test')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should work for users without a username', () => {
|
||||||
|
const user = new User(
|
||||||
|
createStub('user', {
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user.mention()).toEqual(
|
||||||
|
new MessageEntity(
|
||||||
|
{
|
||||||
|
_: 'messageEntityMentionName',
|
||||||
|
userId: user.id,
|
||||||
|
offset: 0,
|
||||||
|
length: 8,
|
||||||
|
},
|
||||||
|
'John Doe',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -145,7 +145,7 @@ export class User {
|
||||||
return this.raw.lastName ?? null
|
return this.raw.lastName ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
static parseStatus(status: tl.TypeUserStatus, bot = false): UserParsedStatus {
|
static parseStatus(status?: tl.TypeUserStatus, bot = false): UserParsedStatus {
|
||||||
let ret: UserStatus
|
let ret: UserStatus
|
||||||
let date: Date
|
let date: Date
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ export class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _parsedStatus() {
|
private get _parsedStatus() {
|
||||||
return User.parseStatus(this.raw.status!, this.raw.bot)
|
return User.parseStatus(this.raw.status, this.raw.bot)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** User's Last Seen & Online status */
|
/** User's Last Seen & Online status */
|
||||||
|
@ -214,18 +214,27 @@ export class User {
|
||||||
|
|
||||||
/** User's or bot's username */
|
/** User's or bot's username */
|
||||||
get username(): string | null {
|
get username(): string | null {
|
||||||
return this.raw.username ?? this.raw.usernames?.[0].username ?? null
|
return this.raw.username ?? this.raw.usernames?.[0]?.username ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** User's or bot's usernames (including collectibles) */
|
/** User's or bot's usernames (including collectibles) */
|
||||||
get usernames(): ReadonlyArray<tl.RawUsername> | null {
|
get usernames(): ReadonlyArray<tl.RawUsername> | null {
|
||||||
return (
|
if (this.raw.username) {
|
||||||
this.raw.usernames ??
|
return [{ _: 'username', username: this.raw.username, active: true }]
|
||||||
(this.raw.username ? [{ _: 'username', username: this.raw.username, active: true }] : null)
|
}
|
||||||
)
|
|
||||||
|
if (!this.raw.usernames?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.raw.usernames
|
||||||
}
|
}
|
||||||
|
|
||||||
/** IETF language tag of the user's language */
|
/**
|
||||||
|
* IETF language tag of the user's language
|
||||||
|
*
|
||||||
|
* Only available in some contexts
|
||||||
|
*/
|
||||||
get language(): string | null {
|
get language(): string | null {
|
||||||
return this.raw.langCode ?? null
|
return this.raw.langCode ?? null
|
||||||
}
|
}
|
||||||
|
@ -354,7 +363,7 @@ export class User {
|
||||||
*
|
*
|
||||||
* > **Note**: This method doesn't format anything on its own.
|
* > **Note**: This method doesn't format anything on its own.
|
||||||
* > Instead, it returns a {@link MessageEntity} that can later
|
* > Instead, it returns a {@link MessageEntity} that can later
|
||||||
* > be used with `html` or `md` template tags, or `unparse` method directly.
|
* > be used with `html` or `md` template tags
|
||||||
*
|
*
|
||||||
* @param text Text of the mention.
|
* @param text Text of the mention.
|
||||||
* @example
|
* @example
|
||||||
|
@ -393,13 +402,16 @@ export class User {
|
||||||
* to actually make it permanent is to send it as a message
|
* to actually make it permanent is to send it as a message
|
||||||
* somewhere and load it from there if needed.
|
* somewhere and load it from there if needed.
|
||||||
*
|
*
|
||||||
|
* Note that some users (particularly, users with hidden forwards)
|
||||||
|
* may not be mentioned like this outside the chats you have in common.
|
||||||
|
*
|
||||||
* This method is only needed when the result will be
|
* This method is only needed when the result will be
|
||||||
* stored somewhere outside current mtcute instance (e.g. saved for later use),
|
* stored somewhere outside current mtcute instance (e.g. saved for later use),
|
||||||
* otherwise {@link mention} will be enough.
|
* otherwise {@link mention} will be enough.
|
||||||
*
|
*
|
||||||
* > **Note**: This method doesn't format anything on its own.
|
* > **Note**: This method doesn't format anything on its own.
|
||||||
* > Instead, it returns a {@link MessageEntity} that can later
|
* > Instead, it returns a {@link MessageEntity} that can later
|
||||||
* > be used with `html` or `md` template tags, or `unparse` method directly.
|
* > be used with `html` or `md` template tags
|
||||||
*
|
*
|
||||||
* > **Note**: the resulting text can only be used by clients
|
* > **Note**: the resulting text can only be used by clients
|
||||||
* > that support mtcute notation of permanent
|
* > that support mtcute notation of permanent
|
||||||
|
|
|
@ -9,7 +9,7 @@ describe('client stub', () => {
|
||||||
const client = new StubTelegramClient()
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
const stubConfig = createStub('config')
|
const stubConfig = createStub('config')
|
||||||
client.respondWith('help.getConfig', stubConfig)
|
client.respondWith('help.getConfig', () => stubConfig)
|
||||||
|
|
||||||
await client.with(async () => {
|
await client.with(async () => {
|
||||||
const result = await client.call({ _: 'help.getConfig' })
|
const result = await client.call({ _: 'help.getConfig' })
|
||||||
|
|
|
@ -50,37 +50,36 @@ export class StubTelegramClient extends BaseTelegramClient {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fake client that may not actually be used for API calls
|
||||||
|
*
|
||||||
|
* Useful for testing "offline" methods
|
||||||
|
*/
|
||||||
|
static offline() {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
client.call = (obj) => {
|
||||||
|
throw new Error(`Expected offline client to not make any API calls (method called: ${obj._})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
// some fake peers handling //
|
// some fake peers handling //
|
||||||
|
|
||||||
readonly _knownChats = new Map<number, tl.TypeChat>()
|
readonly _knownChats = new Map<number, tl.TypeChat>()
|
||||||
readonly _knownUsers = new Map<number, tl.TypeUser>()
|
readonly _knownUsers = new Map<number, tl.TypeUser>()
|
||||||
_selfId = 0
|
_selfId = 0
|
||||||
|
|
||||||
registerChat(chat: tl.TypeChat | tl.TypeChat[]): void {
|
async registerPeers(...peers: (tl.TypeUser | tl.TypeChat)[]): Promise<void> {
|
||||||
if (Array.isArray(chat)) {
|
for (const peer of peers) {
|
||||||
for (const c of chat) {
|
if (tl.isAnyUser(peer)) {
|
||||||
this.registerChat(c)
|
this._knownUsers.set(peer.id, peer)
|
||||||
|
} else {
|
||||||
|
this._knownChats.set(peer.id, peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
await this._cachePeersFrom(peer)
|
||||||
}
|
|
||||||
|
|
||||||
this._knownChats.set(chat.id, chat)
|
|
||||||
}
|
|
||||||
|
|
||||||
registerUser(user: tl.TypeUser | tl.TypeUser[]): void {
|
|
||||||
if (Array.isArray(user)) {
|
|
||||||
for (const u of user) {
|
|
||||||
this.registerUser(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this._knownUsers.set(user.id, user)
|
|
||||||
|
|
||||||
if (user._ === 'user' && user.self) {
|
|
||||||
this._selfId = user.id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,17 +149,14 @@ export class StubTelegramClient extends BaseTelegramClient {
|
||||||
this.respondWith(method, responder)
|
this.respondWith(method, responder)
|
||||||
}
|
}
|
||||||
|
|
||||||
respondWith<T extends tl.RpcMethod['_']>(
|
respondWith<
|
||||||
method: T,
|
T extends tl.RpcMethod['_'],
|
||||||
response: tl.RpcCallReturn[T] | ((data: tl.FindByName<tl.RpcMethod, T>) => tl.RpcCallReturn[T]),
|
Fn extends(data: tl.FindByName<tl.RpcMethod, T>) => MaybeAsync<tl.RpcCallReturn[T]>,
|
||||||
) {
|
>(method: T, response: Fn): Fn {
|
||||||
if (typeof response !== 'function') {
|
|
||||||
const res = response
|
|
||||||
response = () => res
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
this._responders.set(method, response as any)
|
this._responders.set(method, response as any)
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
async call<T extends tl.RpcMethod>(
|
async call<T extends tl.RpcMethod>(
|
||||||
|
|
Loading…
Reference in a new issue