feat(gateway): include production Telegram user gateway
Version the NestJS TDLib gateway with Docker deployment, migrations, automated tests and platform integration documentation while excluding all account sessions, credentials, runtime databases and backups.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import * as dotenv from 'dotenv';
|
||||
import * as path from 'path';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||
username: process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
database: process.env.DB_DATABASE || 'telegram_gateway',
|
||||
entities: [path.join(__dirname, 'entities', '*.entity.{ts,js}')],
|
||||
migrations: [path.join(__dirname, 'migrations', '*.{ts,js}')],
|
||||
synchronize: process.env.NODE_ENV === 'development',
|
||||
logging: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export { Tenant } from './tenant.entity';
|
||||
export { User } from './user.entity';
|
||||
export { TgAccount, TgAccountStatus } from './tg-account.entity';
|
||||
export { TgProxy, ProxyType } from './tg-proxy.entity';
|
||||
export { TgAccountProxyBinding } from './tg-account-proxy-binding.entity';
|
||||
export { TgChat, ChatType } from './tg-chat.entity';
|
||||
export { TgUser } from './tg-user.entity';
|
||||
export { TgContact } from './tg-contact.entity';
|
||||
export { TgGroup } from './tg-group.entity';
|
||||
export { TgMessage } from './tg-message.entity';
|
||||
export { TgSendJob, SendJobStatus } from './tg-send-job.entity';
|
||||
export { TgFile, FileType, FileStorageBackend } from './tg-file.entity';
|
||||
export { TgWebhookSubscription } from './tg-webhook-subscription.entity';
|
||||
export { TgWebhookDelivery, DeliveryStatus } from './tg-webhook-delivery.entity';
|
||||
export { TgAuditLog } from './tg-audit-log.entity';
|
||||
export { TgRawUpdate } from './tg-raw-update.entity';
|
||||
export { TgRateLimitEvent } from './tg-rate-limit-event.entity';
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { TgAccount } from './tg-account.entity';
|
||||
|
||||
@Entity('tenants')
|
||||
export class Tenant {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true, length: 100 })
|
||||
name: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
config: Record<string, unknown>;
|
||||
|
||||
@Column({ type: 'int', default: 5 })
|
||||
maxAccounts: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(() => User, (user) => user.tenant)
|
||||
users: User[];
|
||||
|
||||
@OneToMany(() => TgAccount, (account) => account.tenant)
|
||||
tgAccounts: TgAccount[];
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { TgAccount } from './tg-account.entity';
|
||||
import { TgProxy } from './tg-proxy.entity';
|
||||
|
||||
@Entity('tg_account_proxy_bindings')
|
||||
export class TgAccountProxyBinding {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@ManyToOne(() => TgAccount, (account) => account.proxyBindings)
|
||||
@JoinColumn({ name: 'accountId' })
|
||||
account: TgAccount;
|
||||
|
||||
@Column()
|
||||
proxyId: string;
|
||||
|
||||
@ManyToOne(() => TgProxy, (proxy) => proxy.accountBindings)
|
||||
@JoinColumn({ name: 'proxyId' })
|
||||
proxy: TgProxy;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { Tenant } from './tenant.entity';
|
||||
import { TgAccountProxyBinding } from './tg-account-proxy-binding.entity';
|
||||
import { TgChat } from './tg-chat.entity';
|
||||
|
||||
export enum TgAccountStatus {
|
||||
CREATED = 'created',
|
||||
WAITING_TDLIB = 'waiting_tdlib',
|
||||
WAITING_PHONE = 'waiting_phone',
|
||||
WAITING_CODE = 'waiting_code',
|
||||
WAITING_PASSWORD = 'waiting_password',
|
||||
READY = 'ready',
|
||||
DISABLED = 'disabled',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
@Entity('tg_accounts')
|
||||
export class TgAccount {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
tenantId: string;
|
||||
|
||||
@ManyToOne(() => Tenant, (tenant) => tenant.tgAccounts)
|
||||
@JoinColumn({ name: 'tenantId' })
|
||||
tenant: Tenant;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
phoneNumber: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
firstName: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
lastName: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
username: string;
|
||||
|
||||
@Column({ default: 0 })
|
||||
tgUserId: number;
|
||||
|
||||
@Column({
|
||||
type: 'varchar', length: 50,
|
||||
default: TgAccountStatus.CREATED,
|
||||
})
|
||||
status: TgAccountStatus;
|
||||
|
||||
@Column({ nullable: true })
|
||||
tdlibClientId: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
tdlibState: string;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
metadata: Record<string, unknown>;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(() => TgAccountProxyBinding, (binding) => binding.account)
|
||||
proxyBindings: TgAccountProxyBinding[];
|
||||
|
||||
@OneToMany(() => TgChat, (chat) => chat.account)
|
||||
chats: TgChat[];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('tg_audit_logs')
|
||||
export class TgAuditLog {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
tenantId: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
userId: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
accountId: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
action: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
resource: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
resourceId: string;
|
||||
|
||||
@Column({ length: 45, nullable: true })
|
||||
ipAddress: string;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
details: Record<string, unknown>;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
statusCode: number;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
duration: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { TgAccount } from './tg-account.entity';
|
||||
|
||||
export enum ChatType {
|
||||
PRIVATE = 'private',
|
||||
GROUP = 'group',
|
||||
SUPERGROUP = 'supergroup',
|
||||
CHANNEL = 'channel',
|
||||
SECRET = 'secret',
|
||||
}
|
||||
|
||||
@Entity('tg_chats')
|
||||
export class TgChat {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@ManyToOne(() => TgAccount, (account) => account.chats)
|
||||
@JoinColumn({ name: 'accountId' })
|
||||
account: TgAccount;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
tgChatId: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 50 })
|
||||
type: ChatType;
|
||||
|
||||
@Column({ length: 500 })
|
||||
title: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
lastMessage: string;
|
||||
|
||||
@Column({ type: 'bigint', default: 0 })
|
||||
lastMessageId: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
lastMessageDate: Date;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
unreadCount: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true, transformer: { to: (v) => v ? JSON.stringify(v) : null, from: (v) => v ? JSON.parse(v) : null } })
|
||||
photo: Record<string, unknown>;
|
||||
|
||||
@Column({ type: 'text', nullable: true, transformer: { to: (v) => v ? JSON.stringify(v) : null, from: (v) => v ? JSON.parse(v) : null } })
|
||||
raw: Record<string, unknown>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('tg_contacts')
|
||||
export class TgContact {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
tgUserId: number;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
firstName: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
lastName: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
username: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
phoneNumber: string;
|
||||
|
||||
@Column({ default: false })
|
||||
isMutual: boolean;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
raw: Record<string, unknown>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum FileType {
|
||||
PHOTO = 'photo',
|
||||
VIDEO = 'video',
|
||||
DOCUMENT = 'document',
|
||||
AUDIO = 'audio',
|
||||
VOICE = 'voice',
|
||||
STICKER = 'sticker',
|
||||
ANIMATION = 'animation',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
export enum FileStorageBackend {
|
||||
LOCAL = 'local',
|
||||
S3 = 's3',
|
||||
}
|
||||
|
||||
@Entity('tg_files')
|
||||
export class TgFile {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
tgFileId: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, default: FileType.OTHER })
|
||||
type: FileType;
|
||||
|
||||
@Column({ length: 500 })
|
||||
fileName: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
mimeType: string;
|
||||
|
||||
@Column({ type: 'bigint', default: 0 })
|
||||
size: number;
|
||||
|
||||
@Column({
|
||||
type: 'varchar', length: 50,
|
||||
default: FileStorageBackend.LOCAL,
|
||||
})
|
||||
storageBackend: FileStorageBackend;
|
||||
|
||||
@Column({ length: 1000, nullable: true })
|
||||
storagePath: string;
|
||||
|
||||
@Column({ length: 1000, nullable: true })
|
||||
storageUrl: string;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
metadata: Record<string, unknown>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('tg_groups')
|
||||
export class TgGroup {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
tgChatId: number;
|
||||
|
||||
@Column({ length: 500 })
|
||||
title: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
memberCount: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
inviteLink: string;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
photo: Record<string, unknown>;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
permissions: Record<string, unknown>;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
raw: Record<string, unknown>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('tg_messages')
|
||||
export class TgMessage {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
tgChatId: number;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
tgMessageId: number;
|
||||
|
||||
@Column({ type: 'bigint', nullable: true })
|
||||
senderTgUserId: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
text: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true, transformer: { to: (v) => v ? JSON.stringify(v) : null, from: (v) => v ? JSON.parse(v) : null } })
|
||||
content: Record<string, unknown>;
|
||||
|
||||
@Column({ default: false })
|
||||
isOutgoing: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
tgDate: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
editDate: Date;
|
||||
|
||||
@Column({ type: 'text', nullable: true, transformer: { to: (v) => v ? JSON.stringify(v) : null, from: (v) => v ? JSON.parse(v) : null } })
|
||||
raw: Record<string, unknown>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { Tenant } from './tenant.entity';
|
||||
import { TgAccountProxyBinding } from './tg-account-proxy-binding.entity';
|
||||
|
||||
export enum ProxyType {
|
||||
SOCKS5 = 'socks5',
|
||||
HTTP = 'http',
|
||||
MT_PROTO = 'mtproto',
|
||||
}
|
||||
|
||||
@Entity('tg_proxies')
|
||||
export class TgProxy {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
tenantId: string;
|
||||
|
||||
@ManyToOne(() => Tenant)
|
||||
@JoinColumn({ name: 'tenantId' })
|
||||
tenant: Tenant;
|
||||
|
||||
@Column({ length: 255 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50 })
|
||||
type: ProxyType;
|
||||
|
||||
@Column({ length: 255 })
|
||||
host: string;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
port: number;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
username: string;
|
||||
|
||||
@Column({ length: 255, nullable: true, select: false })
|
||||
password: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true, select: false })
|
||||
secret: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(() => TgAccountProxyBinding, (binding) => binding.proxy)
|
||||
accountBindings: TgAccountProxyBinding[];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('tg_rate_limit_events')
|
||||
export class TgRateLimitEvent {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@Column({ length: 50 })
|
||||
action: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
window: string;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
count: number;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
limit: number;
|
||||
|
||||
@Column()
|
||||
blocked: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('tg_raw_updates')
|
||||
export class TgRawUpdate {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@Column({ length: 100 })
|
||||
updateType: string;
|
||||
|
||||
@Column({ type: 'simple-json' })
|
||||
payload: Record<string, unknown>;
|
||||
|
||||
@Column({ default: false })
|
||||
processed: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum SendJobStatus {
|
||||
PENDING = 'pending',
|
||||
PROCESSING = 'processing',
|
||||
COMPLETED = 'completed',
|
||||
FAILED = 'failed',
|
||||
}
|
||||
|
||||
@Entity('tg_send_jobs')
|
||||
@Index(['accountId', 'idempotencyKey'], { unique: true })
|
||||
export class TgSendJob {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
tgChatId: number;
|
||||
|
||||
@Column()
|
||||
bullJobId: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 128, nullable: true })
|
||||
idempotencyKey: string | null;
|
||||
|
||||
@Column({
|
||||
type: 'varchar', length: 50,
|
||||
default: SendJobStatus.PENDING,
|
||||
})
|
||||
status: SendJobStatus;
|
||||
|
||||
@Column({ type: 'simple-json' })
|
||||
payload: Record<string, unknown>;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
result: Record<string, unknown>;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
errorMessage: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
attempt: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('tg_users')
|
||||
export class TgUser {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'bigint', unique: true })
|
||||
tgUserId: number;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
firstName: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
lastName: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
username: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
phoneNumber: string;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
photo: Record<string, unknown>;
|
||||
|
||||
@Column({ default: 'user' })
|
||||
type: string;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
raw: Record<string, unknown>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum DeliveryStatus {
|
||||
PENDING = 'pending',
|
||||
SUCCESS = 'success',
|
||||
FAILED = 'failed',
|
||||
RETRYING = 'retrying',
|
||||
DEAD_LETTER = 'dead_letter',
|
||||
}
|
||||
|
||||
@Entity('tg_webhook_deliveries')
|
||||
export class TgWebhookDelivery {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
subscriptionId: string;
|
||||
|
||||
@Column({ length: 1000 })
|
||||
url: string;
|
||||
|
||||
@Column({ type: 'simple-json' })
|
||||
payload: Record<string, unknown>;
|
||||
|
||||
@Column({
|
||||
type: 'varchar', length: 50,
|
||||
default: DeliveryStatus.PENDING,
|
||||
})
|
||||
status: DeliveryStatus;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
responseCode: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
responseBody: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
attempt: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
errorMessage: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
export type WebhookEvent =
|
||||
| 'message.received'
|
||||
| 'message.sent'
|
||||
| 'message.edited'
|
||||
| 'message.deleted'
|
||||
| 'update.new_chat'
|
||||
| 'update.user_status'
|
||||
| 'update.callback_query'
|
||||
| '*';
|
||||
|
||||
@Entity('tg_webhook_subscriptions')
|
||||
export class TgWebhookSubscription {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
accountId: string;
|
||||
|
||||
@Column({ length: 1000 })
|
||||
url: string;
|
||||
|
||||
@Column({ length: 255, nullable: true, select: false })
|
||||
secret: string;
|
||||
|
||||
@Column({ type: 'simple-array' })
|
||||
events: string[];
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@Column({ type: 'int', default: 8 })
|
||||
maxRetries: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
lastDeliveryAt: Date;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Tenant } from './tenant.entity';
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 255 })
|
||||
email: string;
|
||||
|
||||
@Column({ length: 255, select: false })
|
||||
passwordHash: string;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
name: string;
|
||||
|
||||
@Column({ default: 'user' })
|
||||
role: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@Column()
|
||||
tenantId: string;
|
||||
|
||||
@ManyToOne(() => Tenant, (tenant) => tenant.users)
|
||||
@JoinColumn({ name: 'tenantId' })
|
||||
tenant: Tenant;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class InitialSchema1700000000000 implements MigrationInterface {
|
||||
name = 'InitialSchema1700000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
if (queryRunner.connection.options.type === 'postgres') {
|
||||
await queryRunner.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
||||
}
|
||||
|
||||
// Capture the entity-to-database delta once, then execute it as this
|
||||
// initial migration. This keeps the initial schema aligned with all
|
||||
// registered entities while production still runs with synchronize=false.
|
||||
const schemaBuilder = (
|
||||
queryRunner.connection.driver as unknown as {
|
||||
createSchemaBuilder: () => {
|
||||
log: () => Promise<{
|
||||
upQueries: Array<{ query: string; parameters?: unknown[] }>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
).createSchemaBuilder();
|
||||
const schema = await schemaBuilder.log();
|
||||
for (const query of schema.upQueries) {
|
||||
await queryRunner.query(query.query, query.parameters);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tables = [
|
||||
'tg_webhook_deliveries',
|
||||
'tg_webhook_subscriptions',
|
||||
'tg_rate_limit_events',
|
||||
'tg_raw_updates',
|
||||
'tg_audit_logs',
|
||||
'tg_files',
|
||||
'tg_send_jobs',
|
||||
'tg_messages',
|
||||
'tg_groups',
|
||||
'tg_contacts',
|
||||
'tg_users',
|
||||
'tg_chats',
|
||||
'tg_account_proxy_bindings',
|
||||
'tg_proxies',
|
||||
'tg_accounts',
|
||||
'users',
|
||||
'tenants',
|
||||
];
|
||||
for (const table of tables) {
|
||||
await queryRunner.dropTable(table, true, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn, TableIndex } from 'typeorm';
|
||||
|
||||
export class SendJobIdempotency1700000001000 implements MigrationInterface {
|
||||
name = 'SendJobIdempotency1700000001000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const table = await queryRunner.getTable('tg_send_jobs');
|
||||
if (!table?.findColumnByName('idempotencyKey')) {
|
||||
await queryRunner.addColumn(
|
||||
'tg_send_jobs',
|
||||
new TableColumn({
|
||||
name: 'idempotencyKey',
|
||||
type: 'varchar',
|
||||
length: '128',
|
||||
isNullable: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const refreshed = await queryRunner.getTable('tg_send_jobs');
|
||||
if (!refreshed?.indices.some((index) => index.name === 'IDX_send_job_idempotency')) {
|
||||
await queryRunner.createIndex(
|
||||
'tg_send_jobs',
|
||||
new TableIndex({
|
||||
name: 'IDX_send_job_idempotency',
|
||||
columnNames: ['accountId', 'idempotencyKey'],
|
||||
isUnique: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const table = await queryRunner.getTable('tg_send_jobs');
|
||||
if (table?.indices.some((index) => index.name === 'IDX_send_job_idempotency')) {
|
||||
await queryRunner.dropIndex('tg_send_jobs', 'IDX_send_job_idempotency');
|
||||
}
|
||||
if (table?.findColumnByName('idempotencyKey')) {
|
||||
await queryRunner.dropColumn('tg_send_jobs', 'idempotencyKey');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user