deno support (#52)
This commit is contained in:
commit
4acd0d58e9
59 changed files with 1575 additions and 79 deletions
|
@ -279,7 +279,7 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/bun/**'],
|
||||
files: ['packages/bun/**', 'packages/deno/**'],
|
||||
rules: {
|
||||
'import/no-unresolved': 'off',
|
||||
'no-restricted-imports': 'off',
|
||||
|
@ -291,7 +291,7 @@ module.exports = {
|
|||
rules: {
|
||||
'import/no-unresolved': 'off',
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
|
|
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
|
@ -67,7 +67,7 @@ jobs:
|
|||
- name: 'Build tests'
|
||||
run: pnpm exec vite build -c .config/vite.deno.mts
|
||||
- name: 'Run tests'
|
||||
run: cd dist/tests && deno test
|
||||
run: cd dist/tests && deno test -A --unstable-ffi
|
||||
|
||||
test-web:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
@jsr:registry=https://npm.jsr.io
|
1
e2e/deno/.gitignore
vendored
1
e2e/deno/.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
/.jsr-data
|
||||
.env
|
||||
/deno.lock
|
||||
/.sessions
|
|
@ -45,19 +45,27 @@ case "$method" in
|
|||
source .env
|
||||
fi
|
||||
|
||||
if [ -n "$DOCKER" ]; then
|
||||
export JSR_URL=http://localhost:4873
|
||||
|
||||
if [ ! -z ${DOCKER+x} ]; then
|
||||
# running behind a socat proxy seems to fix some of the docker networking issues (thx kamillaova)
|
||||
socat TCP-LISTEN:4873,fork,reuseaddr TCP4:jsr:80 &
|
||||
socat_pid=$!
|
||||
|
||||
# run `deno cache` with a few retries to make sure everything is cached
|
||||
for i in {1..5}; do
|
||||
if deno cache tests/*.ts; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
trap "kill $socat_pid" EXIT
|
||||
fi
|
||||
|
||||
export JSR_URL=http://localhost:4873
|
||||
if [ -z "$@" ]; then
|
||||
deno test -A tests/**/*.ts
|
||||
if [ $# -eq 0 ]; then
|
||||
deno test -A --unstable-ffi tests/**/*.ts
|
||||
else
|
||||
deno test -A $@
|
||||
deno test -A --unstable-ffi $@
|
||||
fi
|
||||
;;
|
||||
"run-docker")
|
||||
|
@ -69,7 +77,7 @@ case "$method" in
|
|||
if [ -d .jsr-data ]; then
|
||||
# clean up data from previous runs
|
||||
docker compose down
|
||||
rm -rf .jsr-data
|
||||
sudo rm -rf .jsr-data
|
||||
fi
|
||||
mkdir .jsr-data
|
||||
./cli.sh start
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"@mtcute/wasm": "jsr:@mtcute/wasm@*",
|
||||
"@mtcute/tl": "jsr:@mtcute/tl@*",
|
||||
"@mtcute/tl-runtime": "jsr:@mtcute/tl-runtime@*",
|
||||
"@mtcute/core": "jsr:@mtcute/core@*"
|
||||
"@mtcute/core": "jsr:@mtcute/core@*",
|
||||
"@mtcute/deno": "jsr:@mtcute/deno@*"
|
||||
}
|
||||
}
|
83
e2e/deno/tests/01.auth.ts
Normal file
83
e2e/deno/tests/01.auth.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { assertEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
|
||||
|
||||
import { MtcuteError } from '@mtcute/core'
|
||||
import { BaseTelegramClient, TelegramClient } from '@mtcute/core/client.js'
|
||||
|
||||
import { getApiParams } from '../utils.ts'
|
||||
|
||||
const getAccountId = () =>
|
||||
Math.floor(Math.random() * 10000)
|
||||
.toString()
|
||||
.padStart(4, '0')
|
||||
|
||||
Deno.test('1. authorization', { sanitizeResources: false }, async (t) => {
|
||||
await t.step('should authorize in default dc', async () => {
|
||||
const base = new BaseTelegramClient(getApiParams('dc2.session'))
|
||||
const tg = new TelegramClient({ client: base })
|
||||
|
||||
// reset storage just in case
|
||||
await base.mt.storage.load()
|
||||
await base.storage.clear(true)
|
||||
|
||||
while (true) {
|
||||
const phone = `999662${getAccountId()}`
|
||||
let user
|
||||
|
||||
try {
|
||||
user = await tg.start({
|
||||
phone,
|
||||
code: () => '22222',
|
||||
})
|
||||
} catch (e) {
|
||||
if (e instanceof MtcuteError && e.message.match(/Signup is no longer supported|2FA is enabled/)) {
|
||||
// retry with another number
|
||||
continue
|
||||
} else {
|
||||
await tg.close()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
await tg.close()
|
||||
|
||||
assertEquals(user.isSelf, true)
|
||||
assertEquals(user.phoneNumber, phone)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
await t.step('should authorize in dc 1', async () => {
|
||||
const base = new BaseTelegramClient(getApiParams('dc1.session'))
|
||||
const tg = new TelegramClient({ client: base })
|
||||
|
||||
// reset storage just in case
|
||||
await base.mt.storage.load()
|
||||
await base.mt.storage.clear(true)
|
||||
|
||||
while (true) {
|
||||
const phone = `999661${getAccountId()}`
|
||||
let user
|
||||
|
||||
try {
|
||||
user = await tg.start({
|
||||
phone,
|
||||
code: () => '11111',
|
||||
})
|
||||
} catch (e) {
|
||||
if (e instanceof MtcuteError && e.message.match(/Signup is no longer supported|2FA is enabled/)) {
|
||||
// retry with another number
|
||||
continue
|
||||
} else {
|
||||
await tg.close()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
await tg.close()
|
||||
|
||||
assertEquals(user.isSelf, true)
|
||||
assertEquals(user.phoneNumber, phone)
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
47
e2e/deno/tests/02.methods.ts
Normal file
47
e2e/deno/tests/02.methods.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
|
||||
|
||||
import { TelegramClient } from '@mtcute/core/client.js'
|
||||
|
||||
import { getApiParams } from '../utils.ts'
|
||||
|
||||
Deno.test('2. calling methods', { sanitizeResources: false }, async (t) => {
|
||||
const tg = new TelegramClient(getApiParams('dc2.session'))
|
||||
|
||||
await tg.connect()
|
||||
|
||||
await t.step('getUsers(@BotFather)', async () => {
|
||||
const [user] = await tg.getUsers('botfather')
|
||||
|
||||
assertEquals(user?.isBot, true)
|
||||
assertEquals(user?.displayName, 'BotFather')
|
||||
})
|
||||
|
||||
await t.step('getUsers(@BotFather) - cached', async () => {
|
||||
const [user] = await tg.getUsers('botfather')
|
||||
|
||||
assertEquals(user?.isBot, true)
|
||||
assertEquals(user?.displayName, 'BotFather')
|
||||
})
|
||||
|
||||
await t.step('getHistory(777000)', async () => {
|
||||
const history = await tg.getHistory(777000, { limit: 5 })
|
||||
|
||||
assertEquals(history[0].chat.chatType, 'private')
|
||||
assertEquals(history[0].chat.id, 777000)
|
||||
assertEquals(history[0].chat.firstName, 'Telegram')
|
||||
})
|
||||
|
||||
await t.step('updateProfile', async () => {
|
||||
const bio = `mtcute e2e ${new Date().toISOString()}`
|
||||
|
||||
const oldSelf = await tg.getFullChat('self')
|
||||
const res = await tg.updateProfile({ bio })
|
||||
const newSelf = await tg.getFullChat('self')
|
||||
|
||||
assertEquals(res.isSelf, true)
|
||||
assertNotEquals(oldSelf.bio, newSelf.bio)
|
||||
assertEquals(newSelf.bio, bio)
|
||||
})
|
||||
|
||||
await tg.close()
|
||||
})
|
170
e2e/deno/tests/03.files.ts
Normal file
170
e2e/deno/tests/03.files.ts
Normal file
|
@ -0,0 +1,170 @@
|
|||
import { assertEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
import { FileDownloadLocation, Thumbnail } from '@mtcute/core'
|
||||
import { TelegramClient } from '@mtcute/core/client.js'
|
||||
import { sleep } from '@mtcute/core/utils.js'
|
||||
|
||||
import { getApiParams } from '../utils.ts'
|
||||
|
||||
const CINNAMOROLL_PFP_CHAT = 'test_file_dc2'
|
||||
const CINNAMOROLL_PFP_THUMB_SHA256 = '3e6f220235a12547c16129f50c19ed3224d39b827414d1d500f79569a3431eae'
|
||||
const CINNAMOROLL_PFP_SHA256 = '4d9836a71ac039f5656cde55b83525871549bfbff9cfb658c3f8381c5ba89ce8'
|
||||
|
||||
const UWU_MSG = 'https://t.me/test_file_dc2/8'
|
||||
const UWU_SHA256 = '357b78c9f9d20e813f729a19dd90c6727f30ebd4c8c83557022285f283a705b9'
|
||||
|
||||
const SHREK_MSG = 'https://t.me/test_file_dc2/11'
|
||||
const SHREK_SHA256 = 'd3e6434e027f3d31dc3e05c6ea2eaf84fdd1fb00774a215f89d9ed8b56f86258'
|
||||
|
||||
const LARGE_MSG = 'https://t.me/test_file_dc2/12'
|
||||
|
||||
async function downloadAsSha256(client: TelegramClient, location: FileDownloadLocation): Promise<string> {
|
||||
const sha = createHash('sha256')
|
||||
|
||||
for await (const chunk of client.downloadAsIterable(location)) {
|
||||
sha.update(chunk)
|
||||
}
|
||||
|
||||
return sha.digest('hex')
|
||||
}
|
||||
|
||||
Deno.test('3. working with files', { sanitizeResources: false }, async (t) => {
|
||||
// sometimes test dcs are overloaded and we get FILE_REFERENCE_EXPIRED
|
||||
// because we got multiple -500:No workers running errors in a row
|
||||
// we currently don't have file references database, so we can just retry the test for now
|
||||
//
|
||||
// ...except we can't under deno because it's not implemented
|
||||
// https://github.com/denoland/deno/issues/19882
|
||||
// this.retries(2)
|
||||
|
||||
await t.step('same-dc', async (t) => {
|
||||
const tg = new TelegramClient(getApiParams('dc2.session'))
|
||||
|
||||
await tg.connect()
|
||||
|
||||
await t.step('should download pfp thumbs', async () => {
|
||||
const chat = await tg.getChat(CINNAMOROLL_PFP_CHAT)
|
||||
if (!chat.photo) throw new Error('Chat has no photo')
|
||||
|
||||
assertEquals(await downloadAsSha256(tg, chat.photo.big), CINNAMOROLL_PFP_THUMB_SHA256)
|
||||
})
|
||||
|
||||
await t.step('should download animated pfps', async () => {
|
||||
const chat = await tg.getFullChat(CINNAMOROLL_PFP_CHAT)
|
||||
const thumb = chat.fullPhoto?.getThumbnail(Thumbnail.THUMB_VIDEO_PROFILE)
|
||||
if (!thumb) throw new Error('Chat has no animated pfp')
|
||||
|
||||
assertEquals(await downloadAsSha256(tg, thumb), CINNAMOROLL_PFP_SHA256)
|
||||
})
|
||||
|
||||
await t.step('should download photos', async () => {
|
||||
const msg = await tg.getMessageByLink(UWU_MSG)
|
||||
|
||||
if (msg?.media?.type !== 'photo') {
|
||||
throw new Error('Message not found or not a photo')
|
||||
}
|
||||
|
||||
assertEquals(await downloadAsSha256(tg, msg.media), UWU_SHA256)
|
||||
})
|
||||
|
||||
await t.step('should download documents', async () => {
|
||||
const msg = await tg.getMessageByLink(SHREK_MSG)
|
||||
|
||||
if (msg?.media?.type !== 'document') {
|
||||
throw new Error('Message not found or not a document')
|
||||
}
|
||||
|
||||
assertEquals(await downloadAsSha256(tg, msg.media), SHREK_SHA256)
|
||||
})
|
||||
|
||||
await t.step('should cancel downloads', async () => {
|
||||
const msg = await tg.getMessageByLink(LARGE_MSG)
|
||||
|
||||
if (msg?.media?.type !== 'document') {
|
||||
throw new Error('Message not found or not a document')
|
||||
}
|
||||
|
||||
const media = msg.media
|
||||
|
||||
const abort = new AbortController()
|
||||
|
||||
let downloaded = 0
|
||||
|
||||
async function download() {
|
||||
const dl = tg.downloadAsIterable(media, { abortSignal: abort.signal })
|
||||
|
||||
try {
|
||||
for await (const chunk of dl) {
|
||||
downloaded += chunk.length
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof DOMException && e.name === 'AbortError')) throw e
|
||||
}
|
||||
}
|
||||
|
||||
const promise = download()
|
||||
|
||||
// let it download for 10 seconds
|
||||
await sleep(10000)
|
||||
abort.abort()
|
||||
// abort and snap the downloaded amount
|
||||
const downloadedBefore = downloaded
|
||||
|
||||
const avgSpeed = downloaded / 10
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Average speed: %d KiB/s', avgSpeed / 1024)
|
||||
|
||||
// wait a bit more to make sure it's aborted
|
||||
await sleep(2000)
|
||||
await promise
|
||||
|
||||
assertEquals(downloaded, downloadedBefore, 'nothing should be downloaded after abort')
|
||||
})
|
||||
|
||||
await tg.close()
|
||||
})
|
||||
|
||||
await t.step('cross-dc', async (t) => {
|
||||
const tg = new TelegramClient(getApiParams('dc1.session'))
|
||||
|
||||
await tg.connect()
|
||||
|
||||
await t.step('should download pfp thumbs', async () => {
|
||||
const chat = await tg.getChat(CINNAMOROLL_PFP_CHAT)
|
||||
if (!chat.photo) throw new Error('Chat has no photo')
|
||||
|
||||
assertEquals(await downloadAsSha256(tg, chat.photo.big), CINNAMOROLL_PFP_THUMB_SHA256)
|
||||
})
|
||||
|
||||
await t.step('should download animated pfps', async () => {
|
||||
const chat = await tg.getFullChat(CINNAMOROLL_PFP_CHAT)
|
||||
const thumb = chat.fullPhoto?.getThumbnail(Thumbnail.THUMB_VIDEO_PROFILE)
|
||||
if (!thumb) throw new Error('Chat has no animated pfp')
|
||||
|
||||
assertEquals(await downloadAsSha256(tg, thumb), CINNAMOROLL_PFP_SHA256)
|
||||
})
|
||||
|
||||
await t.step('should download photos', async () => {
|
||||
const msg = await tg.getMessageByLink(UWU_MSG)
|
||||
|
||||
if (msg?.media?.type !== 'photo') {
|
||||
throw new Error('Message not found or not a photo')
|
||||
}
|
||||
|
||||
assertEquals(await downloadAsSha256(tg, msg.media), UWU_SHA256)
|
||||
})
|
||||
|
||||
await t.step('should download documents', async () => {
|
||||
const msg = await tg.getMessageByLink(SHREK_MSG)
|
||||
|
||||
if (msg?.media?.type !== 'document') {
|
||||
throw new Error('Message not found or not a document')
|
||||
}
|
||||
|
||||
assertEquals(await downloadAsSha256(tg, msg.media), SHREK_SHA256)
|
||||
})
|
||||
|
||||
await tg.close()
|
||||
})
|
||||
})
|
47
e2e/deno/tests/04.updates.ts
Normal file
47
e2e/deno/tests/04.updates.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.223.0/assert/mod.ts'
|
||||
|
||||
import { Message } from '@mtcute/core'
|
||||
import { TelegramClient } from '@mtcute/core/client.js'
|
||||
|
||||
import { getApiParams, waitFor } from '../utils.ts'
|
||||
|
||||
Deno.test('4. handling updates', { sanitizeResources: false }, async (t) => {
|
||||
const tg1 = new TelegramClient(getApiParams('dc1.session'))
|
||||
tg1.log.prefix = '[tg1] '
|
||||
const tg2 = new TelegramClient(getApiParams('dc2.session'))
|
||||
tg2.log.prefix = '[tg2] '
|
||||
|
||||
await tg1.connect()
|
||||
await tg1.startUpdatesLoop()
|
||||
await tg2.connect()
|
||||
|
||||
await t.step('should send and receive messages', async () => {
|
||||
const tg1Messages: Message[] = []
|
||||
|
||||
tg1.on('new_message', (msg) => tg1Messages.push(msg))
|
||||
|
||||
const [tg1User] = await tg1.getUsers('self')
|
||||
let username = tg1User!.username
|
||||
|
||||
if (!username) {
|
||||
username = `mtcute_e2e_${Math.random().toString(36).slice(2)}`
|
||||
await tg1.setMyUsername(username)
|
||||
}
|
||||
|
||||
const messageText = `mtcute test message ${Math.random().toString(36).slice(2)}`
|
||||
const sentMsg = await tg2.sendText(username, messageText)
|
||||
|
||||
assertEquals(sentMsg.text, messageText)
|
||||
assertEquals(sentMsg.chat.id, tg1User!.id)
|
||||
|
||||
await waitFor(() => {
|
||||
assertNotEquals(
|
||||
tg1Messages.find((msg) => msg.text === messageText),
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
await tg1.close()
|
||||
await tg2.close()
|
||||
})
|
79
e2e/deno/tests/05.worker.ts
Normal file
79
e2e/deno/tests/05.worker.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { assertEquals, assertGreater, assertInstanceOf } from 'https://deno.land/std@0.223.0/assert/mod.ts'
|
||||
|
||||
import { TelegramClient } from '@mtcute/core/client.js'
|
||||
import { Message, TelegramWorkerPort, tl } from '@mtcute/deno'
|
||||
|
||||
import { getApiParams, waitFor } from '../utils.ts'
|
||||
import type { CustomMethods } from './_worker.ts'
|
||||
|
||||
Deno.test('5. worker', { sanitizeResources: false }, async (t) => {
|
||||
const worker = new Worker(new URL('_worker.ts', import.meta.url), {
|
||||
type: 'module',
|
||||
})
|
||||
|
||||
const port = new TelegramWorkerPort<CustomMethods>({
|
||||
worker,
|
||||
})
|
||||
const portClient = new TelegramClient({ client: port })
|
||||
|
||||
await t.step('should make api calls', async function () {
|
||||
const res = await port.call({ _: 'help.getConfig' })
|
||||
|
||||
assertEquals(res._, 'config')
|
||||
})
|
||||
|
||||
await t.step('should call custom methods', async function () {
|
||||
const hello = await port.invokeCustom('hello')
|
||||
assertEquals(hello, 'world')
|
||||
|
||||
const sum = await port.invokeCustom('sum', 2, 3)
|
||||
assertEquals(sum, 5)
|
||||
})
|
||||
|
||||
await t.step('should throw errors', async function () {
|
||||
try {
|
||||
await port.call({ _: 'test.useConfigSimple' })
|
||||
throw new Error('should have thrown')
|
||||
} catch (e) {
|
||||
assertInstanceOf(e, tl.RpcError)
|
||||
}
|
||||
})
|
||||
|
||||
await t.step('should receive updates', async function () {
|
||||
const client2 = new TelegramClient(getApiParams('dc2.session'))
|
||||
|
||||
try {
|
||||
await client2.connect()
|
||||
await port.startUpdatesLoop()
|
||||
|
||||
const me = await portClient.getMe()
|
||||
let username = me.username
|
||||
|
||||
if (!username) {
|
||||
username = `mtcute_e2e_${Math.random().toString(36).slice(2, 8)}`
|
||||
await portClient.setMyUsername(username)
|
||||
}
|
||||
|
||||
const msgs: Message[] = []
|
||||
portClient.on('new_message', (msg) => {
|
||||
msgs.push(msg)
|
||||
})
|
||||
|
||||
const testText = `test ${Math.random()}`
|
||||
await client2.sendText(username, testText)
|
||||
|
||||
await waitFor(() => {
|
||||
assertGreater(msgs.length, 0)
|
||||
assertEquals(msgs[0].text, testText)
|
||||
})
|
||||
} catch (e) {
|
||||
await client2.close()
|
||||
throw e
|
||||
}
|
||||
|
||||
await client2.close()
|
||||
})
|
||||
|
||||
await port.close()
|
||||
worker.terminate()
|
||||
})
|
18
e2e/deno/tests/_worker.ts
Normal file
18
e2e/deno/tests/_worker.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { WorkerCustomMethods } from '@mtcute/core/worker.js'
|
||||
import { BaseTelegramClient, TelegramWorker } from '@mtcute/deno'
|
||||
|
||||
import { getApiParams } from '../utils.ts'
|
||||
|
||||
const customMethods = {
|
||||
hello: async () => 'world',
|
||||
sum: async (a: number, b: number) => a + b,
|
||||
} as const satisfies WorkerCustomMethods
|
||||
export type CustomMethods = typeof customMethods
|
||||
|
||||
const client = new BaseTelegramClient(getApiParams('dc1.session'))
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new TelegramWorker({
|
||||
client,
|
||||
customMethods,
|
||||
})
|
|
@ -1,25 +1,25 @@
|
|||
import { MaybePromise, MemoryStorage } from '@mtcute/core'
|
||||
import { setPlatform } from '@mtcute/core/platform.js'
|
||||
import { LogManager, sleep } from '@mtcute/core/utils.js'
|
||||
import { WebCryptoProvider, WebPlatform, WebSocketTransport } from '@mtcute/web'
|
||||
import { DenoCryptoProvider, DenoPlatform, SqliteStorage, TcpTransport } from '@mtcute/deno'
|
||||
|
||||
export const getApiParams = (storage?: string) => {
|
||||
if (storage) throw new Error('unsupported yet')
|
||||
|
||||
if (!Deno.env.has('API_ID') || !Deno.env.has('API_HASH')) {
|
||||
throw new Error('API_ID and API_HASH env variables must be set')
|
||||
}
|
||||
|
||||
setPlatform(new WebPlatform())
|
||||
Deno.mkdirSync('.sessions', { recursive: true })
|
||||
|
||||
setPlatform(new DenoPlatform())
|
||||
|
||||
return {
|
||||
apiId: parseInt(Deno.env.get('API_ID')!),
|
||||
apiHash: Deno.env.get('API_HASH')!,
|
||||
testMode: true,
|
||||
storage: new MemoryStorage(),
|
||||
storage: storage ? new SqliteStorage(`.sessions/${storage}`) : new MemoryStorage(),
|
||||
logLevel: LogManager.VERBOSE,
|
||||
transport: () => new WebSocketTransport(),
|
||||
crypto: new WebCryptoProvider(),
|
||||
transport: () => new TcpTransport(),
|
||||
crypto: new DenoCryptoProvider(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
],
|
||||
"scripts": {
|
||||
"prepare": "husky install .config/husky",
|
||||
"postinstall": "node scripts/validate-deps-versions.mjs",
|
||||
"postinstall": "node scripts/validate-deps-versions.mjs && node scripts/remove-jsr-sourcefiles.mjs",
|
||||
"test": "pnpm run -r test && vitest --config .config/vite.mts run",
|
||||
"test:dev": "vitest --config .config/vite.mts watch",
|
||||
"test:ui": "vitest --config .config/vite.mts --ui",
|
||||
|
@ -35,8 +35,9 @@
|
|||
"devDependencies": {
|
||||
"@commitlint/cli": "17.6.5",
|
||||
"@commitlint/config-conventional": "17.6.5",
|
||||
"@teidesu/slow-types-compiler": "1.0.2",
|
||||
"@teidesu/slow-types-compiler": "1.1.0",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/deno": "npm:@teidesu/deno-types@1.42.4",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "6.4.0",
|
||||
"@typescript-eslint/parser": "6.4.0",
|
||||
|
|
|
@ -56,7 +56,7 @@ export class BaseTelegramClient extends BaseTelegramClientBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Telegram client for use in Node.js
|
||||
* Telegram client for use in Bun
|
||||
*/
|
||||
export class TelegramClient extends TelegramClientBase {
|
||||
constructor(opts: TelegramClientOptions) {
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import * as os from 'os'
|
||||
|
||||
import { NodePlatform } from './common-internals-node/platform.js'
|
||||
import { normalizeFile } from './utils/normalize-file.js'
|
||||
|
||||
export class BunPlatform extends NodePlatform {
|
||||
declare normalizeFile: typeof normalizeFile
|
||||
|
||||
getDeviceModel(): string {
|
||||
return `Bun/${process.version} (${os.type()} ${os.arch()})`
|
||||
}
|
||||
}
|
||||
|
||||
BunPlatform.prototype.normalizeFile = normalizeFile
|
||||
|
|
|
@ -91,8 +91,11 @@ export abstract class BaseTcpTransport extends EventEmitter implements ITelegram
|
|||
|
||||
handleError(socket: unknown, error: Error): void {
|
||||
this.log.error('error: %s', error.stack)
|
||||
|
||||
if (this.listenerCount('error') > 0) {
|
||||
this.emit('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
handleConnect(socket: Socket): void {
|
||||
this._socket = socket
|
||||
|
@ -109,7 +112,11 @@ export abstract class BaseTcpTransport extends EventEmitter implements ITelegram
|
|||
this.emit('ready')
|
||||
}
|
||||
})
|
||||
.catch((err) => this.emit('error', err))
|
||||
.catch((err) => {
|
||||
if (this.listenerCount('error') > 0) {
|
||||
this.emit('error', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async send(bytes: Uint8Array): Promise<void> {
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
WorkerMessageHandler,
|
||||
} from '@mtcute/core/worker.js'
|
||||
|
||||
import { NodePlatform } from './common-internals-node/platform.js'
|
||||
import { BunPlatform } from './platform.js'
|
||||
|
||||
export type { TelegramWorkerOptions, TelegramWorkerPortOptions, WorkerCustomMethods }
|
||||
|
||||
|
@ -44,7 +44,7 @@ export class TelegramWorker<T extends WorkerCustomMethods> extends TelegramWorke
|
|||
|
||||
export class TelegramWorkerPort<T extends WorkerCustomMethods> extends TelegramWorkerPortBase<T> {
|
||||
constructor(readonly options: TelegramWorkerPortOptions) {
|
||||
setPlatform(new NodePlatform())
|
||||
setPlatform(new BunPlatform())
|
||||
super(options)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
/* eslint-disable no-restricted-imports */
|
||||
import type { ReadStream } from 'node:fs'
|
||||
|
||||
import { tdFileId } from '@mtcute/file-id'
|
||||
import { tl } from '@mtcute/tl'
|
||||
|
||||
import { AnyToNever } from '../../../types/utils.js'
|
||||
import { FileLocation } from './file-location.js'
|
||||
import { UploadedFile } from './uploaded-file.js'
|
||||
|
||||
|
@ -14,8 +12,9 @@ import { UploadedFile } from './uploaded-file.js'
|
|||
* - `File`, `Blob` (from the Web API)
|
||||
* - `string`, which will be interpreted as file path (**non-browser only!**)
|
||||
* - `URL` (from the Web API, will be `fetch()`-ed; `file://` URLs are not available in browsers)
|
||||
* - `ReadStream` (for Node.js/Bun, from the `fs` module)
|
||||
* - `ReadStream` (for Node.js/Bun, from the `node:fs` module)
|
||||
* - `BunFile` (from `Bun.file()`)
|
||||
* - `Deno.FsFile` (from `Deno.open()` in Deno)
|
||||
* - `ReadableStream` (Web API readable stream)
|
||||
* - `Readable` (Node.js/Bun readable stream)
|
||||
* - `Response` (from `window.fetch`)
|
||||
|
@ -26,10 +25,14 @@ export type UploadFileLike =
|
|||
| File
|
||||
| Blob
|
||||
| string
|
||||
| ReadStream
|
||||
| ReadableStream<Uint8Array>
|
||||
| NodeJS.ReadableStream
|
||||
| Response
|
||||
| AnyToNever<import('node:fs').ReadStream>
|
||||
| AnyToNever<ReadableStream<Uint8Array>>
|
||||
| AnyToNever<NodeJS.ReadableStream>
|
||||
| AnyToNever<Response>
|
||||
| AnyToNever<Deno.FsFile>
|
||||
|
||||
// AnyToNever in the above type ensures we don't make the entire type `any`
|
||||
// if some of the types are not available in the current environment
|
||||
|
||||
/**
|
||||
* Describes types that can be used as an input
|
||||
|
@ -41,6 +44,8 @@ export type UploadFileLike =
|
|||
* - `ReadStream` (for Node.js/Bun, from the `fs` module)
|
||||
* - `ReadableStream` (from the Web API, base readable stream)
|
||||
* - `Readable` (for Node.js/Bun, base readable stream)
|
||||
* - `BunFile` (from `Bun.file()`)
|
||||
* - `Deno.FsFile` (from `Deno.open()` in Deno)
|
||||
* - {@link UploadedFile} returned from {@link TelegramClient.uploadFile}
|
||||
* - `tl.TypeInputFile` and `tl.TypeInputMedia` TL objects
|
||||
* - `string` with a path to a local file prepended with `file:` (non-browser only) (e.g. `file:image.jpg`)
|
||||
|
|
|
@ -62,10 +62,9 @@ export class SqlitePeersRepository implements IPeersRepository {
|
|||
this._driver._writeLater(this._store, [
|
||||
peer.id,
|
||||
peer.accessHash,
|
||||
// add commas to make it easier to search with LIKE
|
||||
JSON.stringify(peer.usernames),
|
||||
peer.updated,
|
||||
peer.phone,
|
||||
peer.phone ?? null,
|
||||
peer.complete,
|
||||
])
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
export type MaybePromise<T> = T | Promise<T>
|
||||
export type PartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>
|
||||
export type PartialOnly<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AnyToNever<T> = any extends T ? never : T
|
||||
|
||||
export type MaybeArray<T> = T | T[]
|
||||
|
||||
|
|
25
packages/deno/README.md
Normal file
25
packages/deno/README.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# @mtcute/deno
|
||||
|
||||
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_deno.html)
|
||||
|
||||
‼️ **Experimental** Deno support package for mtcute. Includes:
|
||||
- SQLite storage (based on [`@db/sqlite`](https://jsr.io/@db/sqlite))
|
||||
- TCP transport (based on Deno-native APIs)
|
||||
- `TelegramClient` implementation using the above
|
||||
- HTML and Markdown parsers
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { TelegramClient } from '@mtcute/deno'
|
||||
|
||||
const tg = new TelegramClient({
|
||||
apiId: 12345,
|
||||
apiHash: 'abcdef',
|
||||
storage: 'my-account'
|
||||
})
|
||||
|
||||
tg.run(async (user) => {
|
||||
console.log(`✨ logged in as ${user.displayName}`)
|
||||
})
|
||||
```
|
12
packages/deno/build.config.cjs
Normal file
12
packages/deno/build.config.cjs
Normal file
|
@ -0,0 +1,12 @@
|
|||
module.exports = ({ outDir, fs, jsr }) => ({
|
||||
buildCjs: false,
|
||||
final() {
|
||||
if (jsr) {
|
||||
// jsr doesn't support symlinks, so we need to copy the files manually
|
||||
const real = fs.realpathSync(`${outDir}/common-internals-web`)
|
||||
fs.unlinkSync(`${outDir}/common-internals-web`)
|
||||
// console.log(real)
|
||||
fs.cpSync(real, `${outDir}/common-internals-web`, { recursive: true })
|
||||
}
|
||||
},
|
||||
})
|
30
packages/deno/package.json
Normal file
30
packages/deno/package.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "@mtcute/deno",
|
||||
"private": true,
|
||||
"version": "0.11.0",
|
||||
"description": "Meta-package for Deno",
|
||||
"author": "alina sireneva <alina@tei.su>",
|
||||
"license": "MIT",
|
||||
"main": "src/index.ts",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"docs": "typedoc",
|
||||
"build": "pnpm run -w build-package deno"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./utils.js": "./src/utils.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mtcute/core": "workspace:^",
|
||||
"@mtcute/wasm": "workspace:^",
|
||||
"@mtcute/markdown-parser": "workspace:^",
|
||||
"@mtcute/html-parser": "workspace:^",
|
||||
"@db/sqlite": "npm:@jsr/db__sqlite@0.11.1",
|
||||
"@std/io": "npm:@jsr/std__io@0.223.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mtcute/test": "workspace:^"
|
||||
}
|
||||
}
|
145
packages/deno/src/client.ts
Normal file
145
packages/deno/src/client.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { createInterface, Interface as RlInterface } from 'node:readline'
|
||||
import { Readable, Writable } from 'node:stream'
|
||||
|
||||
import { FileDownloadLocation, FileDownloadParameters, ITelegramStorageProvider, PartialOnly, User } from '@mtcute/core'
|
||||
import {
|
||||
BaseTelegramClient as BaseTelegramClientBase,
|
||||
BaseTelegramClientOptions as BaseTelegramClientOptionsBase,
|
||||
TelegramClient as TelegramClientBase,
|
||||
TelegramClientOptions,
|
||||
} from '@mtcute/core/client.js'
|
||||
import { setPlatform } from '@mtcute/core/platform.js'
|
||||
|
||||
import { downloadToFile } from './methods/download-file.js'
|
||||
import { DenoPlatform } from './platform.js'
|
||||
import { SqliteStorage } from './sqlite/index.js'
|
||||
import { DenoCryptoProvider } from './utils/crypto.js'
|
||||
import { TcpTransport } from './utils/tcp.js'
|
||||
|
||||
export type { TelegramClientOptions }
|
||||
|
||||
export interface BaseTelegramClientOptions
|
||||
extends PartialOnly<Omit<BaseTelegramClientOptionsBase, 'storage'>, 'transport' | 'crypto'> {
|
||||
/**
|
||||
* Storage to use for this client.
|
||||
*
|
||||
* If a string is passed, it will be used as
|
||||
* a name for an SQLite database file.
|
||||
*
|
||||
* @default `"client.session"`
|
||||
*/
|
||||
storage?: string | ITelegramStorageProvider
|
||||
|
||||
/**
|
||||
* **ADVANCED USE ONLY**
|
||||
*
|
||||
* Whether to not set up the platform.
|
||||
* This is useful if you call `setPlatform` yourself.
|
||||
*/
|
||||
platformless?: boolean
|
||||
}
|
||||
|
||||
export class BaseTelegramClient extends BaseTelegramClientBase {
|
||||
constructor(opts: BaseTelegramClientOptions) {
|
||||
if (!opts.platformless) setPlatform(new DenoPlatform())
|
||||
|
||||
super({
|
||||
crypto: new DenoCryptoProvider(),
|
||||
transport: () => new TcpTransport(),
|
||||
...opts,
|
||||
storage:
|
||||
typeof opts.storage === 'string' ?
|
||||
new SqliteStorage(opts.storage) :
|
||||
opts.storage ?? new SqliteStorage('client.session'),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Telegram client for use in Deno
|
||||
*/
|
||||
export class TelegramClient extends TelegramClientBase {
|
||||
constructor(opts: TelegramClientOptions) {
|
||||
if ('client' in opts) {
|
||||
super(opts)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
super({
|
||||
client: new BaseTelegramClient(opts),
|
||||
})
|
||||
}
|
||||
|
||||
private _rl?: RlInterface
|
||||
|
||||
/**
|
||||
* Tiny wrapper over Node `readline` package
|
||||
* for simpler user input for `.run()` method.
|
||||
*
|
||||
* Associated `readline` interface is closed
|
||||
* after `run()` returns, or with the client.
|
||||
*
|
||||
* @param text Text of the question
|
||||
*/
|
||||
input(text: string): Promise<string> {
|
||||
if (!this._rl) {
|
||||
this._rl = createInterface({
|
||||
// eslint-disable-next-line
|
||||
input: Readable.fromWeb(Deno.stdin.readable as any),
|
||||
// eslint-disable-next-line
|
||||
output: Writable.fromWeb(Deno.stdout.writable as any),
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise((res) => this._rl?.question(text, res))
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
this._rl?.close()
|
||||
|
||||
return super.close()
|
||||
}
|
||||
|
||||
start(params: Parameters<TelegramClientBase['start']>[0] = {}): Promise<User> {
|
||||
if (!params.botToken) {
|
||||
if (!params.phone) params.phone = () => this.input('phone > ')
|
||||
if (!params.code) params.code = () => this.input('code > ')
|
||||
|
||||
if (!params.password) {
|
||||
params.password = () => this.input('2fa password > ')
|
||||
}
|
||||
}
|
||||
|
||||
return super.start(params).then((user) => {
|
||||
if (this._rl) {
|
||||
this._rl.close()
|
||||
delete this._rl
|
||||
}
|
||||
|
||||
return user
|
||||
})
|
||||
}
|
||||
|
||||
run(
|
||||
params: Parameters<TelegramClient['start']>[0] | ((user: User) => void | Promise<void>),
|
||||
then?: (user: User) => void | Promise<void>,
|
||||
): void {
|
||||
if (typeof params === 'function') {
|
||||
then = params
|
||||
params = {}
|
||||
}
|
||||
|
||||
this.start(params)
|
||||
.then(then)
|
||||
.catch((err) => this.emitError(err))
|
||||
}
|
||||
|
||||
downloadToFile(
|
||||
filename: string,
|
||||
location: FileDownloadLocation,
|
||||
params?: FileDownloadParameters | undefined,
|
||||
): Promise<void> {
|
||||
return downloadToFile(this, filename, location, params)
|
||||
}
|
||||
}
|
1
packages/deno/src/common-internals-web
Symbolic link
1
packages/deno/src/common-internals-web
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../web/src/common-internals-web
|
9
packages/deno/src/index.ts
Normal file
9
packages/deno/src/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export * from './client.js'
|
||||
export * from './platform.js'
|
||||
export * from './sqlite/index.js'
|
||||
export * from './utils/crypto.js'
|
||||
export * from './utils/tcp.js'
|
||||
export * from './worker.js'
|
||||
export * from '@mtcute/core'
|
||||
export * from '@mtcute/html-parser'
|
||||
export * from '@mtcute/markdown-parser'
|
2
packages/deno/src/methods.ts
Normal file
2
packages/deno/src/methods.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { downloadToFile } from './methods/download-file.js'
|
||||
export * from '@mtcute/core/methods.js'
|
39
packages/deno/src/methods/download-file.ts
Normal file
39
packages/deno/src/methods/download-file.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { FileDownloadLocation, FileDownloadParameters, FileLocation, ITelegramClient } from '@mtcute/core'
|
||||
import { downloadAsIterable } from '@mtcute/core/methods.js'
|
||||
|
||||
import { writeAll } from '@std/io/write-all'
|
||||
|
||||
/**
|
||||
* Download a remote file to a local file (only for NodeJS).
|
||||
* Promise will resolve once the download is complete.
|
||||
*
|
||||
* @param filename Local file name to which the remote file will be downloaded
|
||||
* @param params File download parameters
|
||||
*/
|
||||
export async function downloadToFile(
|
||||
client: ITelegramClient,
|
||||
filename: string,
|
||||
location: FileDownloadLocation,
|
||||
params?: FileDownloadParameters,
|
||||
): Promise<void> {
|
||||
if (location instanceof FileLocation && ArrayBuffer.isView(location.location)) {
|
||||
// early return for inline files
|
||||
await Deno.writeFile(filename, location.location)
|
||||
}
|
||||
|
||||
const fd = await Deno.open(filename, { write: true, create: true, truncate: true })
|
||||
|
||||
if (params?.abortSignal) {
|
||||
params.abortSignal.addEventListener('abort', () => {
|
||||
client.log.debug('aborting file download %s - cleaning up', filename)
|
||||
fd.close()
|
||||
Deno.removeSync(filename)
|
||||
})
|
||||
}
|
||||
|
||||
for await (const chunk of downloadAsIterable(client, location, params)) {
|
||||
await writeAll(fd, chunk)
|
||||
}
|
||||
|
||||
fd.close()
|
||||
}
|
48
packages/deno/src/platform.ts
Normal file
48
packages/deno/src/platform.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { ICorePlatform } from '@mtcute/core/platform.js'
|
||||
|
||||
import { base64Decode, base64Encode } from './common-internals-web/base64.js'
|
||||
import { hexDecode, hexEncode } from './common-internals-web/hex.js'
|
||||
import { defaultLoggingHandler } from './common-internals-web/logging.js'
|
||||
import { utf8ByteLength, utf8Decode, utf8Encode } from './common-internals-web/utf8.js'
|
||||
import { beforeExit } from './utils/exit-hook.js'
|
||||
import { normalizeFile } from './utils/normalize-file.js'
|
||||
|
||||
export class DenoPlatform implements ICorePlatform {
|
||||
declare log: typeof defaultLoggingHandler
|
||||
declare beforeExit: typeof beforeExit
|
||||
declare normalizeFile: typeof normalizeFile
|
||||
|
||||
getDeviceModel(): string {
|
||||
return `Deno/${Deno.version.deno} (${Deno.build.os} ${Deno.build.arch})`
|
||||
}
|
||||
|
||||
getDefaultLogLevel(): number | null {
|
||||
const envLogLevel = parseInt(Deno.env.get('MTCUTE_LOG_LEVEL') ?? '')
|
||||
|
||||
if (!isNaN(envLogLevel)) {
|
||||
return envLogLevel
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// ITlPlatform
|
||||
declare utf8ByteLength: typeof utf8ByteLength
|
||||
declare utf8Encode: typeof utf8Encode
|
||||
declare utf8Decode: typeof utf8Decode
|
||||
declare hexEncode: typeof hexEncode
|
||||
declare hexDecode: typeof hexDecode
|
||||
declare base64Encode: typeof base64Encode
|
||||
declare base64Decode: typeof base64Decode
|
||||
}
|
||||
|
||||
DenoPlatform.prototype.utf8ByteLength = utf8ByteLength
|
||||
DenoPlatform.prototype.utf8Encode = utf8Encode
|
||||
DenoPlatform.prototype.utf8Decode = utf8Decode
|
||||
DenoPlatform.prototype.hexEncode = hexEncode
|
||||
DenoPlatform.prototype.hexDecode = hexDecode
|
||||
DenoPlatform.prototype.base64Encode = base64Encode
|
||||
DenoPlatform.prototype.base64Decode = base64Decode
|
||||
DenoPlatform.prototype.log = defaultLoggingHandler
|
||||
DenoPlatform.prototype.beforeExit = beforeExit
|
||||
DenoPlatform.prototype.normalizeFile = normalizeFile
|
47
packages/deno/src/sqlite/driver.ts
Normal file
47
packages/deno/src/sqlite/driver.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { BaseSqliteStorageDriver, ISqliteDatabase } from '@mtcute/core'
|
||||
|
||||
let Database: typeof import('@db/sqlite').Database
|
||||
|
||||
export interface SqliteStorageDriverOptions {
|
||||
/**
|
||||
* By default, WAL mode is enabled, which
|
||||
* significantly improves performance.
|
||||
* [Learn more](https://bun.sh/docs/api/sqlite#wal-mode)
|
||||
*
|
||||
* However, you might encounter some issues,
|
||||
* and if you do, you can disable WAL by passing `true`
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disableWal?: boolean
|
||||
}
|
||||
|
||||
export class SqliteStorageDriver extends BaseSqliteStorageDriver {
|
||||
constructor(
|
||||
readonly filename = ':memory:',
|
||||
readonly params?: SqliteStorageDriverOptions,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async _load(): Promise<void> {
|
||||
if (!Database) {
|
||||
// we load this lazily to avoid loading ffi if it's not needed,
|
||||
// in case the user doesn't use sqlite storage
|
||||
Database = (await import('@db/sqlite')).Database
|
||||
}
|
||||
super._load()
|
||||
}
|
||||
|
||||
_createDatabase(): ISqliteDatabase {
|
||||
const db = new Database(this.filename, {
|
||||
int64: true,
|
||||
})
|
||||
|
||||
if (!this.params?.disableWal) {
|
||||
db.exec('PRAGMA journal_mode = WAL;')
|
||||
}
|
||||
|
||||
return db as ISqliteDatabase
|
||||
}
|
||||
}
|
14
packages/deno/src/sqlite/index.ts
Normal file
14
packages/deno/src/sqlite/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { BaseSqliteStorage } from '@mtcute/core'
|
||||
|
||||
import { SqliteStorageDriver, SqliteStorageDriverOptions } from './driver.js'
|
||||
|
||||
export { SqliteStorageDriver } from './driver.js'
|
||||
|
||||
export class SqliteStorage extends BaseSqliteStorage {
|
||||
constructor(
|
||||
readonly filename = ':memory:',
|
||||
readonly params?: SqliteStorageDriverOptions,
|
||||
) {
|
||||
super(new SqliteStorageDriver(filename, params))
|
||||
}
|
||||
}
|
34
packages/deno/src/sqlite/sqlite.test.ts
Normal file
34
packages/deno/src/sqlite/sqlite.test.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { afterAll, beforeAll, describe } from 'vitest'
|
||||
|
||||
import { LogManager } from '@mtcute/core/utils.js'
|
||||
import {
|
||||
testAuthKeysRepository,
|
||||
testKeyValueRepository,
|
||||
testPeersRepository,
|
||||
testRefMessagesRepository,
|
||||
} from '@mtcute/test'
|
||||
|
||||
if (import.meta.env.TEST_ENV === 'deno') {
|
||||
// load sqlite in advance so test runner doesn't complain about us leaking the library
|
||||
// (it's not on us, @db/sqlite doesn't provide an api to unload the library)
|
||||
await import('@db/sqlite')
|
||||
const { SqliteStorage } = await import('./index.js')
|
||||
|
||||
describe('SqliteStorage', () => {
|
||||
const storage = new SqliteStorage(':memory:')
|
||||
|
||||
beforeAll(async () => {
|
||||
storage.driver.setup(new LogManager())
|
||||
await storage.driver.load()
|
||||
})
|
||||
|
||||
testAuthKeysRepository(storage.authKeys)
|
||||
testKeyValueRepository(storage.kv, storage.driver)
|
||||
testPeersRepository(storage.peers, storage.driver)
|
||||
testRefMessagesRepository(storage.refMessages, storage.driver)
|
||||
|
||||
afterAll(() => storage.driver.destroy())
|
||||
})
|
||||
} else {
|
||||
describe.skip('SqliteStorage', () => {})
|
||||
}
|
1
packages/deno/src/utils.ts
Normal file
1
packages/deno/src/utils.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from '@mtcute/core/utils.js'
|
13
packages/deno/src/utils/crypto.test.ts
Normal file
13
packages/deno/src/utils/crypto.test.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { describe } from 'vitest'
|
||||
|
||||
import { testCryptoProvider } from '@mtcute/test'
|
||||
|
||||
if (import.meta.env.TEST_ENV === 'deno') {
|
||||
describe('DenoCryptoProvider', async () => {
|
||||
const { DenoCryptoProvider } = await import('./crypto.js')
|
||||
|
||||
testCryptoProvider(new DenoCryptoProvider())
|
||||
})
|
||||
} else {
|
||||
describe.skip('DenoCryptoProvider', () => {})
|
||||
}
|
97
packages/deno/src/utils/crypto.ts
Normal file
97
packages/deno/src/utils/crypto.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { Buffer } from 'node:buffer'
|
||||
import { createCipheriv, createHash, createHmac, pbkdf2 } from 'node:crypto'
|
||||
import { deflateSync, gunzipSync } from 'node:zlib'
|
||||
|
||||
import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from '@mtcute/core/utils.js'
|
||||
import { getWasmUrl, ige256Decrypt, ige256Encrypt, initSync } from '@mtcute/wasm'
|
||||
|
||||
// node:crypto is properly implemented in deno, so we can just use it
|
||||
// largely just copy-pasting from @mtcute/node
|
||||
|
||||
const toUint8Array = (buf: Buffer | Uint8Array): Uint8Array => {
|
||||
if (!Buffer.isBuffer(buf)) return buf
|
||||
|
||||
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
|
||||
}
|
||||
|
||||
export class DenoCryptoProvider extends BaseCryptoProvider implements ICryptoProvider {
|
||||
async initialize(): Promise<void> {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const wasm = await fetch(getWasmUrl()).then((res) => res.arrayBuffer())
|
||||
initSync(wasm)
|
||||
}
|
||||
|
||||
createAesCtr(key: Uint8Array, iv: Uint8Array): IAesCtr {
|
||||
const cipher = createCipheriv(`aes-${key.length * 8}-ctr`, key, iv)
|
||||
|
||||
const update = (data: Uint8Array) => toUint8Array(cipher.update(data))
|
||||
|
||||
return {
|
||||
process: update,
|
||||
}
|
||||
}
|
||||
|
||||
pbkdf2(
|
||||
password: Uint8Array,
|
||||
salt: Uint8Array,
|
||||
iterations: number,
|
||||
keylen = 64,
|
||||
algo = 'sha512',
|
||||
): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) =>
|
||||
pbkdf2(password, salt, iterations, keylen, algo, (err: Error | null, buf: Uint8Array) =>
|
||||
err !== null ? reject(err) : resolve(toUint8Array(buf)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sha1(data: Uint8Array): Uint8Array {
|
||||
return toUint8Array(createHash('sha1').update(data).digest())
|
||||
}
|
||||
|
||||
sha256(data: Uint8Array): Uint8Array {
|
||||
return toUint8Array(createHash('sha256').update(data).digest())
|
||||
}
|
||||
|
||||
hmacSha256(data: Uint8Array, key: Uint8Array): Uint8Array {
|
||||
return toUint8Array(createHmac('sha256', key).update(data).digest())
|
||||
}
|
||||
|
||||
createAesIge(key: Uint8Array, iv: Uint8Array): IEncryptionScheme {
|
||||
return {
|
||||
encrypt(data: Uint8Array): Uint8Array {
|
||||
return ige256Encrypt(data, key, iv)
|
||||
},
|
||||
decrypt(data: Uint8Array): Uint8Array {
|
||||
return ige256Decrypt(data, key, iv)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
gzip(data: Uint8Array, maxSize: number): Uint8Array | null {
|
||||
try {
|
||||
// telegram accepts both zlib and gzip, but zlib is faster and has less overhead, so we use it here
|
||||
return toUint8Array(
|
||||
deflateSync(data, {
|
||||
maxOutputLength: maxSize,
|
||||
}),
|
||||
)
|
||||
// hot path, avoid additional runtime checks
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
if (e.code === 'ERR_BUFFER_TOO_LARGE') {
|
||||
return null
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
gunzip(data: Uint8Array): Uint8Array {
|
||||
return toUint8Array(gunzipSync(data))
|
||||
}
|
||||
|
||||
randomFill(buf: Uint8Array) {
|
||||
crypto.getRandomValues(buf)
|
||||
}
|
||||
}
|
21
packages/deno/src/utils/exit-hook.ts
Normal file
21
packages/deno/src/utils/exit-hook.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
const callbacks = new Set<() => void>()
|
||||
|
||||
let registered = false
|
||||
|
||||
export function beforeExit(fn: () => void): () => void {
|
||||
if (!registered) {
|
||||
registered = true
|
||||
|
||||
globalThis.addEventListener('unload', () => {
|
||||
for (const callback of callbacks) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
callbacks.add(fn)
|
||||
|
||||
return () => {
|
||||
callbacks.delete(fn)
|
||||
}
|
||||
}
|
50
packages/deno/src/utils/normalize-file.ts
Normal file
50
packages/deno/src/utils/normalize-file.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { ReadStream } from 'node:fs'
|
||||
import { stat } from 'node:fs/promises'
|
||||
import { basename } from 'node:path'
|
||||
import { Readable as NodeReadable } from 'node:stream'
|
||||
|
||||
import { UploadFileLike } from '@mtcute/core'
|
||||
import { extractFileName } from '@mtcute/core/utils.js'
|
||||
|
||||
export async function normalizeFile(file: UploadFileLike) {
|
||||
if (typeof file === 'string') {
|
||||
const fd = await Deno.open(file, { read: true })
|
||||
|
||||
return {
|
||||
file: fd.readable,
|
||||
fileSize: (await fd.stat()).size,
|
||||
fileName: extractFileName(file),
|
||||
}
|
||||
}
|
||||
|
||||
if (file instanceof Deno.FsFile) {
|
||||
const stat = await file.stat()
|
||||
|
||||
return {
|
||||
file: file.readable,
|
||||
// https://github.com/denoland/deno/issues/23591
|
||||
// fileName: ...,
|
||||
fileSize: stat.size,
|
||||
}
|
||||
}
|
||||
|
||||
// while these are not Deno-specific, they still may happen
|
||||
if (file instanceof ReadStream) {
|
||||
const fileName = basename(file.path.toString())
|
||||
const fileSize = await stat(file.path.toString()).then((stat) => stat.size)
|
||||
|
||||
return {
|
||||
file: NodeReadable.toWeb(file) as unknown as ReadableStream<Uint8Array>,
|
||||
fileName,
|
||||
fileSize,
|
||||
}
|
||||
}
|
||||
|
||||
if (file instanceof NodeReadable) {
|
||||
return {
|
||||
file: NodeReadable.toWeb(file) as unknown as ReadableStream<Uint8Array>,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
138
packages/deno/src/utils/tcp.ts
Normal file
138
packages/deno/src/utils/tcp.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
import EventEmitter from 'node:events'
|
||||
|
||||
import { IntermediatePacketCodec, IPacketCodec, ITelegramTransport, MtcuteError, TransportState } from '@mtcute/core'
|
||||
import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js'
|
||||
|
||||
import { writeAll } from '@std/io/write-all'
|
||||
|
||||
/**
|
||||
* Base for TCP transports.
|
||||
* Subclasses must provide packet codec in `_packetCodec` property
|
||||
*/
|
||||
export abstract class BaseTcpTransport extends EventEmitter implements ITelegramTransport {
|
||||
protected _currentDc: BasicDcOption | null = null
|
||||
protected _state: TransportState = TransportState.Idle
|
||||
protected _socket: Deno.TcpConn | null = null
|
||||
|
||||
abstract _packetCodec: IPacketCodec
|
||||
protected _crypto!: ICryptoProvider
|
||||
protected log!: Logger
|
||||
|
||||
packetCodecInitialized = false
|
||||
|
||||
private _updateLogPrefix() {
|
||||
if (this._currentDc) {
|
||||
this.log.prefix = `[TCP:${this._currentDc.ipAddress}:${this._currentDc.port}] `
|
||||
} else {
|
||||
this.log.prefix = '[TCP:disconnected] '
|
||||
}
|
||||
}
|
||||
|
||||
setup(crypto: ICryptoProvider, log: Logger): void {
|
||||
this._crypto = crypto
|
||||
this.log = log.create('tcp')
|
||||
this._updateLogPrefix()
|
||||
}
|
||||
|
||||
state(): TransportState {
|
||||
return this._state
|
||||
}
|
||||
|
||||
currentDc(): BasicDcOption | null {
|
||||
return this._currentDc
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
connect(dc: BasicDcOption, testMode: boolean): void {
|
||||
if (this._state !== TransportState.Idle) {
|
||||
throw new MtcuteError('Transport is not IDLE')
|
||||
}
|
||||
|
||||
if (!this.packetCodecInitialized) {
|
||||
this._packetCodec.setup?.(this._crypto, this.log)
|
||||
this._packetCodec.on('error', (err) => this.emit('error', err))
|
||||
this._packetCodec.on('packet', (buf) => this.emit('message', buf))
|
||||
this.packetCodecInitialized = true
|
||||
}
|
||||
|
||||
this._state = TransportState.Connecting
|
||||
this._currentDc = dc
|
||||
this._updateLogPrefix()
|
||||
|
||||
this.log.debug('connecting to %j', dc)
|
||||
|
||||
Deno.connect({
|
||||
hostname: dc.ipAddress,
|
||||
port: dc.port,
|
||||
transport: 'tcp',
|
||||
})
|
||||
.then(this.handleConnect.bind(this))
|
||||
.catch((err) => {
|
||||
this.handleError(err)
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this._state === TransportState.Idle) return
|
||||
this.log.info('connection closed')
|
||||
|
||||
this._state = TransportState.Idle
|
||||
this._socket?.close()
|
||||
this._socket = null
|
||||
this._currentDc = null
|
||||
this._packetCodec.reset()
|
||||
this.emit('close')
|
||||
}
|
||||
|
||||
handleError(error: unknown): void {
|
||||
this.log.error('error: %s', error)
|
||||
|
||||
if (this.listenerCount('error') > 0) {
|
||||
this.emit('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
async handleConnect(socket: Deno.TcpConn): Promise<void> {
|
||||
this._socket = socket
|
||||
this.log.info('connected')
|
||||
|
||||
try {
|
||||
const packet = await this._packetCodec.tag()
|
||||
|
||||
if (packet.length) {
|
||||
await writeAll(this._socket, packet)
|
||||
}
|
||||
|
||||
this._state = TransportState.Ready
|
||||
this.emit('ready')
|
||||
|
||||
const reader = this._socket.readable.getReader()
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
this._packetCodec.feed(value)
|
||||
}
|
||||
} catch (e) {
|
||||
this.handleError(e)
|
||||
}
|
||||
|
||||
this.close()
|
||||
}
|
||||
|
||||
async send(bytes: Uint8Array): Promise<void> {
|
||||
const framed = await this._packetCodec.encode(bytes)
|
||||
|
||||
if (this._state !== TransportState.Ready) {
|
||||
throw new MtcuteError('Transport is not READY')
|
||||
}
|
||||
|
||||
await writeAll(this._socket!, framed)
|
||||
}
|
||||
}
|
||||
|
||||
export class TcpTransport extends BaseTcpTransport {
|
||||
_packetCodec = new IntermediatePacketCodec()
|
||||
}
|
70
packages/deno/src/worker.ts
Normal file
70
packages/deno/src/worker.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { setPlatform } from '@mtcute/core/platform.js'
|
||||
import {
|
||||
ClientMessageHandler,
|
||||
RespondFn,
|
||||
SendFn,
|
||||
SomeWorker,
|
||||
TelegramWorker as TelegramWorkerBase,
|
||||
TelegramWorkerOptions,
|
||||
TelegramWorkerPort as TelegramWorkerPortBase,
|
||||
TelegramWorkerPortOptions,
|
||||
WorkerCustomMethods,
|
||||
WorkerMessageHandler,
|
||||
} from '@mtcute/core/worker.js'
|
||||
|
||||
import { DenoPlatform } from './platform.js'
|
||||
|
||||
export type { TelegramWorkerOptions, TelegramWorkerPortOptions, WorkerCustomMethods }
|
||||
|
||||
let _registered = false
|
||||
|
||||
export class TelegramWorker<T extends WorkerCustomMethods> extends TelegramWorkerBase<T> {
|
||||
registerWorker(handler: WorkerMessageHandler): RespondFn {
|
||||
if (_registered) {
|
||||
throw new Error('TelegramWorker must be created only once')
|
||||
}
|
||||
|
||||
_registered = true
|
||||
|
||||
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
|
||||
const respond: RespondFn = self.postMessage.bind(self)
|
||||
|
||||
// eslint-disable-next-line
|
||||
self.addEventListener('message', (message) => handler(message.data, respond))
|
||||
|
||||
return respond
|
||||
}
|
||||
|
||||
throw new Error('TelegramWorker must be created from a worker')
|
||||
}
|
||||
}
|
||||
|
||||
const platform = new DenoPlatform()
|
||||
|
||||
export class TelegramWorkerPort<T extends WorkerCustomMethods> extends TelegramWorkerPortBase<T> {
|
||||
constructor(readonly options: TelegramWorkerPortOptions) {
|
||||
setPlatform(platform)
|
||||
super(options)
|
||||
}
|
||||
|
||||
connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] {
|
||||
if (worker instanceof Worker) {
|
||||
const send: SendFn = worker.postMessage.bind(worker)
|
||||
|
||||
const messageHandler = (ev: MessageEvent) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
handler(ev.data)
|
||||
}
|
||||
|
||||
worker.addEventListener('message', messageHandler)
|
||||
|
||||
return [
|
||||
send,
|
||||
() => {
|
||||
worker.removeEventListener('message', messageHandler)
|
||||
},
|
||||
]
|
||||
}
|
||||
throw new Error('Only workers are supported')
|
||||
}
|
||||
}
|
16
packages/deno/tsconfig.json
Normal file
16
packages/deno/tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
},
|
||||
"include": [
|
||||
"./src",
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../dispatcher" },
|
||||
{ "path": "../html-parser" },
|
||||
{ "path": "../markdown-parser" }
|
||||
]
|
||||
}
|
10
packages/deno/typedoc.cjs
Normal file
10
packages/deno/typedoc.cjs
Normal file
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
extends: ['../../.config/typedoc/config.base.cjs'],
|
||||
entryPoints: ['./src/index.ts'],
|
||||
externalPattern: [
|
||||
'../core/**',
|
||||
'../html-parser/**',
|
||||
'../markdown-parser/**',
|
||||
'../sqlite/**',
|
||||
],
|
||||
}
|
|
@ -6,5 +6,8 @@
|
|||
},
|
||||
"include": [
|
||||
"./src",
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
]
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export class NodePlatform implements ICorePlatform {
|
|||
declare normalizeFile: typeof normalizeFile
|
||||
|
||||
getDeviceModel(): string {
|
||||
return `${os.type()} ${os.arch()} ${os.release()}`
|
||||
return `Node.js/${process.version} (${os.type()} ${os.arch()})`
|
||||
}
|
||||
|
||||
getDefaultLogLevel(): number | null {
|
||||
|
|
|
@ -4,7 +4,6 @@ import { readFile } from 'fs/promises'
|
|||
import { createRequire } from 'module'
|
||||
import { deflateSync, gunzipSync } from 'zlib'
|
||||
|
||||
import { MaybePromise } from '@mtcute/core'
|
||||
import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from '@mtcute/core/utils.js'
|
||||
import { ige256Decrypt, ige256Encrypt, initSync } from '@mtcute/wasm'
|
||||
|
||||
|
@ -25,7 +24,7 @@ export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider {
|
|||
iterations: number,
|
||||
keylen = 64,
|
||||
algo = 'sha512',
|
||||
): MaybePromise<Uint8Array> {
|
||||
): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) =>
|
||||
pbkdf2(password, salt, iterations, keylen, algo, (err: Error | null, buf: Uint8Array) =>
|
||||
err !== null ? reject(err) : resolve(buf),
|
||||
|
|
|
@ -134,7 +134,8 @@ if (import.meta.env.TEST_ENV === 'node') {
|
|||
it('should propagate errors', async () => {
|
||||
const t = await create()
|
||||
|
||||
const spyEmit = vi.spyOn(t, 'emit').mockImplementation(() => true)
|
||||
const spyEmit = vi.fn()
|
||||
t.on('error', spyEmit)
|
||||
|
||||
t.connect(defaultProductionDc.main, false)
|
||||
await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready))
|
||||
|
@ -147,7 +148,7 @@ if (import.meta.env.TEST_ENV === 'node') {
|
|||
]
|
||||
onErrorCall[1](new Error('test error'))
|
||||
|
||||
expect(spyEmit).toHaveBeenCalledWith('error', new Error('test error'))
|
||||
expect(spyEmit).toHaveBeenCalledWith(new Error('test error'))
|
||||
})
|
||||
})
|
||||
} else {
|
||||
|
|
|
@ -84,8 +84,11 @@ export abstract class BaseTcpTransport extends EventEmitter implements ITelegram
|
|||
|
||||
handleError(error: Error): void {
|
||||
this.log.error('error: %s', error.stack)
|
||||
|
||||
if (this.listenerCount('error') > 0) {
|
||||
this.emit('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
handleConnect(): void {
|
||||
this.log.info('connected')
|
||||
|
|
2
packages/web/src/common-internals-web/readme.md
Normal file
2
packages/web/src/common-internals-web/readme.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
this folder is for common code across `@mtcute/web` and `@mtcute/deno`.
|
||||
it is symlinked into `@mtcute/deno`
|
|
@ -9,7 +9,7 @@ export function utf8ByteLength(str: string) {
|
|||
const code = str.charCodeAt(i)
|
||||
if (code > 0x7f && code <= 0x7ff) s++
|
||||
else if (code > 0x7ff && code <= 0xffff) s += 2
|
||||
if (code >= 0xDC00 && code <= 0xDFFF) i-- //trail surrogate
|
||||
if (code >= 0xdc00 && code <= 0xdfff) i-- //trail surrogate
|
||||
}
|
||||
|
||||
return s
|
|
@ -1,10 +1,10 @@
|
|||
import { ICorePlatform } from '@mtcute/core/platform.js'
|
||||
|
||||
import { base64Decode, base64Encode } from './encodings/base64.js'
|
||||
import { hexDecode, hexEncode } from './encodings/hex.js'
|
||||
import { utf8ByteLength, utf8Decode, utf8Encode } from './encodings/utf8.js'
|
||||
import { base64Decode, base64Encode } from './common-internals-web/base64.js'
|
||||
import { hexDecode, hexEncode } from './common-internals-web/hex.js'
|
||||
import { defaultLoggingHandler } from './common-internals-web/logging.js'
|
||||
import { utf8ByteLength, utf8Decode, utf8Encode } from './common-internals-web/utf8.js'
|
||||
import { beforeExit } from './exit-hook.js'
|
||||
import { defaultLoggingHandler } from './logging.js'
|
||||
|
||||
export class WebPlatform implements ICorePlatform {
|
||||
// ICorePlatform
|
||||
|
@ -52,7 +52,6 @@ export class WebPlatform implements ICorePlatform {
|
|||
declare utf8Decode: typeof utf8Decode
|
||||
declare hexEncode: typeof hexEncode
|
||||
declare hexDecode: typeof hexDecode
|
||||
|
||||
declare base64Encode: typeof base64Encode
|
||||
declare base64Decode: typeof base64Decode
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export class TelegramWorker<T extends WorkerCustomMethods> extends TelegramWorke
|
|||
}
|
||||
}
|
||||
|
||||
self.onconnect = (event) => {
|
||||
self.onconnect = (event: MessageEvent) => {
|
||||
const port = event.ports[0]
|
||||
connections.push(port)
|
||||
|
||||
|
@ -91,7 +91,7 @@ export class TelegramWorker<T extends WorkerCustomMethods> extends TelegramWorke
|
|||
const respond: RespondFn = self.postMessage.bind(self)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
self.addEventListener('message', (message) => handler(message.data, respond))
|
||||
self.addEventListener('message', (message: MessageEvent) => handler(message.data, respond))
|
||||
|
||||
return respond
|
||||
}
|
||||
|
|
151
pnpm-lock.yaml
151
pnpm-lock.yaml
|
@ -19,8 +19,11 @@ importers:
|
|||
specifier: 17.6.5
|
||||
version: 17.6.5
|
||||
'@teidesu/slow-types-compiler':
|
||||
specifier: 1.0.2
|
||||
version: 1.0.2(typescript@5.4.3)
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0(typescript@5.4.3)
|
||||
'@types/deno':
|
||||
specifier: npm:@teidesu/deno-types@1.42.4
|
||||
version: /@teidesu/deno-types@1.42.4
|
||||
'@types/node':
|
||||
specifier: 20.10.0
|
||||
version: 20.10.0
|
||||
|
@ -231,6 +234,31 @@ importers:
|
|||
specifier: workspace:^
|
||||
version: link:../test
|
||||
|
||||
packages/deno:
|
||||
dependencies:
|
||||
'@db/sqlite':
|
||||
specifier: npm:@jsr/db__sqlite@0.11.1
|
||||
version: /@jsr/db__sqlite@0.11.1
|
||||
'@mtcute/core':
|
||||
specifier: workspace:^
|
||||
version: link:../core
|
||||
'@mtcute/html-parser':
|
||||
specifier: workspace:^
|
||||
version: link:../html-parser
|
||||
'@mtcute/markdown-parser':
|
||||
specifier: workspace:^
|
||||
version: link:../markdown-parser
|
||||
'@mtcute/wasm':
|
||||
specifier: workspace:^
|
||||
version: link:../wasm
|
||||
'@std/io':
|
||||
specifier: npm:@jsr/std__io@0.223.0
|
||||
version: /@jsr/std__io@0.223.0
|
||||
devDependencies:
|
||||
'@mtcute/test':
|
||||
specifier: workspace:^
|
||||
version: link:../test
|
||||
|
||||
packages/dispatcher:
|
||||
dependencies:
|
||||
'@mtcute/core':
|
||||
|
@ -1189,6 +1217,86 @@ packages:
|
|||
'@jridgewell/sourcemap-codec': 1.4.11
|
||||
dev: true
|
||||
|
||||
/@jsr/db__sqlite@0.11.1:
|
||||
resolution: {integrity: sha512-6IKyfi+TQan431kwOy3WrdzgKwITuDdSKfq6nkWINfNWknPQ+lQ6/R008bAUUz+AwW8e0prxi3IMMxzELUV8Lw==, tarball: https://npm.jsr.io/~/8/@jsr/db__sqlite/0.11.1.tgz}
|
||||
dependencies:
|
||||
'@jsr/denosaurs__plug': 1.0.6
|
||||
'@jsr/std__path': 0.217.0
|
||||
dev: false
|
||||
|
||||
/@jsr/denosaurs__plug@1.0.6:
|
||||
resolution: {integrity: sha512-2uqvX2xpDy5W76jJVKazXvHuh5WPNg8eUV+2u+Hcn5XLwKqWGr/xj4wQFRMXrS12Xhya+ToZdUg4gxLh+XOOCg==, tarball: https://npm.jsr.io/~/8/@jsr/denosaurs__plug/1.0.6.tgz}
|
||||
dependencies:
|
||||
'@jsr/std__encoding': 0.221.0
|
||||
'@jsr/std__fmt': 0.221.0
|
||||
'@jsr/std__fs': 0.221.0
|
||||
'@jsr/std__path': 0.221.0
|
||||
dev: false
|
||||
|
||||
/@jsr/std__assert@0.217.0:
|
||||
resolution: {integrity: sha512-RCQbXJeUVCgDGEPsrO57CI9Cgbo9NAWsJUhZ7vrHgtD//Ic32YmUQazdGKPZzao5Zn8dP6xV4Nma3HHZC5ySTw==, tarball: https://npm.jsr.io/~/8/@jsr/std__assert/0.217.0.tgz}
|
||||
dependencies:
|
||||
'@jsr/std__fmt': 0.217.0
|
||||
dev: false
|
||||
|
||||
/@jsr/std__assert@0.221.0:
|
||||
resolution: {integrity: sha512-2B+5fq4Rar8NmLms7sv9YfYlMukZDTNMQV5fXjtZvnaKc8ljt+59UsMtIjTXFsDwsAx7VoxkMKmmdHxlP+h5JA==, tarball: https://npm.jsr.io/~/8/@jsr/std__assert/0.221.0.tgz}
|
||||
dependencies:
|
||||
'@jsr/std__fmt': 0.221.0
|
||||
dev: false
|
||||
|
||||
/@jsr/std__assert@0.223.0:
|
||||
resolution: {integrity: sha512-9FWOoAQN1uF5SliWw3IgdXmk2usz5txvausX4sLAASHfQMbUSCe1akcD7HgFV01J/2Mr9TfCjPvsSUzuuASouQ==, tarball: https://npm.jsr.io/~/8/@jsr/std__assert/0.223.0.tgz}
|
||||
dependencies:
|
||||
'@jsr/std__fmt': 0.223.0
|
||||
dev: false
|
||||
|
||||
/@jsr/std__bytes@0.223.0:
|
||||
resolution: {integrity: sha512-BBjhj0uFlB3+AVEmaPygEwY5CL5mj3vSZlusC8xxjCRNWDYGukfQT/F5GOTTfjeaq7njduk7TYe6e5cDg659yg==, tarball: https://npm.jsr.io/~/8/@jsr/std__bytes/0.223.0.tgz}
|
||||
dev: false
|
||||
|
||||
/@jsr/std__encoding@0.221.0:
|
||||
resolution: {integrity: sha512-FT5i/WHNtXJvqOITDK0eOVIyyOphqtxwhzo5PiVWoYTFmUuFcRYKas39GT1UQDi4s24FcHd2deQEBbi3tPAj1Q==, tarball: https://npm.jsr.io/~/8/@jsr/std__encoding/0.221.0.tgz}
|
||||
dev: false
|
||||
|
||||
/@jsr/std__fmt@0.217.0:
|
||||
resolution: {integrity: sha512-L3mVYP7DsujrJ001SvPr4Fl/Fu0e3uzgHJ6NYTRUk7sgi9k7YKeLOLVwRijUX7qIsp3Ourp2DyAHHgYDgT4GcQ==, tarball: https://npm.jsr.io/~/8/@jsr/std__fmt/0.217.0.tgz}
|
||||
dev: false
|
||||
|
||||
/@jsr/std__fmt@0.221.0:
|
||||
resolution: {integrity: sha512-VLqM052U78LQ11p/KfqI49a2/sDbKtHFHuxO/h+3Cnvhze9beIZU4Lg3Gpu8rGYjB2YS6CfXzKXHuyAJn5FJFg==, tarball: https://npm.jsr.io/~/8/@jsr/std__fmt/0.221.0.tgz}
|
||||
dev: false
|
||||
|
||||
/@jsr/std__fmt@0.223.0:
|
||||
resolution: {integrity: sha512-J6SVTw/l3C4hOwEuqnZ4ZHD1jVIIZt09fb5LP9CMGyVGNnoW8/lxJvCNhIOv+3ZXC1ErGlIzW4bgYSxHwbvSaQ==, tarball: https://npm.jsr.io/~/8/@jsr/std__fmt/0.223.0.tgz}
|
||||
dev: false
|
||||
|
||||
/@jsr/std__fs@0.221.0:
|
||||
resolution: {integrity: sha512-2XMlO67zQlKoxbCsfGOBVlnyWhMMdOzYUWfajvggfw2p+yITd9hJj9+tpfiwLf/88CzknhlMLwSCamSYjHKloA==, tarball: https://npm.jsr.io/~/8/@jsr/std__fs/0.221.0.tgz}
|
||||
dependencies:
|
||||
'@jsr/std__assert': 0.221.0
|
||||
'@jsr/std__path': 0.221.0
|
||||
dev: false
|
||||
|
||||
/@jsr/std__io@0.223.0:
|
||||
resolution: {integrity: sha512-K+OXJHsIf9227aYgNTaapEkpphHrI+oYVkl14UV+le+Fk9MzkJmebU0XAU6krgVS283mW7VPJsXVV3gD5JWvJw==, tarball: https://npm.jsr.io/~/8/@jsr/std__io/0.223.0.tgz}
|
||||
dependencies:
|
||||
'@jsr/std__assert': 0.223.0
|
||||
'@jsr/std__bytes': 0.223.0
|
||||
dev: false
|
||||
|
||||
/@jsr/std__path@0.217.0:
|
||||
resolution: {integrity: sha512-OkP+yiBJpFZKTH3gHqlepYU3TQzXM/UjEQ0U1gYw8BwVr87TwKfzwAb1WT1vY/Bs8NXScvuP4Kpu/UhEsNHD3A==, tarball: https://npm.jsr.io/~/8/@jsr/std__path/0.217.0.tgz}
|
||||
dependencies:
|
||||
'@jsr/std__assert': 0.217.0
|
||||
dev: false
|
||||
|
||||
/@jsr/std__path@0.221.0:
|
||||
resolution: {integrity: sha512-uOWaY4cWp28CFBSisr8M/92FtpyjiFO0+wQSH7GgmiXQUls+vALqdCGewFkunG8RfA/25RGdot5hFXedmtPdOg==, tarball: https://npm.jsr.io/~/8/@jsr/std__path/0.221.0.tgz}
|
||||
dependencies:
|
||||
'@jsr/std__assert': 0.221.0
|
||||
dev: false
|
||||
|
||||
/@ljharb/through@2.3.11:
|
||||
resolution: {integrity: sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -1362,8 +1470,12 @@ packages:
|
|||
/@sinclair/typebox@0.27.8:
|
||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||
|
||||
/@teidesu/slow-types-compiler@1.0.2(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-N0e3J/My4t405V5qD2kr6xXwLMlaB+el7bdYKLXJ2yyrLN1eAx4elf6qkeRTF4xy0GoiWrS0gKS0RZfddSOw1w==}
|
||||
/@teidesu/deno-types@1.42.4:
|
||||
resolution: {integrity: sha512-MMDkfWOsfedYWy+aPK4fAYyZfrBsY6+DeC7DGg6eESzh90zxuf1fSvXRsY8y09Hh4mm04tAf1632S2/JLaTXQg==}
|
||||
dev: true
|
||||
|
||||
/@teidesu/slow-types-compiler@1.1.0(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-+WUHSKh56B32Jk5aJgXf07E2EOkMX1yilvgKLKBCJPFAJZ4xeo1U5aDu3wwHX3lrFl7AiVGXUP+FfuHy8X43BA==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0
|
||||
|
@ -1371,6 +1483,7 @@ packages:
|
|||
arg: 5.0.2
|
||||
dedent: 1.5.3
|
||||
eager-async-pool: 1.0.0
|
||||
glob: 10.3.12
|
||||
gunzip-maybe: 1.4.2
|
||||
semver: 7.6.0
|
||||
tar-stream: 3.1.7
|
||||
|
@ -3634,6 +3747,18 @@ packages:
|
|||
path-scurry: 1.10.1
|
||||
dev: true
|
||||
|
||||
/glob@10.3.12:
|
||||
resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
foreground-child: 3.1.1
|
||||
jackspeak: 2.3.6
|
||||
minimatch: 9.0.3
|
||||
minipass: 7.0.4
|
||||
path-scurry: 1.10.2
|
||||
dev: true
|
||||
|
||||
/glob@7.2.0:
|
||||
resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
|
||||
dependencies:
|
||||
|
@ -4502,6 +4627,11 @@ packages:
|
|||
get-func-name: 2.0.2
|
||||
dev: true
|
||||
|
||||
/lru-cache@10.2.2:
|
||||
resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
|
||||
engines: {node: 14 || >=16.14}
|
||||
dev: true
|
||||
|
||||
/lru-cache@6.0.0:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -4747,6 +4877,11 @@ packages:
|
|||
resolution: {integrity: sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
/minipass@7.0.4:
|
||||
resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
dev: true
|
||||
|
||||
/minizlib@2.1.2:
|
||||
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
@ -5198,6 +5333,14 @@ packages:
|
|||
lru-cache: 9.1.2
|
||||
minipass: 6.0.2
|
||||
|
||||
/path-scurry@1.10.2:
|
||||
resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
dependencies:
|
||||
lru-cache: 10.2.2
|
||||
minipass: 7.0.4
|
||||
dev: true
|
||||
|
||||
/path-type@4.0.0:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
|
@ -428,11 +428,19 @@ if (buildConfig.buildTs && !IS_JSR) {
|
|||
'from: (data: any, encoding?: string) => { toString(encoding?: string): string }, ' +
|
||||
' }',
|
||||
SharedWorker: ['type', 'never'],
|
||||
WorkerGlobalScope:
|
||||
'{ ' +
|
||||
' new (): typeof WorkerGlobalScope, ' +
|
||||
' postMessage: (message: any, transfer?: Transferable[]) => void, ' +
|
||||
' addEventListener: (type: "message", listener: (ev: MessageEvent) => void) => void, ' +
|
||||
' }',
|
||||
process: '{ ' + 'hrtime: { bigint: () => bigint }, ' + '}',
|
||||
}
|
||||
|
||||
for (const [name, decl_] of Object.entries(nodeSpecificApis)) {
|
||||
if (fileContent.includes(name)) {
|
||||
if (name === 'Buffer' && fileContent.includes('node:buffer')) continue
|
||||
|
||||
changed = true
|
||||
const isType = Array.isArray(decl_) && decl_[0] === 'type'
|
||||
const decl = isType ? decl_[1] : decl_
|
||||
|
@ -499,6 +507,10 @@ if (IS_JSR) {
|
|||
for (const [name, version] of Object.entries(builtPkgJson.dependencies)) {
|
||||
if (name.startsWith('@mtcute/')) {
|
||||
importMap[name] = `jsr:${name}@${version}`
|
||||
} else if (version.startsWith('npm:@jsr/')) {
|
||||
const jsrName = version.slice(9).split('@')[0].replace('__', '/')
|
||||
const jsrVersion = version.slice(9).split('@')[1]
|
||||
importMap[name] = `jsr:@${jsrName}@${jsrVersion}`
|
||||
} else {
|
||||
importMap[name] = `npm:${name}@${version}`
|
||||
}
|
||||
|
@ -543,6 +555,38 @@ if (IS_JSR) {
|
|||
),
|
||||
)
|
||||
|
||||
if (process.env.E2E) {
|
||||
// populate dependencies, if any
|
||||
const depsToPopulate = []
|
||||
|
||||
for (const dep of Object.values(importMap)) {
|
||||
if (!dep.startsWith('jsr:')) continue
|
||||
if (dep.startsWith('jsr:@mtcute/')) continue
|
||||
depsToPopulate.push(dep.slice(4))
|
||||
}
|
||||
|
||||
if (depsToPopulate.length) {
|
||||
console.log('[i] Populating %d dependencies...', depsToPopulate.length)
|
||||
cp.spawnSync(
|
||||
'pnpm',
|
||||
[
|
||||
'exec',
|
||||
'slow-types-compiler',
|
||||
'populate',
|
||||
'--downstream',
|
||||
process.env.JSR_URL,
|
||||
'--token',
|
||||
process.env.JSR_TOKEN,
|
||||
'--unstable-create-via-api',
|
||||
...depsToPopulate,
|
||||
],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[i] Processing with slow-types-compiler...')
|
||||
const project = stc.createProject()
|
||||
stc.processPackage(project, denoJson)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const cp = require('child_process')
|
||||
const stc = require('@teidesu/slow-types-compiler')
|
||||
|
||||
const IS_JSR = process.env.JSR === '1'
|
||||
const MAIN_REGISTRY = IS_JSR ? 'http://jsr.test/' : 'https://registry.npmjs.org'
|
||||
|
@ -25,6 +26,7 @@ const JSR_EXCEPTIONS = {
|
|||
bun: 'never',
|
||||
'create-bot': 'never',
|
||||
'crypto-node': 'never',
|
||||
deno: 'only',
|
||||
node: 'never',
|
||||
'http-proxy': 'never',
|
||||
'socks-proxy': 'never',
|
||||
|
@ -53,30 +55,6 @@ async function checkVersion(name, version) {
|
|||
return fetchRetry(url).then((r) => r.status === 200)
|
||||
}
|
||||
|
||||
async function jsrMaybeCreatePackage(name) {
|
||||
// check if the package even exists
|
||||
const packageMeta = await fetchRetry(`${REGISTRY}api/scopes/mtcute/packages/${name}`)
|
||||
|
||||
if (packageMeta.status === 404) {
|
||||
console.error('[i] %s does not exist, creating..', name)
|
||||
|
||||
const create = await fetchRetry(`${REGISTRY}api/scopes/mtcute/packages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `token=${process.env.JSR_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify({ package: name }),
|
||||
})
|
||||
|
||||
if (create.status !== 200) {
|
||||
throw new Error(`Failed to create package: ${create.statusText} ${await create.text()}`)
|
||||
}
|
||||
} else if (packageMeta.status !== 200) {
|
||||
throw new Error(`Failed to check package: ${packageMeta.statusText} ${await packageMeta.text()}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function publishSinglePackage(name) {
|
||||
let packageDir = path.join(__dirname, '../packages', name)
|
||||
|
||||
|
@ -109,7 +87,11 @@ async function publishSinglePackage(name) {
|
|||
return
|
||||
}
|
||||
} else if (IS_JSR && process.env.JSR_TOKEN) {
|
||||
await jsrMaybeCreatePackage(name)
|
||||
await stc.jsrMaybeCreatePackage({
|
||||
name: `@mtcute/${name}`,
|
||||
token: process.env.JSR_TOKEN,
|
||||
registry: REGISTRY,
|
||||
})
|
||||
}
|
||||
|
||||
if (IS_JSR) {
|
||||
|
@ -156,7 +138,6 @@ function listPackages() {
|
|||
.map((d) => d.slice(8))
|
||||
}
|
||||
|
||||
const stc = require('@teidesu/slow-types-compiler')
|
||||
packages = stc.determinePublishOrder(map)
|
||||
console.log('[i] Publishing order:', packages.join(', '))
|
||||
}
|
||||
|
|
23
scripts/remove-jsr-sourcefiles.mjs
Normal file
23
scripts/remove-jsr-sourcefiles.mjs
Normal file
|
@ -0,0 +1,23 @@
|
|||
import * as fs from 'fs'
|
||||
import { globSync } from 'glob'
|
||||
import { join } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
// for whatever reason, jsr's npm compatibility jayer doesn't remove
|
||||
// original typescript source files, which results in type errors when
|
||||
// trying to build the project. this script removes all source files from @jsr/*
|
||||
// https://discord.com/channels/684898665143206084/1203185670508515399/1234222204044967967
|
||||
|
||||
const nodeModules = fileURLToPath(new URL('../node_modules', import.meta.url))
|
||||
|
||||
let count = 0
|
||||
|
||||
for (const file of globSync(join(nodeModules, '.pnpm/**/node_modules/@jsr/**/*.ts'))) {
|
||||
if (file.endsWith('.d.ts')) continue
|
||||
if (!fs.existsSync(file)) continue
|
||||
|
||||
fs.unlinkSync(file)
|
||||
count++
|
||||
}
|
||||
|
||||
console.log(`[jsr] removed ${count} source files`)
|
|
@ -19,6 +19,7 @@
|
|||
"composite": true,
|
||||
"types": [
|
||||
"node",
|
||||
"deno/ns",
|
||||
"vite/client"
|
||||
],
|
||||
"lib": [
|
||||
|
|
Loading…
Reference in a new issue