@miguelmorales13/nestkit 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/dist/index.cjs +5 -5
- package/dist/index.js +8 -8
- package/dist/tiktok/index.cjs +142 -0
- package/dist/tiktok/index.d.ts +3 -0
- package/dist/tiktok/index.js +142 -0
- package/dist/tiktok/tiktok.module.d.ts +13 -0
- package/dist/tiktok/tiktok.service.d.ts +75 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -578,6 +578,44 @@ El token se saca en developers.pinterest.com con los permisos `boards:read`,
|
|
|
578
578
|
`pins:read` y `pins:write`. Los títulos se recortan a 100 caracteres y las
|
|
579
579
|
descripciones a 800, que son los límites del API — pasarse devuelve un 400.
|
|
580
580
|
|
|
581
|
+
### `tiktok` — publicar videos, y el modo que evita la auditoría
|
|
582
|
+
|
|
583
|
+
Tres llamadas, ninguna opcional: preguntar por la cuenta, inicializar la
|
|
584
|
+
subida y sondear hasta que TikTok termine de procesar. La primera es un
|
|
585
|
+
requisito de sus guías, no una cortesía: la API rechaza publicar si no se
|
|
586
|
+
consultó antes qué admite la cuenta.
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
import { TikTokModule, TikTokService } from '@miguelmorales13/nestkit/tiktok';
|
|
590
|
+
|
|
591
|
+
@Module({ imports: [TikTokModule.forRoot()] }) // lee TIKTOK_ACCESS_TOKEN
|
|
592
|
+
export class AppModule {}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
await tiktok.creatorInfo(); // obligatorio antes de publicar
|
|
597
|
+
const id = await tiktok.publish({ data, title }); // sin privacyLevel: a borradores
|
|
598
|
+
await tiktok.status(id); // hasta PUBLISH_COMPLETE
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**La diferencia entre los dos modos importa mucho al principio.** En *Direct
|
|
602
|
+
Post* el video sale en vivo, pero exige el permiso `video.publish`, que solo se
|
|
603
|
+
consigue pasando una auditoría de TikTok de varias semanas —y hasta aprobarla
|
|
604
|
+
todo lo publicado queda en `SELF_ONLY`, o sea que no lo ve nadie—. En modo
|
|
605
|
+
**borrador**, que es el de por defecto aquí, el video cae en la bandeja de la
|
|
606
|
+
cuenta y una persona le da publicar desde el teléfono: llega con todo su
|
|
607
|
+
alcance y solo necesita `video.upload`.
|
|
608
|
+
|
|
609
|
+
Dicho de otro modo: publicar en directo antes de la auditoría es peor que no
|
|
610
|
+
publicar, porque gastas la pieza sin que la vea nadie.
|
|
611
|
+
|
|
612
|
+
Sube el binario en vez de pasar una URL a propósito. La otra vía,
|
|
613
|
+
`PULL_FROM_URL`, obliga a verificar el dominio en el portal de TikTok, y
|
|
614
|
+
cambiar un trámite por otro no compensa cuando el archivo ya está en disco.
|
|
615
|
+
|
|
616
|
+
El video tiene que ir en **MP4 H.264**. Límite de **6 peticiones por minuto**
|
|
617
|
+
por token, así que el sondeo del estado va espaciado.
|
|
618
|
+
|
|
581
619
|
### `stripe` — cliente de la SDK oficial
|
|
582
620
|
|
|
583
621
|
```ts
|
|
@@ -1023,6 +1061,7 @@ trae los datos.
|
|
|
1023
1061
|
| `@miguelmorales13/nestkit/meta` | `MetaModule`, `MetaService` — publicar en Instagram y Facebook Pages |
|
|
1024
1062
|
| `@miguelmorales13/nestkit/media` | `MediaModule`, `MediaService` — hosting efímero en memoria |
|
|
1025
1063
|
| `@miguelmorales13/nestkit/pinterest` | `PinterestModule`, `PinterestService` — publicar pines |
|
|
1064
|
+
| `@miguelmorales13/nestkit/tiktok` | `TikTokModule`, `TikTokService` — publicar videos |
|
|
1026
1065
|
| `@miguelmorales13/nestkit/stripe` | `StripeModule`, `STRIPE_CLIENT`, `createStripeWebhookController` |
|
|
1027
1066
|
| `@miguelmorales13/nestkit/auth` | `AuthUserPort`, `BaseAuthService`, `createAuthController`, `TokenService`, `JwtAuthGuard`, `RolesGuard`, `RequireTenantGuard`, `CurrentUser`, `CurrentTenant`, `Roles` |
|
|
1028
1067
|
| `@miguelmorales13/nestkit/auth/oauth` | `OAuthAuthModule`, `OAuthAuthService`, `createOAuthController`, `googleProvider`, `facebookProvider`, `OAuthStorePort`, `RefreshTokenStorePort` |
|
|
@@ -1035,7 +1074,7 @@ trae los datos.
|
|
|
1035
1074
|
|
|
1036
1075
|
## Estado del paquete
|
|
1037
1076
|
|
|
1038
|
-
`0.
|
|
1077
|
+
`0.9.0`. Sin adaptador Mongo real. Sin tests unitarios propios.
|
|
1039
1078
|
|
|
1040
1079
|
`auth/oauth` y `umami` sí tienen un consumidor real: **nutrimx** corre su acceso con Google y
|
|
1041
1080
|
Facebook sobre este módulo en producción. El resto sigue sin ejercitarse de verdad.
|
package/dist/index.cjs
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunkRF75KC63cjs = require('./chunk-RF75KC63.cjs');
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
var
|
|
11
|
+
var _chunkSPTDXUEDcjs = require('./chunk-SPTDXUED.cjs');
|
|
12
12
|
require('./chunk-MR2IFCZE.cjs');
|
|
13
13
|
|
|
14
14
|
|
|
@@ -25,9 +25,6 @@ var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
var _chunk3CLYZC3Tcjs = require('./chunk-3CLYZC3T.cjs');
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
|
|
31
28
|
require('./chunk-7SOM7EZP.cjs');
|
|
32
29
|
|
|
33
30
|
|
|
@@ -47,6 +44,9 @@ var _chunkZA56XBCKcjs = require('./chunk-ZA56XBCK.cjs');
|
|
|
47
44
|
var _chunkR7BVS6CIcjs = require('./chunk-R7BVS6CI.cjs');
|
|
48
45
|
|
|
49
46
|
|
|
47
|
+
var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
|
|
48
|
+
|
|
49
|
+
|
|
50
50
|
|
|
51
51
|
var _chunkM3EL5O6Tcjs = require('./chunk-M3EL5O6T.cjs');
|
|
52
52
|
require('./chunk-2REOCMUD.cjs');
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import "./chunk-EPVKCBPT.js";
|
|
2
|
-
import {
|
|
3
|
-
SUPABASE_ANON_CLIENT,
|
|
4
|
-
SUPABASE_SERVICE_ROLE_CLIENT,
|
|
5
|
-
SupabaseModule
|
|
6
|
-
} from "./chunk-PA24P76K.js";
|
|
7
2
|
import {
|
|
8
3
|
PG_POOL,
|
|
9
4
|
PgModule,
|
|
10
5
|
withTenantScope
|
|
11
6
|
} from "./chunk-VKOPDDCC.js";
|
|
7
|
+
import {
|
|
8
|
+
SUPABASE_ANON_CLIENT,
|
|
9
|
+
SUPABASE_SERVICE_ROLE_CLIENT,
|
|
10
|
+
SupabaseModule
|
|
11
|
+
} from "./chunk-PA24P76K.js";
|
|
12
12
|
import "./chunk-NAK4WDKS.js";
|
|
13
13
|
import {
|
|
14
14
|
REQUEST_ID_HEADER,
|
|
@@ -25,9 +25,6 @@ import {
|
|
|
25
25
|
BaseCrudService,
|
|
26
26
|
createCrudController
|
|
27
27
|
} from "./chunk-JOVBJDJ2.js";
|
|
28
|
-
import {
|
|
29
|
-
BaseResponseDto
|
|
30
|
-
} from "./chunk-XX2HPTRU.js";
|
|
31
28
|
import "./chunk-DQYAIQQ5.js";
|
|
32
29
|
import {
|
|
33
30
|
ConflictAppException,
|
|
@@ -45,6 +42,9 @@ import {
|
|
|
45
42
|
import {
|
|
46
43
|
AppException
|
|
47
44
|
} from "./chunk-YFYHLYHN.js";
|
|
45
|
+
import {
|
|
46
|
+
BaseResponseDto
|
|
47
|
+
} from "./chunk-XX2HPTRU.js";
|
|
48
48
|
import {
|
|
49
49
|
I18nModule,
|
|
50
50
|
translateOr
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
|
|
2
|
+
|
|
3
|
+
var _chunk2REOCMUDcjs = require('../chunk-2REOCMUD.cjs');
|
|
4
|
+
|
|
5
|
+
// src/tiktok/tiktok.service.ts
|
|
6
|
+
var _common = require('@nestjs/common');
|
|
7
|
+
var API = "https://open.tiktokapis.com/v2";
|
|
8
|
+
var TikTokService = class {
|
|
9
|
+
constructor(credentials) {
|
|
10
|
+
this.credentials = credentials;
|
|
11
|
+
}
|
|
12
|
+
get configured() {
|
|
13
|
+
return Boolean(this.credentials.accessToken);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Datos de la cuenta. Hay que llamarlo antes de publicar: TikTok lo exige
|
|
17
|
+
* y además dice qué niveles de privacidad admite, que varían según si la
|
|
18
|
+
* cuenta es privada, de empresa o de menor de edad.
|
|
19
|
+
*/
|
|
20
|
+
async creatorInfo() {
|
|
21
|
+
return this.request("/post/publish/creator_info/query/", {});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Sube un vídeo y devuelve el `publish_id` con el que seguir su estado.
|
|
25
|
+
*
|
|
26
|
+
* Sin `privacyLevel` va a borradores; con él, se publica directo —y para eso
|
|
27
|
+
* hace falta que la aplicación haya pasado la auditoría.
|
|
28
|
+
*/
|
|
29
|
+
async publish(video) {
|
|
30
|
+
const directo = Boolean(video.privacyLevel);
|
|
31
|
+
const ruta = directo ? "/post/publish/video/init/" : "/post/publish/inbox/video/init/";
|
|
32
|
+
const cuerpo = {
|
|
33
|
+
source_info: {
|
|
34
|
+
source: "FILE_UPLOAD",
|
|
35
|
+
video_size: video.data.byteLength,
|
|
36
|
+
// Una sola parte: estos clips son de unos pocos megas y trocearlos
|
|
37
|
+
// solo añadiría puntos donde fallar.
|
|
38
|
+
chunk_size: video.data.byteLength,
|
|
39
|
+
total_chunk_count: 1
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
if (directo) {
|
|
43
|
+
cuerpo.post_info = {
|
|
44
|
+
title: video.title.slice(0, 2200),
|
|
45
|
+
privacy_level: video.privacyLevel
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const inicio = await this.request(
|
|
49
|
+
ruta,
|
|
50
|
+
cuerpo
|
|
51
|
+
);
|
|
52
|
+
const subida = await fetch(inicio.upload_url, {
|
|
53
|
+
method: "PUT",
|
|
54
|
+
headers: {
|
|
55
|
+
"Content-Type": "video/mp4",
|
|
56
|
+
// TikTok exige el rango incluso subiendo el archivo entero de una vez.
|
|
57
|
+
"Content-Range": `bytes 0-${video.data.byteLength - 1}/${video.data.byteLength}`
|
|
58
|
+
},
|
|
59
|
+
body: new Uint8Array(video.data)
|
|
60
|
+
});
|
|
61
|
+
if (!subida.ok) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`TikTok rechaz\xF3 el archivo (HTTP ${subida.status}): ${(await subida.text()).slice(0, 200)}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return inicio.publish_id;
|
|
67
|
+
}
|
|
68
|
+
/** En qué va el procesado. Termina en `PUBLISH_COMPLETE` o en un fallo. */
|
|
69
|
+
async status(publishId) {
|
|
70
|
+
return this.request(
|
|
71
|
+
"/post/publish/status/fetch/",
|
|
72
|
+
{ publish_id: publishId }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
async request(path, body) {
|
|
76
|
+
if (!this.credentials.accessToken) {
|
|
77
|
+
throw new Error("TikTokService: no access token configured");
|
|
78
|
+
}
|
|
79
|
+
const response = await fetch(`${API}${path}`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${this.credentials.accessToken}`,
|
|
83
|
+
"Content-Type": "application/json; charset=UTF-8"
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(body)
|
|
86
|
+
});
|
|
87
|
+
const text = await response.text();
|
|
88
|
+
let json;
|
|
89
|
+
try {
|
|
90
|
+
json = JSON.parse(text);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
throw new Error(`TikTok respondi\xF3 ${response.status} y no era JSON: ${text.slice(0, 200)}`);
|
|
93
|
+
}
|
|
94
|
+
if (json.error && json.error.code && json.error.code !== "ok") {
|
|
95
|
+
throw new Error(`TikTok: ${json.error.code} \u2014 ${_nullishCoalesce(json.error.message, () => ( ""))}`.trim());
|
|
96
|
+
}
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`TikTok respondi\xF3 HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
99
|
+
}
|
|
100
|
+
if (!json.data) {
|
|
101
|
+
throw new Error(`TikTok devolvi\xF3 una respuesta sin datos: ${text.slice(0, 200)}`);
|
|
102
|
+
}
|
|
103
|
+
return json.data;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
TikTokService = exports.TikTokService = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
107
|
+
_common.Injectable.call(void 0, )
|
|
108
|
+
], TikTokService);
|
|
109
|
+
|
|
110
|
+
// src/tiktok/tiktok.module.ts
|
|
111
|
+
|
|
112
|
+
var TikTokModule = class {
|
|
113
|
+
static forRoot(credentials) {
|
|
114
|
+
return {
|
|
115
|
+
module: TikTokModule,
|
|
116
|
+
providers: [
|
|
117
|
+
{
|
|
118
|
+
provide: TikTokService,
|
|
119
|
+
useFactory: () => {
|
|
120
|
+
const resolved = _nullishCoalesce(credentials, () => ( {
|
|
121
|
+
accessToken: _nullishCoalesce(process.env.TIKTOK_ACCESS_TOKEN, () => ( ""))
|
|
122
|
+
}));
|
|
123
|
+
if (!resolved.accessToken) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"TikTokModule: no access token. Set TIKTOK_ACCESS_TOKEN or pass credentials to forRoot()."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return new TikTokService(resolved);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
exports: [TikTokService]
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
TikTokModule = exports.TikTokModule = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
137
|
+
_common.Module.call(void 0, {})
|
|
138
|
+
], TikTokModule);
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
exports.TikTokModule = TikTokModule; exports.TikTokService = TikTokService;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__decorateClass
|
|
3
|
+
} from "../chunk-4MGIQFAJ.js";
|
|
4
|
+
|
|
5
|
+
// src/tiktok/tiktok.service.ts
|
|
6
|
+
import { Injectable } from "@nestjs/common";
|
|
7
|
+
var API = "https://open.tiktokapis.com/v2";
|
|
8
|
+
var TikTokService = class {
|
|
9
|
+
constructor(credentials) {
|
|
10
|
+
this.credentials = credentials;
|
|
11
|
+
}
|
|
12
|
+
get configured() {
|
|
13
|
+
return Boolean(this.credentials.accessToken);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Datos de la cuenta. Hay que llamarlo antes de publicar: TikTok lo exige
|
|
17
|
+
* y además dice qué niveles de privacidad admite, que varían según si la
|
|
18
|
+
* cuenta es privada, de empresa o de menor de edad.
|
|
19
|
+
*/
|
|
20
|
+
async creatorInfo() {
|
|
21
|
+
return this.request("/post/publish/creator_info/query/", {});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Sube un vídeo y devuelve el `publish_id` con el que seguir su estado.
|
|
25
|
+
*
|
|
26
|
+
* Sin `privacyLevel` va a borradores; con él, se publica directo —y para eso
|
|
27
|
+
* hace falta que la aplicación haya pasado la auditoría.
|
|
28
|
+
*/
|
|
29
|
+
async publish(video) {
|
|
30
|
+
const directo = Boolean(video.privacyLevel);
|
|
31
|
+
const ruta = directo ? "/post/publish/video/init/" : "/post/publish/inbox/video/init/";
|
|
32
|
+
const cuerpo = {
|
|
33
|
+
source_info: {
|
|
34
|
+
source: "FILE_UPLOAD",
|
|
35
|
+
video_size: video.data.byteLength,
|
|
36
|
+
// Una sola parte: estos clips son de unos pocos megas y trocearlos
|
|
37
|
+
// solo añadiría puntos donde fallar.
|
|
38
|
+
chunk_size: video.data.byteLength,
|
|
39
|
+
total_chunk_count: 1
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
if (directo) {
|
|
43
|
+
cuerpo.post_info = {
|
|
44
|
+
title: video.title.slice(0, 2200),
|
|
45
|
+
privacy_level: video.privacyLevel
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const inicio = await this.request(
|
|
49
|
+
ruta,
|
|
50
|
+
cuerpo
|
|
51
|
+
);
|
|
52
|
+
const subida = await fetch(inicio.upload_url, {
|
|
53
|
+
method: "PUT",
|
|
54
|
+
headers: {
|
|
55
|
+
"Content-Type": "video/mp4",
|
|
56
|
+
// TikTok exige el rango incluso subiendo el archivo entero de una vez.
|
|
57
|
+
"Content-Range": `bytes 0-${video.data.byteLength - 1}/${video.data.byteLength}`
|
|
58
|
+
},
|
|
59
|
+
body: new Uint8Array(video.data)
|
|
60
|
+
});
|
|
61
|
+
if (!subida.ok) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`TikTok rechaz\xF3 el archivo (HTTP ${subida.status}): ${(await subida.text()).slice(0, 200)}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return inicio.publish_id;
|
|
67
|
+
}
|
|
68
|
+
/** En qué va el procesado. Termina en `PUBLISH_COMPLETE` o en un fallo. */
|
|
69
|
+
async status(publishId) {
|
|
70
|
+
return this.request(
|
|
71
|
+
"/post/publish/status/fetch/",
|
|
72
|
+
{ publish_id: publishId }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
async request(path, body) {
|
|
76
|
+
if (!this.credentials.accessToken) {
|
|
77
|
+
throw new Error("TikTokService: no access token configured");
|
|
78
|
+
}
|
|
79
|
+
const response = await fetch(`${API}${path}`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${this.credentials.accessToken}`,
|
|
83
|
+
"Content-Type": "application/json; charset=UTF-8"
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(body)
|
|
86
|
+
});
|
|
87
|
+
const text = await response.text();
|
|
88
|
+
let json;
|
|
89
|
+
try {
|
|
90
|
+
json = JSON.parse(text);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(`TikTok respondi\xF3 ${response.status} y no era JSON: ${text.slice(0, 200)}`);
|
|
93
|
+
}
|
|
94
|
+
if (json.error && json.error.code && json.error.code !== "ok") {
|
|
95
|
+
throw new Error(`TikTok: ${json.error.code} \u2014 ${json.error.message ?? ""}`.trim());
|
|
96
|
+
}
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`TikTok respondi\xF3 HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
99
|
+
}
|
|
100
|
+
if (!json.data) {
|
|
101
|
+
throw new Error(`TikTok devolvi\xF3 una respuesta sin datos: ${text.slice(0, 200)}`);
|
|
102
|
+
}
|
|
103
|
+
return json.data;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
TikTokService = __decorateClass([
|
|
107
|
+
Injectable()
|
|
108
|
+
], TikTokService);
|
|
109
|
+
|
|
110
|
+
// src/tiktok/tiktok.module.ts
|
|
111
|
+
import { Module } from "@nestjs/common";
|
|
112
|
+
var TikTokModule = class {
|
|
113
|
+
static forRoot(credentials) {
|
|
114
|
+
return {
|
|
115
|
+
module: TikTokModule,
|
|
116
|
+
providers: [
|
|
117
|
+
{
|
|
118
|
+
provide: TikTokService,
|
|
119
|
+
useFactory: () => {
|
|
120
|
+
const resolved = credentials ?? {
|
|
121
|
+
accessToken: process.env.TIKTOK_ACCESS_TOKEN ?? ""
|
|
122
|
+
};
|
|
123
|
+
if (!resolved.accessToken) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"TikTokModule: no access token. Set TIKTOK_ACCESS_TOKEN or pass credentials to forRoot()."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return new TikTokService(resolved);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
exports: [TikTokService]
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
TikTokModule = __decorateClass([
|
|
137
|
+
Module({})
|
|
138
|
+
], TikTokModule);
|
|
139
|
+
export {
|
|
140
|
+
TikTokModule,
|
|
141
|
+
TikTokService
|
|
142
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { type TikTokCredentials } from './tiktok.service.js';
|
|
3
|
+
/**
|
|
4
|
+
* Outbound client for TikTok's Content Posting API. See `TikTokService`.
|
|
5
|
+
*
|
|
6
|
+
* Reads `TIKTOK_ACCESS_TOKEN` by default. Like `PinterestService`, the service
|
|
7
|
+
* takes plain credentials and uses nothing but `fetch`, so a script outside
|
|
8
|
+
* Nest can do `new TikTokService({ accessToken })` — which is what the caller
|
|
9
|
+
* holding the rendered video usually is.
|
|
10
|
+
*/
|
|
11
|
+
export declare class TikTokModule {
|
|
12
|
+
static forRoot(credentials?: TikTokCredentials): DynamicModule;
|
|
13
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface TikTokCredentials {
|
|
2
|
+
/** Token de usuario con `video.upload` o `video.publish`. */
|
|
3
|
+
accessToken: string;
|
|
4
|
+
}
|
|
5
|
+
/** Lo que TikTok cuenta de la cuenta antes de dejar publicar. */
|
|
6
|
+
export interface TikTokCreatorInfo {
|
|
7
|
+
creator_username: string;
|
|
8
|
+
creator_nickname: string;
|
|
9
|
+
/** Niveles de privacidad que la cuenta admite. Ver `publish`. */
|
|
10
|
+
privacy_level_options: string[];
|
|
11
|
+
max_video_post_duration_sec: number;
|
|
12
|
+
}
|
|
13
|
+
export interface TikTokVideo {
|
|
14
|
+
/** El vídeo, ya en MP4 H.264. */
|
|
15
|
+
data: Buffer;
|
|
16
|
+
/** El texto del post. Hasta 2200 caracteres. */
|
|
17
|
+
title: string;
|
|
18
|
+
/**
|
|
19
|
+
* Cómo queda publicado. Si se omite, va a **borradores**: aparece en la
|
|
20
|
+
* bandeja de TikTok de la cuenta y una persona le da publicar.
|
|
21
|
+
*
|
|
22
|
+
* Ese es el modo que conviene mientras la aplicación no haya pasado la
|
|
23
|
+
* auditoría de TikTok, porque el modo directo publica en `SELF_ONLY` hasta
|
|
24
|
+
* entonces —es decir, no lo ve nadie— mientras que el borrador llega igual
|
|
25
|
+
* y se publica a mano sin perder alcance.
|
|
26
|
+
*/
|
|
27
|
+
privacyLevel?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Publica vídeos con la Content Posting API de TikTok.
|
|
31
|
+
*
|
|
32
|
+
* El flujo son tres llamadas y ninguna es opcional: preguntar por la cuenta,
|
|
33
|
+
* inicializar la subida, y sondear hasta que TikTok termine de procesar. La
|
|
34
|
+
* primera es un requisito de sus guías —no una cortesía—: la API rechaza
|
|
35
|
+
* publicar sin haber consultado antes qué admite la cuenta.
|
|
36
|
+
*
|
|
37
|
+
* **Dos modos, y la diferencia importa mucho al principio.** En *Direct Post*
|
|
38
|
+
* el vídeo sale en vivo, pero exige el permiso `video.publish`, que solo se
|
|
39
|
+
* consigue pasando una auditoría de TikTok de varias semanas; hasta aprobarla
|
|
40
|
+
* todo lo publicado queda en `SELF_ONLY` y no lo ve nadie. En modo *borrador*
|
|
41
|
+
* —el de por defecto aquí— el vídeo cae en la bandeja de la cuenta y una
|
|
42
|
+
* persona le da publicar desde el teléfono: llega con todo su alcance y solo
|
|
43
|
+
* necesita `video.upload`.
|
|
44
|
+
*
|
|
45
|
+
* Se sube el binario en vez de pasar una URL a propósito. La otra vía,
|
|
46
|
+
* `PULL_FROM_URL`, obliga a verificar el dominio en el portal de TikTok, y
|
|
47
|
+
* cambiar un trámite por otro no compensa cuando el archivo ya está en disco.
|
|
48
|
+
*
|
|
49
|
+
* El límite es de 6 peticiones por minuto y por token, así que el sondeo del
|
|
50
|
+
* estado va espaciado.
|
|
51
|
+
*/
|
|
52
|
+
export declare class TikTokService {
|
|
53
|
+
private readonly credentials;
|
|
54
|
+
constructor(credentials: TikTokCredentials);
|
|
55
|
+
get configured(): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Datos de la cuenta. Hay que llamarlo antes de publicar: TikTok lo exige
|
|
58
|
+
* y además dice qué niveles de privacidad admite, que varían según si la
|
|
59
|
+
* cuenta es privada, de empresa o de menor de edad.
|
|
60
|
+
*/
|
|
61
|
+
creatorInfo(): Promise<TikTokCreatorInfo>;
|
|
62
|
+
/**
|
|
63
|
+
* Sube un vídeo y devuelve el `publish_id` con el que seguir su estado.
|
|
64
|
+
*
|
|
65
|
+
* Sin `privacyLevel` va a borradores; con él, se publica directo —y para eso
|
|
66
|
+
* hace falta que la aplicación haya pasado la auditoría.
|
|
67
|
+
*/
|
|
68
|
+
publish(video: TikTokVideo): Promise<string>;
|
|
69
|
+
/** En qué va el procesado. Termina en `PUBLISH_COMPLETE` o en un fallo. */
|
|
70
|
+
status(publishId: string): Promise<{
|
|
71
|
+
status: string;
|
|
72
|
+
fail_reason?: string;
|
|
73
|
+
}>;
|
|
74
|
+
private request;
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miguelmorales13/nestkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -121,6 +121,11 @@
|
|
|
121
121
|
"import": "./dist/pinterest/index.js",
|
|
122
122
|
"require": "./dist/pinterest/index.cjs"
|
|
123
123
|
},
|
|
124
|
+
"./tiktok": {
|
|
125
|
+
"types": "./dist/tiktok/index.d.ts",
|
|
126
|
+
"import": "./dist/tiktok/index.js",
|
|
127
|
+
"require": "./dist/tiktok/index.cjs"
|
|
128
|
+
},
|
|
124
129
|
"./media": {
|
|
125
130
|
"types": "./dist/media/index.d.ts",
|
|
126
131
|
"import": "./dist/media/index.js",
|