platform-agnostic sqlite implementation + bun support #26
71 changed files with 1115 additions and 316 deletions
|
@ -273,11 +273,19 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['e2e/**', 'packages/node/**'],
|
files: ['e2e/**', 'packages/node/**', 'packages/bun/**'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-restricted-globals': 'off',
|
'no-restricted-globals': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['packages/bun/**'],
|
||||||
|
rules: {
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'no-restricted-imports': 'off',
|
||||||
|
'import/no-relative-packages': 'off', // common-internals is symlinked from node
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
|
|
|
@ -5,5 +5,5 @@ const TEST_ENV = import.meta.env.TEST_ENV
|
||||||
if (TEST_ENV === 'browser') {
|
if (TEST_ENV === 'browser') {
|
||||||
setPlatform(new (await import('../../packages/web/src/platform.js')).WebPlatform())
|
setPlatform(new (await import('../../packages/web/src/platform.js')).WebPlatform())
|
||||||
} else {
|
} else {
|
||||||
setPlatform(new (await import('../../packages/node/src/platform.js')).NodePlatform())
|
setPlatform(new (await import('../../packages/node/src/common-internals-node/platform.js')).NodePlatform())
|
||||||
}
|
}
|
|
@ -26,7 +26,7 @@ const SKIP_TESTS = [
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: (() => {
|
entry: process.env.ENTRYPOINT ? [process.env.ENTRYPOINT] : (() => {
|
||||||
const files: string[] = []
|
const files: string[] = []
|
||||||
|
|
||||||
const packages = resolve(__dirname, '../packages')
|
const packages = resolve(__dirname, '../packages')
|
||||||
|
@ -57,11 +57,15 @@ export default defineConfig({
|
||||||
'module',
|
'module',
|
||||||
'fs',
|
'fs',
|
||||||
'fs/promises',
|
'fs/promises',
|
||||||
|
'node:fs',
|
||||||
|
'readline',
|
||||||
|
'worker_threads',
|
||||||
'events',
|
'events',
|
||||||
'path',
|
'path',
|
||||||
'util',
|
'util',
|
||||||
'os',
|
'os',
|
||||||
'bun:test',
|
'bun:test',
|
||||||
|
'bun:sqlite',
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
chunkFileNames: 'chunk-[hash].js',
|
chunkFileNames: 'chunk-[hash].js',
|
||||||
|
@ -73,7 +77,7 @@ export default defineConfig({
|
||||||
commonjsOptions: {
|
commonjsOptions: {
|
||||||
ignoreDynamicRequires: true,
|
ignoreDynamicRequires: true,
|
||||||
},
|
},
|
||||||
outDir: 'dist/tests',
|
outDir: process.env.OUT_DIR || 'dist/tests',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
minify: false,
|
minify: false,
|
||||||
|
|
2
.github/actions/init-bun/action.yml
vendored
2
.github/actions/init-bun/action.yml
vendored
|
@ -1,6 +1,6 @@
|
||||||
inputs:
|
inputs:
|
||||||
bun-version:
|
bun-version:
|
||||||
default: '1.0.25'
|
default: '1.0.32'
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "mtcute-e2e",
|
"name": "mtcute-e2e",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mtcute/bun": "*",
|
||||||
"@mtcute/core": "*",
|
"@mtcute/core": "*",
|
||||||
"@mtcute/crypto-node": "*",
|
"@mtcute/crypto-node": "*",
|
||||||
"@mtcute/dispatcher": "*",
|
"@mtcute/dispatcher": "*",
|
||||||
|
@ -13,7 +14,6 @@
|
||||||
"@mtcute/mtproxy": "*",
|
"@mtcute/mtproxy": "*",
|
||||||
"@mtcute/node": "*",
|
"@mtcute/node": "*",
|
||||||
"@mtcute/socks-proxy": "*",
|
"@mtcute/socks-proxy": "*",
|
||||||
"@mtcute/sqlite": "*",
|
|
||||||
"@mtcute/tl": "*",
|
"@mtcute/tl": "*",
|
||||||
"@mtcute/tl-runtime": "*",
|
"@mtcute/tl-runtime": "*",
|
||||||
"@mtcute/tl-utils": "*",
|
"@mtcute/tl-utils": "*",
|
||||||
|
|
|
@ -4,8 +4,7 @@ import { join } from 'path'
|
||||||
import { MaybePromise, MemoryStorage } from '@mtcute/core'
|
import { MaybePromise, MemoryStorage } from '@mtcute/core'
|
||||||
import { setPlatform } from '@mtcute/core/platform.js'
|
import { setPlatform } from '@mtcute/core/platform.js'
|
||||||
import { LogManager, sleep } from '@mtcute/core/utils.js'
|
import { LogManager, sleep } from '@mtcute/core/utils.js'
|
||||||
import { NodeCryptoProvider, NodePlatform, TcpTransport } from '@mtcute/node'
|
import { NodeCryptoProvider, NodePlatform, SqliteStorage, TcpTransport } from '@mtcute/node'
|
||||||
import { SqliteStorage } from '@mtcute/sqlite'
|
|
||||||
|
|
||||||
export const getApiParams = (storage?: string) => {
|
export const getApiParams = (storage?: string) => {
|
||||||
if (!process.env.API_ID || !process.env.API_HASH) {
|
if (!process.env.API_ID || !process.env.API_HASH) {
|
||||||
|
|
25
packages/bun/README.md
Normal file
25
packages/bun/README.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# @mtcute/bun
|
||||||
|
|
||||||
|
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_node.html)
|
||||||
|
|
||||||
|
‼️ **Experimental** Bun support package for mtcute. Includes:
|
||||||
|
- SQLite storage (based on `bun:sqlite`)
|
||||||
|
- TCP transport (based on Bun-native APIs)
|
||||||
|
- `TelegramClient` implementation using the above
|
||||||
|
- HTML and Markdown parsers
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TelegramClient } from '@mtcute/bun'
|
||||||
|
|
||||||
|
const tg = new TelegramClient({
|
||||||
|
apiId: 12345,
|
||||||
|
apiHash: 'abcdef',
|
||||||
|
storage: 'my-account'
|
||||||
|
})
|
||||||
|
|
||||||
|
tg.run(async (user) => {
|
||||||
|
console.log(`✨ logged in as ${user.displayName}`)
|
||||||
|
})
|
||||||
|
```
|
1
packages/bun/build.config.cjs
Normal file
1
packages/bun/build.config.cjs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
module.exports = () => ({ buildCjs: false })
|
35
packages/bun/package.json
Normal file
35
packages/bun/package.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "@mtcute/bun",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.8.0",
|
||||||
|
"description": "Meta-package for Bun",
|
||||||
|
"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 bun"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./utils.js": "./src/utils.ts"
|
||||||
|
},
|
||||||
|
"distOnlyFields": {
|
||||||
|
"exports": {
|
||||||
|
".": "./index.js",
|
||||||
|
"./utils.js": "./utils.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mtcute/core": "workspace:^",
|
||||||
|
"@mtcute/wasm": "workspace:^",
|
||||||
|
"@mtcute/markdown-parser": "workspace:^",
|
||||||
|
"@mtcute/html-parser": "workspace:^"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mtcute/test": "workspace:^",
|
||||||
|
"bun-types": "1.0.33"
|
||||||
|
}
|
||||||
|
}
|
147
packages/bun/src/client.ts
Normal file
147
packages/bun/src/client.ts
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import { createInterface, Interface as RlInterface } from 'readline'
|
||||||
|
|
||||||
|
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 { NodePlatform } from './common-internals-node/platform.js'
|
||||||
|
import { downloadToFile } from './methods/download-file.js'
|
||||||
|
import { downloadAsNodeStream } from './methods/download-node-stream.js'
|
||||||
|
import { SqliteStorage } from './sqlite/index.js'
|
||||||
|
import { BunCryptoProvider } 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 NodePlatform())
|
||||||
|
|
||||||
|
super({
|
||||||
|
crypto: new BunCryptoProvider(),
|
||||||
|
transport: () => new TcpTransport(),
|
||||||
|
...opts,
|
||||||
|
storage:
|
||||||
|
typeof opts.storage === 'string' ?
|
||||||
|
new SqliteStorage(opts.storage) :
|
||||||
|
opts.storage ?? new SqliteStorage('client.session'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Telegram client for use in Node.js
|
||||||
|
*/
|
||||||
|
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({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadAsNodeStream(location: FileDownloadLocation, params?: FileDownloadParameters | undefined) {
|
||||||
|
return downloadAsNodeStream(this, location, params)
|
||||||
|
}
|
||||||
|
}
|
1
packages/bun/src/common-internals-node
Symbolic link
1
packages/bun/src/common-internals-node
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../node/src/common-internals-node
|
9
packages/bun/src/index.ts
Normal file
9
packages/bun/src/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export * from './client.js'
|
||||||
|
export * from './common-internals-node/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'
|
3
packages/bun/src/methods.ts
Normal file
3
packages/bun/src/methods.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { downloadToFile } from './methods/download-file.js'
|
||||||
|
export { downloadAsNodeStream } from './methods/download-node-stream.js'
|
||||||
|
export * from '@mtcute/core/methods.js'
|
39
packages/bun/src/methods/download-file.ts
Normal file
39
packages/bun/src/methods/download-file.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { unlinkSync } from 'node:fs'
|
||||||
|
|
||||||
|
import { FileDownloadLocation, FileDownloadParameters, FileLocation, ITelegramClient } from '@mtcute/core'
|
||||||
|
import { downloadAsIterable } from '@mtcute/core/methods.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 Bun.write(filename, location.location)
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = Bun.file(filename).writer()
|
||||||
|
|
||||||
|
if (params?.abortSignal) {
|
||||||
|
params.abortSignal.addEventListener('abort', () => {
|
||||||
|
client.log.debug('aborting file download %s - cleaning up', filename)
|
||||||
|
Promise.resolve(output.end()).catch(() => {})
|
||||||
|
unlinkSync(filename)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const chunk of downloadAsIterable(client, location, params)) {
|
||||||
|
output.write(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
await output.end()
|
||||||
|
}
|
19
packages/bun/src/methods/download-node-stream.ts
Normal file
19
packages/bun/src/methods/download-node-stream.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Readable } from 'stream'
|
||||||
|
|
||||||
|
import { FileDownloadLocation, FileDownloadParameters, ITelegramClient } from '@mtcute/core'
|
||||||
|
import { downloadAsStream } from '@mtcute/core/methods.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a remote file as a Node.js Readable stream
|
||||||
|
* (discouraged under Bun, since Web Streams are first-class).
|
||||||
|
*
|
||||||
|
* @param params File download parameters
|
||||||
|
*/
|
||||||
|
export function downloadAsNodeStream(
|
||||||
|
client: ITelegramClient,
|
||||||
|
location: FileDownloadLocation,
|
||||||
|
params?: FileDownloadParameters,
|
||||||
|
): Readable {
|
||||||
|
// @ts-expect-error typings are wrong
|
||||||
|
return Readable.fromWeb(downloadAsStream(client, location, params))
|
||||||
|
}
|
36
packages/bun/src/sqlite/driver.ts
Normal file
36
packages/bun/src/sqlite/driver.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { Database } from 'bun:sqlite'
|
||||||
|
|
||||||
|
import { BaseSqliteStorageDriver, ISqliteDatabase } from '@mtcute/core'
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
_createDatabase(): ISqliteDatabase {
|
||||||
|
const db = new Database(this.filename)
|
||||||
|
|
||||||
|
if (!this.params?.disableWal) {
|
||||||
|
db.exec('PRAGMA journal_mode = WAL;')
|
||||||
|
}
|
||||||
|
|
||||||
|
return db as ISqliteDatabase
|
||||||
|
}
|
||||||
|
}
|
14
packages/bun/src/sqlite/index.ts
Normal file
14
packages/bun/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))
|
||||||
|
}
|
||||||
|
}
|
31
packages/bun/src/sqlite/sqlite.test.ts
Normal file
31
packages/bun/src/sqlite/sqlite.test.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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 === 'bun') {
|
||||||
|
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/bun/src/utils.ts
Normal file
1
packages/bun/src/utils.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from '@mtcute/core/utils.js'
|
13
packages/bun/src/utils/crypto.test.ts
Normal file
13
packages/bun/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 === 'bun') {
|
||||||
|
describe('BunCryptoProvider', async () => {
|
||||||
|
const { BunCryptoProvider } = await import('./crypto.js')
|
||||||
|
|
||||||
|
testCryptoProvider(new BunCryptoProvider())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
describe.skip('BunCryptoProvider', () => {})
|
||||||
|
}
|
119
packages/bun/src/utils/crypto.ts
Normal file
119
packages/bun/src/utils/crypto.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
|
||||||
|
import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from '@mtcute/core/utils.js'
|
||||||
|
import {
|
||||||
|
createCtr256,
|
||||||
|
ctr256,
|
||||||
|
deflateMaxSize,
|
||||||
|
freeCtr256,
|
||||||
|
gunzip,
|
||||||
|
ige256Decrypt,
|
||||||
|
ige256Encrypt,
|
||||||
|
initSync,
|
||||||
|
} from '@mtcute/wasm'
|
||||||
|
|
||||||
|
// we currently prefer subtle crypto and wasm for ctr because bun uses browserify polyfills for node:crypto
|
||||||
|
// which are slow AND semi-broken
|
||||||
|
// we currently prefer wasm for gzip because bun uses browserify polyfills for node:zlib too
|
||||||
|
// native node-api addon is broken on macos so we don't support it either
|
||||||
|
//
|
||||||
|
// largely just copy-pasting from @mtcute/web, todo: maybe refactor this into common-internals-web?
|
||||||
|
|
||||||
|
const ALGO_TO_SUBTLE: Record<string, string> = {
|
||||||
|
sha256: 'SHA-256',
|
||||||
|
sha1: 'SHA-1',
|
||||||
|
sha512: 'SHA-512',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BunCryptoProvider extends BaseCryptoProvider implements ICryptoProvider {
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
const wasmFile = require.resolve('@mtcute/wasm/mtcute.wasm')
|
||||||
|
const wasm = await readFile(wasmFile)
|
||||||
|
initSync(wasm)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createAesCtr(key: Uint8Array, iv: Uint8Array): IAesCtr {
|
||||||
|
const ctx = createCtr256(key, iv)
|
||||||
|
|
||||||
|
return {
|
||||||
|
process: (data) => ctr256(ctx, data),
|
||||||
|
close: () => freeCtr256(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pbkdf2(
|
||||||
|
password: Uint8Array,
|
||||||
|
salt: Uint8Array,
|
||||||
|
iterations: number,
|
||||||
|
keylen = 64,
|
||||||
|
algo = 'sha512',
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const keyMaterial = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits'])
|
||||||
|
|
||||||
|
return crypto.subtle
|
||||||
|
.deriveBits(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt,
|
||||||
|
iterations,
|
||||||
|
hash: algo ? ALGO_TO_SUBTLE[algo] : 'SHA-512',
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
(keylen || 64) * 8,
|
||||||
|
)
|
||||||
|
.then((result) => new Uint8Array(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
sha1(data: Uint8Array): Uint8Array {
|
||||||
|
const res = new Uint8Array(Bun.SHA1.byteLength)
|
||||||
|
Bun.SHA1.hash(data, res)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
sha256(data: Uint8Array): Uint8Array {
|
||||||
|
const res = new Uint8Array(Bun.SHA256.byteLength)
|
||||||
|
Bun.SHA256.hash(data, res)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
async hmacSha256(data: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
|
||||||
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
key,
|
||||||
|
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
||||||
|
false,
|
||||||
|
['sign'],
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await crypto.subtle.sign({ name: 'HMAC' }, keyMaterial, data)
|
||||||
|
|
||||||
|
return new Uint8Array(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
gzip(data: Uint8Array, maxSize: number): Uint8Array | null {
|
||||||
|
return deflateMaxSize(data, maxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
gunzip(data: Uint8Array): Uint8Array {
|
||||||
|
return gunzip(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
randomFill(buf: Uint8Array) {
|
||||||
|
crypto.getRandomValues(buf)
|
||||||
|
}
|
||||||
|
}
|
33
packages/bun/src/utils/normalize-file.ts
Normal file
33
packages/bun/src/utils/normalize-file.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { ReadStream } from 'fs'
|
||||||
|
import { stat } from 'fs/promises'
|
||||||
|
import { basename } from 'path'
|
||||||
|
import { Readable as NodeReadable } from 'stream'
|
||||||
|
|
||||||
|
import { UploadFileLike } from '@mtcute/core'
|
||||||
|
|
||||||
|
export async function normalizeFile(file: UploadFileLike) {
|
||||||
|
if (typeof file === 'string') {
|
||||||
|
file = Bun.file(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// while these are not Bun-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>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// string -> ReadStream, thus already handled
|
||||||
|
return null
|
||||||
|
}
|
126
packages/bun/src/utils/tcp.ts
Normal file
126
packages/bun/src/utils/tcp.ts
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import { Socket } from 'bun'
|
||||||
|
import EventEmitter from 'events'
|
||||||
|
|
||||||
|
import { IntermediatePacketCodec, IPacketCodec, ITelegramTransport, MtcuteError, TransportState } from '@mtcute/core'
|
||||||
|
import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: Socket | 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)
|
||||||
|
|
||||||
|
Bun.connect({
|
||||||
|
hostname: dc.ipAddress,
|
||||||
|
port: dc.port,
|
||||||
|
socket: {
|
||||||
|
open: this.handleConnect.bind(this),
|
||||||
|
error: this.handleError.bind(this),
|
||||||
|
data: (socket, data) => this._packetCodec.feed(data),
|
||||||
|
close: this.close.bind(this),
|
||||||
|
},
|
||||||
|
}).catch((err) => {
|
||||||
|
this.handleError(null, err as Error)
|
||||||
|
this.close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
if (this._state === TransportState.Idle) return
|
||||||
|
this.log.info('connection closed')
|
||||||
|
|
||||||
|
this.emit('close')
|
||||||
|
this._state = TransportState.Idle
|
||||||
|
this._socket?.end()
|
||||||
|
this._socket = null
|
||||||
|
this._currentDc = null
|
||||||
|
this._packetCodec.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(socket: unknown, error: Error): void {
|
||||||
|
this.log.error('error: %s', error.stack)
|
||||||
|
this.emit('error', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnect(socket: Socket): void {
|
||||||
|
this._socket = socket
|
||||||
|
this.log.info('connected')
|
||||||
|
|
||||||
|
Promise.resolve(this._packetCodec.tag())
|
||||||
|
.then((initialMessage) => {
|
||||||
|
if (initialMessage.length) {
|
||||||
|
this._socket!.write(initialMessage)
|
||||||
|
this._state = TransportState.Ready
|
||||||
|
this.emit('ready')
|
||||||
|
} else {
|
||||||
|
this._state = TransportState.Ready
|
||||||
|
this.emit('ready')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => this.emit('error', err))
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
this._socket!.write(framed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TcpTransport extends BaseTcpTransport {
|
||||||
|
_packetCodec = new IntermediatePacketCodec()
|
||||||
|
}
|
67
packages/bun/src/worker.ts
Normal file
67
packages/bun/src/worker.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { parentPort, Worker } from 'worker_threads'
|
||||||
|
|
||||||
|
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 { NodePlatform } from './common-internals-node/platform.js'
|
||||||
|
|
||||||
|
export type { TelegramWorkerOptions, TelegramWorkerPortOptions, WorkerCustomMethods }
|
||||||
|
|
||||||
|
let _registered = false
|
||||||
|
|
||||||
|
export class TelegramWorker<T extends WorkerCustomMethods> extends TelegramWorkerBase<T> {
|
||||||
|
registerWorker(handler: WorkerMessageHandler): RespondFn {
|
||||||
|
if (!parentPort) {
|
||||||
|
throw new Error('TelegramWorker must be created from a worker thread')
|
||||||
|
}
|
||||||
|
if (_registered) {
|
||||||
|
throw new Error('TelegramWorker must be created only once')
|
||||||
|
}
|
||||||
|
|
||||||
|
_registered = true
|
||||||
|
|
||||||
|
const port = parentPort
|
||||||
|
|
||||||
|
const respond: RespondFn = port.postMessage.bind(port)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
parentPort.on('message', (message) => handler(message, respond))
|
||||||
|
|
||||||
|
return respond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TelegramWorkerPort<T extends WorkerCustomMethods> extends TelegramWorkerPortBase<T> {
|
||||||
|
constructor(readonly options: TelegramWorkerPortOptions) {
|
||||||
|
setPlatform(new NodePlatform())
|
||||||
|
super(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] {
|
||||||
|
if (!(worker instanceof Worker)) {
|
||||||
|
throw new Error('Only worker_threads are supported')
|
||||||
|
}
|
||||||
|
|
||||||
|
const send: SendFn = worker.postMessage.bind(worker)
|
||||||
|
|
||||||
|
worker.on('message', handler)
|
||||||
|
|
||||||
|
return [
|
||||||
|
send,
|
||||||
|
() => {
|
||||||
|
worker.off('message', handler)
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
20
packages/bun/tsconfig.json
Normal file
20
packages/bun/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"types": [
|
||||||
|
"bun-types",
|
||||||
|
"vite/client"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src",
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../core" },
|
||||||
|
{ "path": "../dispatcher" },
|
||||||
|
{ "path": "../html-parser" },
|
||||||
|
{ "path": "../markdown-parser" }
|
||||||
|
]
|
||||||
|
}
|
10
packages/bun/typedoc.cjs
Normal file
10
packages/bun/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/**',
|
||||||
|
],
|
||||||
|
}
|
|
@ -25,6 +25,8 @@ const MAX_PART_COUNT_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB
|
||||||
// platform-specific
|
// platform-specific
|
||||||
const HAS_FILE = typeof File !== 'undefined'
|
const HAS_FILE = typeof File !== 'undefined'
|
||||||
const HAS_RESPONSE = typeof Response !== 'undefined'
|
const HAS_RESPONSE = typeof Response !== 'undefined'
|
||||||
|
const HAS_URL = typeof URL !== 'undefined'
|
||||||
|
const HAS_BLOB = typeof Blob !== 'undefined'
|
||||||
|
|
||||||
// @available=both
|
// @available=both
|
||||||
/**
|
/**
|
||||||
|
@ -129,6 +131,15 @@ export async function uploadFile(
|
||||||
file = file.stream()
|
file = file.stream()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HAS_URL && file instanceof URL) {
|
||||||
|
file = await fetch(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HAS_BLOB && file instanceof Blob) {
|
||||||
|
fileSize = file.size
|
||||||
|
file = file.stream()
|
||||||
|
}
|
||||||
|
|
||||||
if (HAS_RESPONSE && file instanceof Response) {
|
if (HAS_RESPONSE && file instanceof Response) {
|
||||||
const length = parseInt(file.headers.get('content-length') || '0')
|
const length = parseInt(file.headers.get('content-length') || '0')
|
||||||
if (!isNaN(length) && length) fileSize = length
|
if (!isNaN(length) && length) fileSize = length
|
||||||
|
|
|
@ -11,16 +11,20 @@ import { UploadedFile } from './uploaded-file.js'
|
||||||
* Describes types that can be used in {@link TelegramClient.uploadFile}
|
* Describes types that can be used in {@link TelegramClient.uploadFile}
|
||||||
* method. Can be one of:
|
* method. Can be one of:
|
||||||
* - `Uint8Array`/`Buffer`, which will be interpreted as raw file contents
|
* - `Uint8Array`/`Buffer`, which will be interpreted as raw file contents
|
||||||
* - `File` (from the Web API)
|
* - `File`, `Blob` (from the Web API)
|
||||||
* - `string`, which will be interpreted as file path (**non-browser only!**)
|
* - `string`, which will be interpreted as file path (**non-browser only!**)
|
||||||
* - `ReadStream` (for NodeJS, from the `fs` module)
|
* - `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)
|
||||||
|
* - `BunFile` (from `Bun.file()`)
|
||||||
* - `ReadableStream` (Web API readable stream)
|
* - `ReadableStream` (Web API readable stream)
|
||||||
* - `Readable` (NodeJS readable stream)
|
* - `Readable` (Node.js/Bun readable stream)
|
||||||
* - `Response` (from `window.fetch`)
|
* - `Response` (from `window.fetch`)
|
||||||
*/
|
*/
|
||||||
export type UploadFileLike =
|
export type UploadFileLike =
|
||||||
|
| URL
|
||||||
| Uint8Array
|
| Uint8Array
|
||||||
| File
|
| File
|
||||||
|
| Blob
|
||||||
| string
|
| string
|
||||||
| ReadStream
|
| ReadStream
|
||||||
| ReadableStream<Uint8Array>
|
| ReadableStream<Uint8Array>
|
||||||
|
@ -34,14 +38,15 @@ export type UploadFileLike =
|
||||||
* Can be one of:
|
* Can be one of:
|
||||||
* - `Buffer`, which will be interpreted as raw file contents
|
* - `Buffer`, which will be interpreted as raw file contents
|
||||||
* - `File` (from the Web API)
|
* - `File` (from the Web API)
|
||||||
* - `ReadStream` (for NodeJS, from the `fs` module)
|
* - `ReadStream` (for Node.js/Bun, from the `fs` module)
|
||||||
* - `ReadableStream` (from the Web API, base readable stream)
|
* - `ReadableStream` (from the Web API, base readable stream)
|
||||||
* - `Readable` (for NodeJS, base readable stream)
|
* - `Readable` (for Node.js/Bun, base readable stream)
|
||||||
* - {@link UploadedFile} returned from {@link TelegramClient.uploadFile}
|
* - {@link UploadedFile} returned from {@link TelegramClient.uploadFile}
|
||||||
* - `tl.TypeInputFile` and `tl.TypeInputMedia` TL objects
|
* - `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`)
|
* - `string` with a path to a local file prepended with `file:` (non-browser only) (e.g. `file:image.jpg`)
|
||||||
* - `string` with a URL to remote files (e.g. `https://example.com/image.jpg`)
|
* - `string` with a URL to remote files (e.g. `https://example.com/image.jpg`)
|
||||||
* - `string` with TDLib and Bot API compatible File ID.
|
* - `string` with TDLib and Bot API compatible File ID.
|
||||||
|
* - `URL` (from the Web API, will be `fetch()`-ed if needed; `file://` URLs are not available in browsers)
|
||||||
* - `td.RawFullRemoteFileLocation` (parsed File ID)
|
* - `td.RawFullRemoteFileLocation` (parsed File ID)
|
||||||
*/
|
*/
|
||||||
export type InputFileLike =
|
export type InputFileLike =
|
||||||
|
|
|
@ -2,4 +2,5 @@ export * from './driver.js'
|
||||||
export * from './memory/index.js'
|
export * from './memory/index.js'
|
||||||
export * from './provider.js'
|
export * from './provider.js'
|
||||||
export * from './repository/index.js'
|
export * from './repository/index.js'
|
||||||
|
export * from './sqlite/index.js'
|
||||||
export * from './storage.js'
|
export * from './storage.js'
|
||||||
|
|
|
@ -1,26 +1,6 @@
|
||||||
import sqlite3, { Database, Options, Statement } from 'better-sqlite3'
|
import { getPlatform } from '../../platform.js'
|
||||||
|
import { BaseStorageDriver } from '../driver.js'
|
||||||
import { BaseStorageDriver } from '@mtcute/core'
|
import { ISqliteDatabase, ISqliteStatement } from './types.js'
|
||||||
import { getPlatform } from '@mtcute/core/platform.js'
|
|
||||||
|
|
||||||
export interface SqliteStorageDriverOptions {
|
|
||||||
/**
|
|
||||||
* By default, WAL mode is enabled, which
|
|
||||||
* significantly improves performance.
|
|
||||||
* [Learn more](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/performance.md)
|
|
||||||
*
|
|
||||||
* However, you might encounter some issues,
|
|
||||||
* and if you do, you can disable WAL by passing `true`
|
|
||||||
*
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
disableWal?: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Additional options to pass to `better-sqlite3`
|
|
||||||
*/
|
|
||||||
options?: Options
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIGRATIONS_TABLE_NAME = 'mtcute_migrations'
|
const MIGRATIONS_TABLE_NAME = 'mtcute_migrations'
|
||||||
const MIGRATIONS_TABLE_SQL = `
|
const MIGRATIONS_TABLE_SQL = `
|
||||||
|
@ -30,26 +10,19 @@ create table if not exists ${MIGRATIONS_TABLE_NAME} (
|
||||||
);
|
);
|
||||||
`.trim()
|
`.trim()
|
||||||
|
|
||||||
type MigrationFunction = (db: Database) => void
|
type MigrationFunction = (db: ISqliteDatabase) => void
|
||||||
|
|
||||||
export class SqliteStorageDriver extends BaseStorageDriver {
|
export abstract class BaseSqliteStorageDriver extends BaseStorageDriver {
|
||||||
db!: Database
|
db!: ISqliteDatabase
|
||||||
|
|
||||||
constructor(
|
private _pending: [ISqliteStatement, unknown[]][] = []
|
||||||
readonly filename = ':memory:',
|
private _runMany!: (stmts: [ISqliteStatement, unknown[]][]) => void
|
||||||
readonly params?: SqliteStorageDriverOptions,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
private _pending: [Statement, unknown[]][] = []
|
|
||||||
private _runMany!: (stmts: [Statement, unknown[]][]) => void
|
|
||||||
private _cleanup?: () => void
|
private _cleanup?: () => void
|
||||||
|
|
||||||
private _migrations: Map<string, Map<number, MigrationFunction>> = new Map()
|
private _migrations: Map<string, Map<number, MigrationFunction>> = new Map()
|
||||||
private _maxVersion: Map<string, number> = new Map()
|
private _maxVersion: Map<string, number> = new Map()
|
||||||
|
|
||||||
// todo: remove in 1.0.0 + remove direct dep on @mtcute/tl
|
// todo: remove in 1.0.0
|
||||||
private _legacyMigrations: Map<string, MigrationFunction> = new Map()
|
private _legacyMigrations: Map<string, MigrationFunction> = new Map()
|
||||||
|
|
||||||
registerLegacyMigration(repo: string, migration: MigrationFunction): void {
|
registerLegacyMigration(repo: string, migration: MigrationFunction): void {
|
||||||
|
@ -85,9 +58,9 @@ export class SqliteStorageDriver extends BaseStorageDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onLoad = new Set<(db: Database) => void>()
|
private _onLoad = new Set<(db: ISqliteDatabase) => void>()
|
||||||
|
|
||||||
onLoad(cb: (db: Database) => void): void {
|
onLoad(cb: (db: ISqliteDatabase) => void): void {
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
cb(this.db)
|
cb(this.db)
|
||||||
} else {
|
} else {
|
||||||
|
@ -95,7 +68,7 @@ export class SqliteStorageDriver extends BaseStorageDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_writeLater(stmt: Statement, params: unknown[]): void {
|
_writeLater(stmt: ISqliteStatement, params: unknown[]): void {
|
||||||
this._pending.push([stmt, params])
|
this._pending.push([stmt, params])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,17 +122,12 @@ export class SqliteStorageDriver extends BaseStorageDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract _createDatabase(): ISqliteDatabase
|
||||||
|
|
||||||
_load(): void {
|
_load(): void {
|
||||||
this.db = sqlite3(this.filename, {
|
this.db = this._createDatabase()
|
||||||
...this.params?.options,
|
|
||||||
verbose: this._log.mgr.level >= 5 ? (this._log.verbose as Options['verbose']) : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!this.params?.disableWal) {
|
this._runMany = this.db.transaction((stmts: [ISqliteStatement, unknown[]][]) => {
|
||||||
this.db.pragma('journal_mode = WAL')
|
|
||||||
}
|
|
||||||
|
|
||||||
this._runMany = this.db.transaction((stmts: [Statement, unknown[]][]) => {
|
|
||||||
stmts.forEach((stmt) => {
|
stmts.forEach((stmt) => {
|
||||||
stmt[0].run(stmt[1])
|
stmt[0].run(stmt[1])
|
||||||
})
|
})
|
19
packages/core/src/storage/sqlite/index.ts
Normal file
19
packages/core/src/storage/sqlite/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { ITelegramStorageProvider } from '../../highlevel/storage/provider.js'
|
||||||
|
import { IMtStorageProvider } from '../provider.js'
|
||||||
|
import { BaseSqliteStorageDriver } from './driver.js'
|
||||||
|
import { SqliteAuthKeysRepository } from './repository/auth-keys.js'
|
||||||
|
import { SqliteKeyValueRepository } from './repository/kv.js'
|
||||||
|
import { SqlitePeersRepository } from './repository/peers.js'
|
||||||
|
import { SqliteRefMessagesRepository } from './repository/ref-messages.js'
|
||||||
|
|
||||||
|
export { BaseSqliteStorageDriver }
|
||||||
|
export * from './types.js'
|
||||||
|
|
||||||
|
export class BaseSqliteStorage implements IMtStorageProvider, ITelegramStorageProvider {
|
||||||
|
constructor(readonly driver: BaseSqliteStorageDriver) {}
|
||||||
|
|
||||||
|
readonly authKeys = new SqliteAuthKeysRepository(this.driver)
|
||||||
|
readonly kv = new SqliteKeyValueRepository(this.driver)
|
||||||
|
readonly refMessages = new SqliteRefMessagesRepository(this.driver)
|
||||||
|
readonly peers = new SqlitePeersRepository(this.driver)
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
import { Statement } from 'better-sqlite3'
|
import { IAuthKeysRepository } from '../../repository/auth-keys.js'
|
||||||
|
import { BaseSqliteStorageDriver } from '../driver.js'
|
||||||
import { IAuthKeysRepository } from '@mtcute/core'
|
import { ISqliteStatement } from '../types.js'
|
||||||
|
|
||||||
import { SqliteStorageDriver } from '../driver.js'
|
|
||||||
|
|
||||||
interface AuthKeyDto {
|
interface AuthKeyDto {
|
||||||
dc: number
|
dc: number
|
||||||
|
@ -15,7 +13,7 @@ interface TempAuthKeyDto extends AuthKeyDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('auth_keys', 1, (db) => {
|
_driver.registerMigration('auth_keys', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table if not exists auth_keys (
|
create table if not exists auth_keys (
|
||||||
|
@ -47,8 +45,8 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _set!: Statement
|
private _set!: ISqliteStatement
|
||||||
private _del!: Statement
|
private _del!: ISqliteStatement
|
||||||
set(dc: number, key: Uint8Array | null): void {
|
set(dc: number, key: Uint8Array | null): void {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
this._del.run(dc)
|
this._del.run(dc)
|
||||||
|
@ -59,7 +57,7 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
this._set.run(dc, key)
|
this._set.run(dc, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _get!: Statement
|
private _get!: ISqliteStatement
|
||||||
get(dc: number): Uint8Array | null {
|
get(dc: number): Uint8Array | null {
|
||||||
const row = this._get.get(dc)
|
const row = this._get.get(dc)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -67,8 +65,8 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
return (row as AuthKeyDto).key
|
return (row as AuthKeyDto).key
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setTemp!: Statement
|
private _setTemp!: ISqliteStatement
|
||||||
private _delTemp!: Statement
|
private _delTemp!: ISqliteStatement
|
||||||
setTemp(dc: number, idx: number, key: Uint8Array | null, expires: number): void {
|
setTemp(dc: number, idx: number, key: Uint8Array | null, expires: number): void {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
this._delTemp.run(dc, idx)
|
this._delTemp.run(dc, idx)
|
||||||
|
@ -79,7 +77,7 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
this._setTemp.run(dc, idx, key, expires)
|
this._setTemp.run(dc, idx, key, expires)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getTemp!: Statement
|
private _getTemp!: ISqliteStatement
|
||||||
getTemp(dc: number, idx: number, now: number): Uint8Array | null {
|
getTemp(dc: number, idx: number, now: number): Uint8Array | null {
|
||||||
const row = this._getTemp.get(dc, idx, now)
|
const row = this._getTemp.get(dc, idx, now)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -87,13 +85,13 @@ export class SqliteAuthKeysRepository implements IAuthKeysRepository {
|
||||||
return (row as TempAuthKeyDto).key
|
return (row as TempAuthKeyDto).key
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delTempAll!: Statement
|
private _delTempAll!: ISqliteStatement
|
||||||
deleteByDc(dc: number): void {
|
deleteByDc(dc: number): void {
|
||||||
this._del.run(dc)
|
this._del.run(dc)
|
||||||
this._delTempAll.run(dc)
|
this._delTempAll.run(dc)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delAll!: Statement
|
private _delAll!: ISqliteStatement
|
||||||
deleteAll(): void {
|
deleteAll(): void {
|
||||||
this._delAll.run()
|
this._delAll.run()
|
||||||
}
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
import { Statement } from 'better-sqlite3'
|
|
||||||
|
|
||||||
import { IKeyValueRepository } from '@mtcute/core'
|
|
||||||
import { CurrentUserService, DefaultDcsService, ServiceOptions, UpdatesStateService } from '@mtcute/core/utils.js'
|
|
||||||
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
|
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
|
||||||
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
|
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
|
||||||
|
|
||||||
import { SqliteStorageDriver } from '../driver.js'
|
import { CurrentUserService } from '../../../highlevel/storage/service/current-user.js'
|
||||||
|
import { UpdatesStateService } from '../../../highlevel/storage/service/updates.js'
|
||||||
|
import { IKeyValueRepository } from '../../repository/key-value.js'
|
||||||
|
import { ServiceOptions } from '../../service/base.js'
|
||||||
|
import { DefaultDcsService } from '../../service/default-dcs.js'
|
||||||
|
import { BaseSqliteStorageDriver } from '../driver.js'
|
||||||
|
import { ISqliteStatement } from '../types.js'
|
||||||
|
|
||||||
interface KeyValueDto {
|
interface KeyValueDto {
|
||||||
key: string
|
key: string
|
||||||
|
@ -13,7 +15,7 @@ interface KeyValueDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqliteKeyValueRepository implements IKeyValueRepository {
|
export class SqliteKeyValueRepository implements IKeyValueRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('kv', 1, (db) => {
|
_driver.registerMigration('kv', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table key_value (
|
create table key_value (
|
||||||
|
@ -88,12 +90,12 @@ export class SqliteKeyValueRepository implements IKeyValueRepository {
|
||||||
/* eslint-enable @typescript-eslint/no-unsafe-argument */
|
/* eslint-enable @typescript-eslint/no-unsafe-argument */
|
||||||
}
|
}
|
||||||
|
|
||||||
private _set!: Statement
|
private _set!: ISqliteStatement
|
||||||
set(key: string, value: Uint8Array): void {
|
set(key: string, value: Uint8Array): void {
|
||||||
this._driver._writeLater(this._set, [key, value])
|
this._driver._writeLater(this._set, [key, value])
|
||||||
}
|
}
|
||||||
|
|
||||||
private _get!: Statement
|
private _get!: ISqliteStatement
|
||||||
get(key: string): Uint8Array | null {
|
get(key: string): Uint8Array | null {
|
||||||
const res = this._get.get(key)
|
const res = this._get.get(key)
|
||||||
if (!res) return null
|
if (!res) return null
|
||||||
|
@ -101,12 +103,12 @@ export class SqliteKeyValueRepository implements IKeyValueRepository {
|
||||||
return (res as KeyValueDto).value
|
return (res as KeyValueDto).value
|
||||||
}
|
}
|
||||||
|
|
||||||
private _del!: Statement
|
private _del!: ISqliteStatement
|
||||||
delete(key: string): void {
|
delete(key: string): void {
|
||||||
this._del.run(key)
|
this._del.run(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delAll!: Statement
|
private _delAll!: ISqliteStatement
|
||||||
deleteAll(): void {
|
deleteAll(): void {
|
||||||
this._delAll.run()
|
this._delAll.run()
|
||||||
}
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
import { Statement } from 'better-sqlite3'
|
import { IPeersRepository } from '../../../highlevel/storage/repository/peers.js'
|
||||||
|
import { BaseSqliteStorageDriver } from '../driver.js'
|
||||||
import { IPeersRepository } from '@mtcute/core'
|
import { ISqliteStatement } from '../types.js'
|
||||||
|
|
||||||
import { SqliteStorageDriver } from '../driver.js'
|
|
||||||
|
|
||||||
interface PeerDto {
|
interface PeerDto {
|
||||||
id: number
|
id: number
|
||||||
|
@ -26,7 +24,7 @@ function mapPeerDto(dto: PeerDto): IPeersRepository.PeerInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqlitePeersRepository implements IPeersRepository {
|
export class SqlitePeersRepository implements IPeersRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('peers', 1, (db) => {
|
_driver.registerMigration('peers', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table peers (
|
create table peers (
|
||||||
|
@ -60,7 +58,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _store!: Statement
|
private _store!: ISqliteStatement
|
||||||
store(peer: IPeersRepository.PeerInfo): void {
|
store(peer: IPeersRepository.PeerInfo): void {
|
||||||
this._driver._writeLater(this._store, [
|
this._driver._writeLater(this._store, [
|
||||||
peer.id,
|
peer.id,
|
||||||
|
@ -73,7 +71,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getById!: Statement
|
private _getById!: ISqliteStatement
|
||||||
getById(id: number): IPeersRepository.PeerInfo | null {
|
getById(id: number): IPeersRepository.PeerInfo | null {
|
||||||
const row = this._getById.get(id)
|
const row = this._getById.get(id)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -81,7 +79,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
return mapPeerDto(row as PeerDto)
|
return mapPeerDto(row as PeerDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getByUsername!: Statement
|
private _getByUsername!: ISqliteStatement
|
||||||
getByUsername(username: string): IPeersRepository.PeerInfo | null {
|
getByUsername(username: string): IPeersRepository.PeerInfo | null {
|
||||||
const row = this._getByUsername.get(username)
|
const row = this._getByUsername.get(username)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -89,7 +87,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
return mapPeerDto(row as PeerDto)
|
return mapPeerDto(row as PeerDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getByPhone!: Statement
|
private _getByPhone!: ISqliteStatement
|
||||||
getByPhone(phone: string): IPeersRepository.PeerInfo | null {
|
getByPhone(phone: string): IPeersRepository.PeerInfo | null {
|
||||||
const row = this._getByPhone.get(phone)
|
const row = this._getByPhone.get(phone)
|
||||||
if (!row) return null
|
if (!row) return null
|
||||||
|
@ -97,7 +95,7 @@ export class SqlitePeersRepository implements IPeersRepository {
|
||||||
return mapPeerDto(row as PeerDto)
|
return mapPeerDto(row as PeerDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delAll!: Statement
|
private _delAll!: ISqliteStatement
|
||||||
deleteAll(): void {
|
deleteAll(): void {
|
||||||
this._delAll.run()
|
this._delAll.run()
|
||||||
}
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
import { Statement } from 'better-sqlite3'
|
|
||||||
|
|
||||||
import { IReferenceMessagesRepository } from '@mtcute/core'
|
import { IReferenceMessagesRepository } from '@mtcute/core'
|
||||||
|
|
||||||
import { SqliteStorageDriver } from '../driver.js'
|
import { BaseSqliteStorageDriver } from '../driver.js'
|
||||||
|
import { ISqliteStatement } from '../types.js'
|
||||||
|
|
||||||
interface ReferenceMessageDto {
|
interface ReferenceMessageDto {
|
||||||
peer_id: number
|
peer_id: number
|
||||||
|
@ -11,7 +10,7 @@ interface ReferenceMessageDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqliteRefMessagesRepository implements IReferenceMessagesRepository {
|
export class SqliteRefMessagesRepository implements IReferenceMessagesRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('ref_messages', 1, (db) => {
|
_driver.registerMigration('ref_messages', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table if not exists message_refs (
|
create table if not exists message_refs (
|
||||||
|
@ -36,12 +35,12 @@ export class SqliteRefMessagesRepository implements IReferenceMessagesRepository
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _store!: Statement
|
private _store!: ISqliteStatement
|
||||||
store(peerId: number, chatId: number, msgId: number): void {
|
store(peerId: number, chatId: number, msgId: number): void {
|
||||||
this._store.run(peerId, chatId, msgId)
|
this._store.run(peerId, chatId, msgId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getByPeer!: Statement
|
private _getByPeer!: ISqliteStatement
|
||||||
getByPeer(peerId: number): [number, number] | null {
|
getByPeer(peerId: number): [number, number] | null {
|
||||||
const res = this._getByPeer.get(peerId)
|
const res = this._getByPeer.get(peerId)
|
||||||
if (!res) return null
|
if (!res) return null
|
||||||
|
@ -51,19 +50,19 @@ export class SqliteRefMessagesRepository implements IReferenceMessagesRepository
|
||||||
return [res_.chat_id, res_.msg_id]
|
return [res_.chat_id, res_.msg_id]
|
||||||
}
|
}
|
||||||
|
|
||||||
private _del!: Statement
|
private _del!: ISqliteStatement
|
||||||
delete(chatId: number, msgIds: number[]): void {
|
delete(chatId: number, msgIds: number[]): void {
|
||||||
for (const msgId of msgIds) {
|
for (const msgId of msgIds) {
|
||||||
this._driver._writeLater(this._del, [chatId, msgId])
|
this._driver._writeLater(this._del, [chatId, msgId])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delByPeer!: Statement
|
private _delByPeer!: ISqliteStatement
|
||||||
deleteByPeer(peerId: number): void {
|
deleteByPeer(peerId: number): void {
|
||||||
this._delByPeer.run(peerId)
|
this._delByPeer.run(peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delAll!: Statement
|
private _delAll!: ISqliteStatement
|
||||||
deleteAll(): void {
|
deleteAll(): void {
|
||||||
this._delAll.run()
|
this._delAll.run()
|
||||||
}
|
}
|
22
packages/core/src/storage/sqlite/types.ts
Normal file
22
packages/core/src/storage/sqlite/types.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* An abstract interface for a SQLite database.
|
||||||
|
*
|
||||||
|
* Roughly based on `better-sqlite3`'s `Database` class,
|
||||||
|
* (which can be used as-is), but only with the methods
|
||||||
|
* that are used by mtcute.
|
||||||
|
*/
|
||||||
|
export interface ISqliteDatabase {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
transaction<F extends (...args: any[]) => any>(fn: F): F
|
||||||
|
|
||||||
|
prepare<BindParameters extends unknown[]>(sql: string): ISqliteStatement<BindParameters>
|
||||||
|
|
||||||
|
exec(sql: string): void
|
||||||
|
close(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISqliteStatement<BindParameters extends unknown[] = unknown[]> {
|
||||||
|
run(...params: BindParameters): void
|
||||||
|
get(...params: BindParameters): unknown
|
||||||
|
all(...params: BindParameters): unknown[]
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
# @mtcute/create-bot
|
# @mtcute/create-bot
|
||||||
|
|
||||||
Starter kit for creating bots using `@mtcute/node`.
|
Starter kit for creating bots using `@mtcute/node` or `@mtcute/bun`.
|
||||||
|
|
||||||
[Learn more](https://mtcute.dev/guide/)
|
[Learn more](https://mtcute.dev/guide/)
|
||||||
|
|
||||||
|
@ -12,7 +12,15 @@ Starter kit for creating bots using `@mtcute/node`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
Depending on your preferred package manager, run one of the following commands:
|
||||||
```bash
|
```bash
|
||||||
pnpm create @mtcute/bot
|
pnpm create @mtcute/bot
|
||||||
# and follow the instructions
|
# or
|
||||||
|
yarn create @mtcute/bot
|
||||||
|
# or
|
||||||
|
npm create @mtcute/bot
|
||||||
|
# or
|
||||||
|
bun create @mtcute/bot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
and follow the instructions
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { readConfig, UserConfigPersisted, writeConfig } from './config.js'
|
||||||
import { TELEGRAM_APPS_PAGE } from './constants.js'
|
import { TELEGRAM_APPS_PAGE } from './constants.js'
|
||||||
import { getFeatureChoices } from './features/cli.js'
|
import { getFeatureChoices } from './features/cli.js'
|
||||||
import { MtcuteFeature } from './features/types.js'
|
import { MtcuteFeature } from './features/types.js'
|
||||||
import { getPackageManager, PackageManager } from './package-manager.js'
|
import { PackageManager } from './package-manager.js'
|
||||||
|
|
||||||
interface UserConfigAnswers {
|
interface UserConfigAnswers {
|
||||||
reuse?: boolean
|
reuse?: boolean
|
||||||
|
@ -101,7 +101,7 @@ export async function askForConfigPersisted(): Promise<UserConfigPersisted> {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function askForConfig(): Promise<UserConfig> {
|
export async function askForConfig(packageManager: PackageManager): Promise<UserConfig> {
|
||||||
const persisted = await askForConfigPersisted()
|
const persisted = await askForConfigPersisted()
|
||||||
|
|
||||||
let allowEmptyBotToken = false
|
let allowEmptyBotToken = false
|
||||||
|
@ -128,7 +128,7 @@ export async function askForConfig(): Promise<UserConfig> {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
choices: getFeatureChoices(),
|
choices: getFeatureChoices(packageManager),
|
||||||
name: 'features',
|
name: 'features',
|
||||||
message: 'Select features:',
|
message: 'Select features:',
|
||||||
},
|
},
|
||||||
|
@ -137,7 +137,7 @@ export async function askForConfig(): Promise<UserConfig> {
|
||||||
return {
|
return {
|
||||||
...persisted,
|
...persisted,
|
||||||
name: '', // will be filled later
|
name: '', // will be filled later
|
||||||
packageManager: getPackageManager(),
|
packageManager,
|
||||||
botToken: botToken || undefined,
|
botToken: botToken || undefined,
|
||||||
features,
|
features,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
import { UserConfig } from './cli.js'
|
import { UserConfig } from './cli.js'
|
||||||
import { MtcuteFeature } from './features/types.js'
|
import { MtcuteFeature } from './features/types.js'
|
||||||
import { getInstallCommand } from './package-manager.js'
|
import { getInstallCommand, PackageManager } from './package-manager.js'
|
||||||
import { exec } from './utils.js'
|
import { exec } from './utils.js'
|
||||||
|
|
||||||
export function buildDependenciesList(config: UserConfig) {
|
export function buildDependenciesList(config: UserConfig) {
|
||||||
const dependencies = ['@mtcute/node']
|
const dependencies = []
|
||||||
const devDepdenencies = ['dotenv-cli']
|
const devDepdenencies = ['dotenv-cli']
|
||||||
|
|
||||||
|
if (config.packageManager === PackageManager.Bun) {
|
||||||
|
dependencies.push('@mtcute/bun')
|
||||||
|
} else {
|
||||||
|
dependencies.push('@mtcute/node')
|
||||||
|
}
|
||||||
|
|
||||||
if (config.features.includes(MtcuteFeature.Dispatcher)) {
|
if (config.features.includes(MtcuteFeature.Dispatcher)) {
|
||||||
dependencies.push('@mtcute/dispatcher')
|
dependencies.push('@mtcute/dispatcher')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
import { CheckboxChoiceOptions } from 'inquirer'
|
import { CheckboxChoiceOptions } from 'inquirer'
|
||||||
|
|
||||||
|
import { PackageManager } from '../package-manager.js'
|
||||||
import { MtcuteFeature } from './types.js'
|
import { MtcuteFeature } from './types.js'
|
||||||
|
|
||||||
export function getFeatureChoices(): CheckboxChoiceOptions[] {
|
export function getFeatureChoices(packageMananger: PackageManager): CheckboxChoiceOptions[] {
|
||||||
return [
|
const arr: CheckboxChoiceOptions[] = [
|
||||||
{
|
|
||||||
name: ' 🚀 Native addon (better performance)',
|
|
||||||
short: 'Native addon',
|
|
||||||
value: MtcuteFeature.NativeAddon,
|
|
||||||
checked: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: ' 🌐 Internationalization',
|
name: ' 🌐 Internationalization',
|
||||||
short: 'i18n',
|
short: 'i18n',
|
||||||
|
@ -21,12 +16,6 @@ export function getFeatureChoices(): CheckboxChoiceOptions[] {
|
||||||
value: MtcuteFeature.Dispatcher,
|
value: MtcuteFeature.Dispatcher,
|
||||||
checked: true,
|
checked: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: ' 🐳 Generate Dockerfile',
|
|
||||||
short: 'Dockerfile',
|
|
||||||
value: MtcuteFeature.Docker,
|
|
||||||
checked: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: ' ✨ Use TypeScript',
|
name: ' ✨ Use TypeScript',
|
||||||
short: 'TypeScript',
|
short: 'TypeScript',
|
||||||
|
@ -40,4 +29,25 @@ export function getFeatureChoices(): CheckboxChoiceOptions[] {
|
||||||
checked: true,
|
checked: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (packageMananger !== PackageManager.Bun) {
|
||||||
|
arr.unshift({
|
||||||
|
name: ' 🚀 Native addon (better performance)',
|
||||||
|
short: 'Native addon',
|
||||||
|
value: MtcuteFeature.NativeAddon,
|
||||||
|
checked: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageMananger === PackageManager.Pnpm) {
|
||||||
|
// todo: add support for dockerfile generation for other package managers
|
||||||
|
arr.push({
|
||||||
|
name: ' 🐳 Generate Dockerfile',
|
||||||
|
short: 'Dockerfile',
|
||||||
|
value: MtcuteFeature.Docker,
|
||||||
|
checked: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'
|
||||||
import { askForConfig } from './cli.js'
|
import { askForConfig } from './cli.js'
|
||||||
import { installDependencies } from './dependencies.js'
|
import { installDependencies } from './dependencies.js'
|
||||||
import { MtcuteFeature } from './features/types.js'
|
import { MtcuteFeature } from './features/types.js'
|
||||||
import { getExecCommand } from './package-manager.js'
|
import { getExecCommand, getPackageManager, PackageManager } from './package-manager.js'
|
||||||
import { runTemplater } from './templater.js'
|
import { runTemplater } from './templater.js'
|
||||||
import { exec } from './utils.js'
|
import { exec } from './utils.js'
|
||||||
|
|
||||||
|
@ -17,7 +17,13 @@ if (!projectName) {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await askForConfig()
|
const packageManager = getPackageManager()
|
||||||
|
|
||||||
|
if (packageManager === PackageManager.Bun) {
|
||||||
|
console.log(`${colors.red('‼️ Warning:')} ${colors.yellow('Bun')} support is ${colors.bold('experimental')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await askForConfig(packageManager)
|
||||||
config.name = projectName
|
config.name = projectName
|
||||||
const outDir = process.env.TARGET_DIR || join(process.cwd(), projectName)
|
const outDir = process.env.TARGET_DIR || join(process.cwd(), projectName)
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,19 @@
|
||||||
"lint:fix": "eslint --fix .",
|
"lint:fix": "eslint --fix .",
|
||||||
"format": "prettier --write \"src/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\"",
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq packageManager "bun")}}
|
||||||
|
{{#if features.typescript}}
|
||||||
|
"start": "bun ./src/index.ts"
|
||||||
|
{{else}}
|
||||||
|
"start": "bun ./src/index.js"
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
{{#if features.typescript}}
|
{{#if features.typescript}}
|
||||||
"start": "tsc && dotenv node ./dist/index.js",
|
"start": "tsc && dotenv node ./dist/index.js",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
{{else}}
|
{{else}}
|
||||||
"start": "dotenv node ./src/index.js"
|
"start": "dotenv node ./src/index.js"
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,11 @@
|
||||||
{{#if features.dispatcher}}
|
{{#if features.dispatcher}}
|
||||||
import { Dispatcher, filters } from '@mtcute/dispatcher'
|
import { Dispatcher, filters } from '@mtcute/dispatcher'
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq packageManager "bun")}}
|
||||||
|
import { TelegramClient } from '@mtcute/bun'
|
||||||
|
{{else}}
|
||||||
import { TelegramClient } from '@mtcute/node'
|
import { TelegramClient } from '@mtcute/node'
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
import * as env from './env.js'
|
import * as env from './env.js'
|
||||||
{{#if features.i18n}}
|
{{#if features.i18n}}
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
{{#if features.dispatcher}}
|
{{#if features.dispatcher}}
|
||||||
import { Dispatcher, filters } from '@mtcute/dispatcher'
|
import { Dispatcher, filters } from '@mtcute/dispatcher'
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (eq packageManager "bun")}}
|
||||||
|
import { TelegramClient } from '@mtcute/bun'
|
||||||
|
{{else}}
|
||||||
import { TelegramClient } from '@mtcute/node'
|
import { TelegramClient } from '@mtcute/node'
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
import * as env from './env.js'
|
import * as env from './env.js'
|
||||||
{{#if features.i18n}}
|
{{#if features.i18n}}
|
||||||
|
|
|
@ -26,13 +26,5 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mtcute/test": "workspace:^"
|
"@mtcute/test": "workspace:^"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@mtcute/sqlite": "workspace:^"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@mtcute/sqlite": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { MaybePromise } from '@mtcute/core'
|
import { BaseSqliteStorage, BaseSqliteStorageDriver, ISqliteStatement, MaybePromise } from '@mtcute/core'
|
||||||
import type { SqliteStorage, SqliteStorageDriver, Statement } from '@mtcute/sqlite'
|
|
||||||
|
|
||||||
import { IStateStorageProvider } from '../provider.js'
|
import { IStateStorageProvider } from '../provider.js'
|
||||||
import { IStateRepository } from '../repository.js'
|
import { IStateRepository } from '../repository.js'
|
||||||
|
@ -15,7 +14,7 @@ interface RateLimitDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SqliteStateRepository implements IStateRepository {
|
class SqliteStateRepository implements IStateRepository {
|
||||||
constructor(readonly _driver: SqliteStorageDriver) {
|
constructor(readonly _driver: BaseSqliteStorageDriver) {
|
||||||
_driver.registerMigration('state', 1, (db) => {
|
_driver.registerMigration('state', 1, (db) => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
create table fsm_state (
|
create table fsm_state (
|
||||||
|
@ -49,12 +48,12 @@ class SqliteStateRepository implements IStateRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setState!: Statement
|
private _setState!: ISqliteStatement
|
||||||
setState(key: string, state: string, ttl?: number | undefined): MaybePromise<void> {
|
setState(key: string, state: string, ttl?: number | undefined): MaybePromise<void> {
|
||||||
this._setState.run(key, state, ttl ? Date.now() + ttl * 1000 : undefined)
|
this._setState.run(key, state, ttl ? Date.now() + ttl * 1000 : undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getState!: Statement
|
private _getState!: ISqliteStatement
|
||||||
getState(key: string, now: number): MaybePromise<string | null> {
|
getState(key: string, now: number): MaybePromise<string | null> {
|
||||||
const res_ = this._getState.get(key)
|
const res_ = this._getState.get(key)
|
||||||
if (!res_) return null
|
if (!res_) return null
|
||||||
|
@ -69,21 +68,21 @@ class SqliteStateRepository implements IStateRepository {
|
||||||
return res.value
|
return res.value
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deleteState!: Statement
|
private _deleteState!: ISqliteStatement
|
||||||
deleteState(key: string): MaybePromise<void> {
|
deleteState(key: string): MaybePromise<void> {
|
||||||
this._deleteState.run(key)
|
this._deleteState.run(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deleteOldState!: Statement
|
private _deleteOldState!: ISqliteStatement
|
||||||
private _deleteOldRl!: Statement
|
private _deleteOldRl!: ISqliteStatement
|
||||||
vacuum(now: number): MaybePromise<void> {
|
vacuum(now: number): MaybePromise<void> {
|
||||||
this._deleteOldState.run(now)
|
this._deleteOldState.run(now)
|
||||||
this._deleteOldRl.run(now)
|
this._deleteOldRl.run(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setRl!: Statement
|
private _setRl!: ISqliteStatement
|
||||||
private _getRl!: Statement
|
private _getRl!: ISqliteStatement
|
||||||
private _deleteRl!: Statement
|
private _deleteRl!: ISqliteStatement
|
||||||
|
|
||||||
getRateLimit(key: string, now: number, limit: number, window: number): [number, number] {
|
getRateLimit(key: string, now: number, limit: number, window: number): [number, number] {
|
||||||
const val = this._getRl.get(key) as RateLimitDto | undefined
|
const val = this._getRl.get(key) as RateLimitDto | undefined
|
||||||
|
@ -117,9 +116,9 @@ class SqliteStateRepository implements IStateRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SqliteStateStorage implements IStateStorageProvider {
|
export class SqliteStateStorage implements IStateStorageProvider {
|
||||||
constructor(readonly driver: SqliteStorageDriver) {}
|
constructor(readonly driver: BaseSqliteStorageDriver) {}
|
||||||
|
|
||||||
static from(provider: SqliteStorage) {
|
static from(provider: BaseSqliteStorage) {
|
||||||
return new SqliteStateStorage(provider.driver)
|
return new SqliteStateStorage(provider.driver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "@mtcute/node",
|
"name": "@mtcute/node",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"description": "Meta-package for Node JS",
|
"description": "Meta-package for Node.js",
|
||||||
"author": "alina sireneva <alina@tei.su>",
|
"author": "alina sireneva <alina@tei.su>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
@ -31,11 +31,12 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mtcute/core": "workspace:^",
|
"@mtcute/core": "workspace:^",
|
||||||
"@mtcute/wasm": "workspace:^",
|
"@mtcute/wasm": "workspace:^",
|
||||||
"@mtcute/sqlite": "workspace:^",
|
|
||||||
"@mtcute/markdown-parser": "workspace:^",
|
"@mtcute/markdown-parser": "workspace:^",
|
||||||
"@mtcute/html-parser": "workspace:^"
|
"@mtcute/html-parser": "workspace:^",
|
||||||
|
"better-sqlite3": "9.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mtcute/test": "workspace:^"
|
"@mtcute/test": "workspace:^",
|
||||||
|
"@types/better-sqlite3": "7.6.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,11 @@ import {
|
||||||
TelegramClientOptions,
|
TelegramClientOptions,
|
||||||
} from '@mtcute/core/client.js'
|
} from '@mtcute/core/client.js'
|
||||||
import { setPlatform } from '@mtcute/core/platform.js'
|
import { setPlatform } from '@mtcute/core/platform.js'
|
||||||
import { SqliteStorage } from '@mtcute/sqlite'
|
|
||||||
|
|
||||||
|
import { NodePlatform } from './common-internals-node/platform.js'
|
||||||
import { downloadToFile } from './methods/download-file.js'
|
import { downloadToFile } from './methods/download-file.js'
|
||||||
import { downloadAsNodeStream } from './methods/download-node-stream.js'
|
import { downloadAsNodeStream } from './methods/download-node-stream.js'
|
||||||
import { NodePlatform } from './platform.js'
|
import { SqliteStorage } from './sqlite/index.js'
|
||||||
import { NodeCryptoProvider } from './utils/crypto.js'
|
import { NodeCryptoProvider } from './utils/crypto.js'
|
||||||
import { TcpTransport } from './utils/tcp.js'
|
import { TcpTransport } from './utils/tcp.js'
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,13 @@ import * as os from 'os'
|
||||||
|
|
||||||
import { ICorePlatform } from '@mtcute/core/platform.js'
|
import { ICorePlatform } from '@mtcute/core/platform.js'
|
||||||
|
|
||||||
import { beforeExit } from './utils/exit-hook.js'
|
import { normalizeFile } from '../utils/normalize-file.js'
|
||||||
import { defaultLoggingHandler } from './utils/logging.js'
|
import { beforeExit } from './exit-hook.js'
|
||||||
import { normalizeFile } from './utils/normalize-file.js'
|
import { defaultLoggingHandler } from './logging.js'
|
||||||
|
|
||||||
const BUFFER_BASE64_URL_AVAILABLE = typeof Buffer.isEncoding === 'function' && Buffer.isEncoding('base64url')
|
const BUFFER_BASE64_URL_AVAILABLE = typeof Buffer.isEncoding === 'function' && Buffer.isEncoding('base64url')
|
||||||
|
|
||||||
const toBuffer = (buf: Uint8Array): Buffer => Buffer.from(
|
const toBuffer = (buf: Uint8Array): Buffer => Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength)
|
||||||
buf.buffer,
|
|
||||||
buf.byteOffset,
|
|
||||||
buf.byteLength,
|
|
||||||
)
|
|
||||||
|
|
||||||
export class NodePlatform implements ICorePlatform {
|
export class NodePlatform implements ICorePlatform {
|
||||||
// ICorePlatform
|
// ICorePlatform
|
2
packages/node/src/common-internals-node/readme.md
Normal file
2
packages/node/src/common-internals-node/readme.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
this folder is for common code across `@mtcute/node` and `@mtcute/bun`.
|
||||||
|
it is symlinked into `@mtcute/bun`
|
|
@ -1,9 +1,9 @@
|
||||||
export * from './client.js'
|
export * from './client.js'
|
||||||
export * from './platform.js'
|
export * from './common-internals-node/platform.js'
|
||||||
|
export * from './sqlite/index.js'
|
||||||
export * from './utils/crypto.js'
|
export * from './utils/crypto.js'
|
||||||
export * from './utils/tcp.js'
|
export * from './utils/tcp.js'
|
||||||
export * from './worker.js'
|
export * from './worker.js'
|
||||||
export * from '@mtcute/core'
|
export * from '@mtcute/core'
|
||||||
export * from '@mtcute/html-parser'
|
export * from '@mtcute/html-parser'
|
||||||
export * from '@mtcute/markdown-parser'
|
export * from '@mtcute/markdown-parser'
|
||||||
export * from '@mtcute/sqlite'
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/* eslint-disable import/export, simple-import-sort/exports */
|
|
||||||
export * from '@mtcute/core/methods.js'
|
|
||||||
|
|
||||||
export { downloadToFile } from './methods/download-file.js'
|
export { downloadToFile } from './methods/download-file.js'
|
||||||
export { downloadAsNodeStream } from './methods/download-node-stream.js'
|
export { downloadAsNodeStream } from './methods/download-node-stream.js'
|
||||||
|
export * from '@mtcute/core/methods.js'
|
||||||
|
|
44
packages/node/src/sqlite/driver.ts
Normal file
44
packages/node/src/sqlite/driver.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import sqlite3, { Options } from 'better-sqlite3'
|
||||||
|
|
||||||
|
import { BaseSqliteStorageDriver, ISqliteDatabase } from '@mtcute/core'
|
||||||
|
|
||||||
|
export interface SqliteStorageDriverOptions {
|
||||||
|
/**
|
||||||
|
* By default, WAL mode is enabled, which
|
||||||
|
* significantly improves performance.
|
||||||
|
* [Learn more](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/performance.md)
|
||||||
|
*
|
||||||
|
* However, you might encounter some issues,
|
||||||
|
* and if you do, you can disable WAL by passing `true`
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
disableWal?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional options to pass to `better-sqlite3`
|
||||||
|
*/
|
||||||
|
options?: Options
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SqliteStorageDriver extends BaseSqliteStorageDriver {
|
||||||
|
constructor(
|
||||||
|
readonly filename = ':memory:',
|
||||||
|
readonly params?: SqliteStorageDriverOptions,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
_createDatabase(): ISqliteDatabase {
|
||||||
|
const db = sqlite3(this.filename, {
|
||||||
|
...this.params?.options,
|
||||||
|
verbose: this._log.mgr.level >= 5 ? (this._log.verbose as Options['verbose']) : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!this.params?.disableWal) {
|
||||||
|
db.pragma('journal_mode = WAL')
|
||||||
|
}
|
||||||
|
|
||||||
|
return db as ISqliteDatabase
|
||||||
|
}
|
||||||
|
}
|
14
packages/node/src/sqlite/index.ts
Normal file
14
packages/node/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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from '@mtcute/test'
|
} from '@mtcute/test'
|
||||||
|
|
||||||
if (import.meta.env.TEST_ENV === 'node') {
|
if (import.meta.env.TEST_ENV === 'node') {
|
||||||
const { SqliteStorage } = await import('../src/index.js')
|
const { SqliteStorage } = await import('./index.js')
|
||||||
|
|
||||||
describe('SqliteStorage', () => {
|
describe('SqliteStorage', () => {
|
||||||
const storage = new SqliteStorage(':memory:')
|
const storage = new SqliteStorage(':memory:')
|
|
@ -2,7 +2,7 @@ import { describe } from 'vitest'
|
||||||
|
|
||||||
import { testCryptoProvider } from '@mtcute/test'
|
import { testCryptoProvider } from '@mtcute/test'
|
||||||
|
|
||||||
if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') {
|
if (import.meta.env.TEST_ENV === 'node') {
|
||||||
describe('NodeCryptoProvider', async () => {
|
describe('NodeCryptoProvider', async () => {
|
||||||
const { NodeCryptoProvider } = await import('./crypto.js')
|
const { NodeCryptoProvider } = await import('./crypto.js')
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Readable } from 'stream'
|
||||||
|
|
||||||
import { UploadFileLike } from '@mtcute/core'
|
import { UploadFileLike } from '@mtcute/core'
|
||||||
|
|
||||||
import { nodeStreamToWeb } from '../utils/stream-utils.js'
|
import { nodeStreamToWeb } from './stream-utils.js'
|
||||||
|
|
||||||
export async function normalizeFile(file: UploadFileLike) {
|
export async function normalizeFile(file: UploadFileLike) {
|
||||||
if (typeof file === 'string') {
|
if (typeof file === 'string') {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { isNodeVersionAfter } from './version.js'
|
||||||
|
|
||||||
export function nodeStreamToWeb(stream: Readable): ReadableStream<Uint8Array> {
|
export function nodeStreamToWeb(stream: Readable): ReadableStream<Uint8Array> {
|
||||||
if (typeof Readable.toWeb === 'function') {
|
if (typeof Readable.toWeb === 'function') {
|
||||||
return Readable.toWeb(stream)
|
return Readable.toWeb(stream) as unknown as ReadableStream<Uint8Array>
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise, use a silly little adapter
|
// otherwise, use a silly little adapter
|
||||||
|
@ -57,7 +57,10 @@ export function webStreamToNode(stream: ReadableStream<Uint8Array>): Readable {
|
||||||
},
|
},
|
||||||
destroy(error, cb) {
|
destroy(error, cb) {
|
||||||
if (!ended) {
|
if (!ended) {
|
||||||
void reader.cancel(error).catch(() => {}).then(() => {
|
void reader
|
||||||
|
.cancel(error)
|
||||||
|
.catch(() => {})
|
||||||
|
.then(() => {
|
||||||
cb(error)
|
cb(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -68,9 +71,11 @@ export function webStreamToNode(stream: ReadableStream<Uint8Array>): Readable {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
reader.closed.then(() => {
|
reader.closed
|
||||||
|
.then(() => {
|
||||||
ended = true
|
ended = true
|
||||||
}).catch((err) => {
|
})
|
||||||
|
.catch((err) => {
|
||||||
readable.destroy(err as Error)
|
readable.destroy(err as Error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export const NODE_VERSION = typeof process !== 'undefined' && 'node' in process.versions ? process.versions.node : null
|
export const NODE_VERSION = typeof process !== 'undefined' && 'node' in process.versions ? process.versions.node : null
|
||||||
export const NODE_VERSION_TUPLE = NODE_VERSION ? NODE_VERSION.split('.').map(Number) : null
|
export const NODE_VERSION_TUPLE = NODE_VERSION ? /*#__PURE__*/ NODE_VERSION.split('.').map(Number) : null
|
||||||
|
|
||||||
export function isNodeVersionAfter(major: number, minor: number, patch: number): boolean {
|
export function isNodeVersionAfter(major: number, minor: number, patch: number): boolean {
|
||||||
if (!NODE_VERSION_TUPLE) return true // assume non-node environment is always "after"
|
if (!NODE_VERSION_TUPLE) return true // assume non-node environment is always "after"
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
WorkerMessageHandler,
|
WorkerMessageHandler,
|
||||||
} from '@mtcute/core/worker.js'
|
} from '@mtcute/core/worker.js'
|
||||||
|
|
||||||
import { NodePlatform } from './platform.js'
|
import { NodePlatform } from './common-internals-node/platform.js'
|
||||||
|
|
||||||
export type { TelegramWorkerOptions, TelegramWorkerPortOptions, WorkerCustomMethods }
|
export type { TelegramWorkerOptions, TelegramWorkerPortOptions, WorkerCustomMethods }
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../core" },
|
{ "path": "../core" },
|
||||||
{ "path": "../sqlite" },
|
|
||||||
{ "path": "../dispatcher" },
|
{ "path": "../dispatcher" },
|
||||||
{ "path": "../html-parser" },
|
{ "path": "../html-parser" },
|
||||||
{ "path": "../markdown-parser" }
|
{ "path": "../markdown-parser" }
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# @mtcute/sqlite
|
|
||||||
|
|
||||||
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_sqlite.html)
|
|
||||||
|
|
||||||
SQLite backed storage for mtcute, built with `better-sqlite3`
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { SqliteStorage } from '@mtcute/sqlite'
|
|
||||||
|
|
||||||
const tg = new TelegramClient({
|
|
||||||
// ...
|
|
||||||
storage: new SqliteStorage('client.session')
|
|
||||||
})
|
|
||||||
```
|
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@mtcute/sqlite",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.8.0",
|
|
||||||
"description": "SQLite-based storage for mtcute",
|
|
||||||
"author": "alina sireneva <alina@tei.su>",
|
|
||||||
"license": "MIT",
|
|
||||||
"main": "src/index.ts",
|
|
||||||
"type": "module",
|
|
||||||
"sideEffects": false,
|
|
||||||
"distOnlyFields": {
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"import": "./esm/index.js",
|
|
||||||
"require": "./cjs/index.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"docs": "typedoc",
|
|
||||||
"build": "pnpm run -w build-package sqlite"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@mtcute/core": "workspace:^",
|
|
||||||
"@mtcute/tl": "*",
|
|
||||||
"@mtcute/tl-runtime": "workspace:^",
|
|
||||||
"better-sqlite3": "9.2.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@mtcute/test": "workspace:^",
|
|
||||||
"@types/better-sqlite3": "7.6.4"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { IMtStorageProvider, ITelegramStorageProvider } from '@mtcute/core'
|
|
||||||
|
|
||||||
import { SqliteStorageDriver, SqliteStorageDriverOptions } from './driver.js'
|
|
||||||
import { SqliteAuthKeysRepository } from './repository/auth-keys.js'
|
|
||||||
import { SqliteKeyValueRepository } from './repository/kv.js'
|
|
||||||
import { SqlitePeersRepository } from './repository/peers.js'
|
|
||||||
import { SqliteRefMessagesRepository } from './repository/ref-messages.js'
|
|
||||||
|
|
||||||
export { SqliteStorageDriver } from './driver.js'
|
|
||||||
export type { Statement } from 'better-sqlite3'
|
|
||||||
|
|
||||||
export class SqliteStorage implements IMtStorageProvider, ITelegramStorageProvider {
|
|
||||||
constructor(
|
|
||||||
readonly filename = ':memory:',
|
|
||||||
readonly params?: SqliteStorageDriverOptions,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
readonly driver = new SqliteStorageDriver(this.filename, this.params)
|
|
||||||
|
|
||||||
readonly authKeys = new SqliteAuthKeysRepository(this.driver)
|
|
||||||
readonly kv = new SqliteKeyValueRepository(this.driver)
|
|
||||||
readonly refMessages = new SqliteRefMessagesRepository(this.driver)
|
|
||||||
readonly peers = new SqlitePeersRepository(this.driver)
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../../tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"references": [
|
|
||||||
{ "path": "../" }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist/esm",
|
|
||||||
"rootDir": "./src"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"./src"
|
|
||||||
],
|
|
||||||
"references": [
|
|
||||||
{ "path": "../core" }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
extends: ['../../.config/typedoc/config.base.cjs'],
|
|
||||||
entryPoints: ['./src/index.ts'],
|
|
||||||
}
|
|
|
@ -41,7 +41,7 @@ export class TlBinaryReader {
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
readonly objectsMap: TlReaderMap | undefined,
|
readonly objectsMap: TlReaderMap | undefined,
|
||||||
data: ArrayBuffer,
|
data: ArrayBuffer | ArrayBufferView,
|
||||||
start = 0,
|
start = 0,
|
||||||
) {
|
) {
|
||||||
if (ArrayBuffer.isView(data)) {
|
if (ArrayBuffer.isView(data)) {
|
||||||
|
@ -61,7 +61,7 @@ export class TlBinaryReader {
|
||||||
* @param data Buffer to read from
|
* @param data Buffer to read from
|
||||||
* @param start Position to start reading from
|
* @param start Position to start reading from
|
||||||
*/
|
*/
|
||||||
static manual(data: ArrayBuffer, start = 0): TlBinaryReader {
|
static manual(data: ArrayBuffer | ArrayBufferView, start = 0): TlBinaryReader {
|
||||||
return new TlBinaryReader(undefined, data, start)
|
return new TlBinaryReader(undefined, data, start)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,28 @@ importers:
|
||||||
specifier: 1.4.0
|
specifier: 1.4.0
|
||||||
version: 1.4.0(@types/node@20.10.0)(@vitest/browser@1.4.0)(@vitest/ui@1.4.0)
|
version: 1.4.0(@types/node@20.10.0)(@vitest/browser@1.4.0)(@vitest/ui@1.4.0)
|
||||||
|
|
||||||
|
packages/bun:
|
||||||
|
dependencies:
|
||||||
|
'@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
|
||||||
|
devDependencies:
|
||||||
|
'@mtcute/test':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../test
|
||||||
|
bun-types:
|
||||||
|
specifier: 1.0.33
|
||||||
|
version: 1.0.33
|
||||||
|
|
||||||
packages/convert:
|
packages/convert:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mtcute/core':
|
'@mtcute/core':
|
||||||
|
@ -205,9 +227,6 @@ importers:
|
||||||
'@mtcute/core':
|
'@mtcute/core':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../core
|
version: link:../core
|
||||||
'@mtcute/sqlite':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../sqlite
|
|
||||||
events:
|
events:
|
||||||
specifier: 3.2.0
|
specifier: 3.2.0
|
||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
|
@ -284,37 +303,9 @@ importers:
|
||||||
'@mtcute/markdown-parser':
|
'@mtcute/markdown-parser':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../markdown-parser
|
version: link:../markdown-parser
|
||||||
'@mtcute/sqlite':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../sqlite
|
|
||||||
'@mtcute/wasm':
|
'@mtcute/wasm':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../wasm
|
version: link:../wasm
|
||||||
devDependencies:
|
|
||||||
'@mtcute/test':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../test
|
|
||||||
|
|
||||||
packages/socks-proxy:
|
|
||||||
dependencies:
|
|
||||||
'@mtcute/node':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../node
|
|
||||||
ip6:
|
|
||||||
specifier: 0.2.7
|
|
||||||
version: 0.2.7
|
|
||||||
|
|
||||||
packages/sqlite:
|
|
||||||
dependencies:
|
|
||||||
'@mtcute/core':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../core
|
|
||||||
'@mtcute/tl':
|
|
||||||
specifier: '*'
|
|
||||||
version: link:../tl
|
|
||||||
'@mtcute/tl-runtime':
|
|
||||||
specifier: workspace:^
|
|
||||||
version: link:../tl-runtime
|
|
||||||
better-sqlite3:
|
better-sqlite3:
|
||||||
specifier: 9.2.2
|
specifier: 9.2.2
|
||||||
version: 9.2.2
|
version: 9.2.2
|
||||||
|
@ -326,6 +317,15 @@ importers:
|
||||||
specifier: 7.6.4
|
specifier: 7.6.4
|
||||||
version: 7.6.4
|
version: 7.6.4
|
||||||
|
|
||||||
|
packages/socks-proxy:
|
||||||
|
dependencies:
|
||||||
|
'@mtcute/node':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../node
|
||||||
|
ip6:
|
||||||
|
specifier: 0.2.7
|
||||||
|
version: 0.2.7
|
||||||
|
|
||||||
packages/test:
|
packages/test:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mtcute/core':
|
'@mtcute/core':
|
||||||
|
@ -1425,6 +1425,12 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 5.26.5
|
undici-types: 5.26.5
|
||||||
|
|
||||||
|
/@types/node@20.11.30:
|
||||||
|
resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==}
|
||||||
|
dependencies:
|
||||||
|
undici-types: 5.26.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/normalize-package-data@2.4.1:
|
/@types/normalize-package-data@2.4.1:
|
||||||
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
|
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1445,6 +1451,12 @@ packages:
|
||||||
'@types/node': 20.10.0
|
'@types/node': 20.10.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/ws@8.5.10:
|
||||||
|
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.10.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/ws@8.5.4:
|
/@types/ws@8.5.4:
|
||||||
resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==}
|
resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2054,6 +2066,13 @@ packages:
|
||||||
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
|
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/bun-types@1.0.33:
|
||||||
|
resolution: {integrity: sha512-L5tBIf9g6rBBkvshqysi5NoLQ9NnhSPU1pfJ9FzqoSfofYdyac3WLUnOIuQ+M5za/sooVUOP2ko+E6Tco0OLIA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.11.30
|
||||||
|
'@types/ws': 8.5.10
|
||||||
|
dev: true
|
||||||
|
|
||||||
/cac@6.7.14:
|
/cac@6.7.14:
|
||||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
Loading…
Reference in a new issue