@miguelmorales13/nestkit 0.8.0 → 0.10.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 +49 -1
- package/dist/index.cjs +4 -4
- package/dist/index.js +4 -4
- package/dist/tiktok/index.cjs +165 -0
- package/dist/tiktok/index.d.ts +3 -0
- package/dist/tiktok/index.js +165 -0
- package/dist/tiktok/tiktok.module.d.ts +13 -0
- package/dist/tiktok/tiktok.service.d.ts +92 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -578,6 +578,49 @@ 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
|
+
Publicar son dos llamadas: inicializar la subida y sondear hasta que TikTok
|
|
584
|
+
termine de procesar.
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
import { TikTokModule, TikTokService } from '@miguelmorales13/nestkit/tiktok';
|
|
588
|
+
|
|
589
|
+
@Module({ imports: [TikTokModule.forRoot()] }) // lee TIKTOK_ACCESS_TOKEN
|
|
590
|
+
export class AppModule {}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
```ts
|
|
594
|
+
await tiktok.userInfo(); // comprueba el token sin publicar
|
|
595
|
+
const id = await tiktok.publish({ data, title }); // sin privacyLevel: a borradores
|
|
596
|
+
await tiktok.status(id); // hasta PUBLISH_COMPLETE
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**Cuidado con `creatorInfo()`.** Parece el paso previo obligatorio —así lo
|
|
600
|
+
sugiere la documentación— pero solo lo es para publicar **en directo**, y
|
|
601
|
+
además exige `video.publish`. Llamarlo con un token de borradores devuelve
|
|
602
|
+
`scope_not_authorized`, o sea que falla justo en la configuración más común.
|
|
603
|
+
Para comprobar que el token vive, `userInfo()`, que se conforma con
|
|
604
|
+
`user.info.basic`.
|
|
605
|
+
|
|
606
|
+
**La diferencia entre los dos modos importa mucho al principio.** En *Direct
|
|
607
|
+
Post* el video sale en vivo, pero exige el permiso `video.publish`, que solo se
|
|
608
|
+
consigue pasando una auditoría de TikTok de varias semanas —y hasta aprobarla
|
|
609
|
+
todo lo publicado queda en `SELF_ONLY`, o sea que no lo ve nadie—. En modo
|
|
610
|
+
**borrador**, que es el de por defecto aquí, el video cae en la bandeja de la
|
|
611
|
+
cuenta y una persona le da publicar desde el teléfono: llega con todo su
|
|
612
|
+
alcance y solo necesita `video.upload`.
|
|
613
|
+
|
|
614
|
+
Dicho de otro modo: publicar en directo antes de la auditoría es peor que no
|
|
615
|
+
publicar, porque gastas la pieza sin que la vea nadie.
|
|
616
|
+
|
|
617
|
+
Sube el binario en vez de pasar una URL a propósito. La otra vía,
|
|
618
|
+
`PULL_FROM_URL`, obliga a verificar el dominio en el portal de TikTok, y
|
|
619
|
+
cambiar un trámite por otro no compensa cuando el archivo ya está en disco.
|
|
620
|
+
|
|
621
|
+
El video tiene que ir en **MP4 H.264**. Límite de **6 peticiones por minuto**
|
|
622
|
+
por token, así que el sondeo del estado va espaciado.
|
|
623
|
+
|
|
581
624
|
### `stripe` — cliente de la SDK oficial
|
|
582
625
|
|
|
583
626
|
```ts
|
|
@@ -1023,6 +1066,7 @@ trae los datos.
|
|
|
1023
1066
|
| `@miguelmorales13/nestkit/meta` | `MetaModule`, `MetaService` — publicar en Instagram y Facebook Pages |
|
|
1024
1067
|
| `@miguelmorales13/nestkit/media` | `MediaModule`, `MediaService` — hosting efímero en memoria |
|
|
1025
1068
|
| `@miguelmorales13/nestkit/pinterest` | `PinterestModule`, `PinterestService` — publicar pines |
|
|
1069
|
+
| `@miguelmorales13/nestkit/tiktok` | `TikTokModule`, `TikTokService` — publicar videos |
|
|
1026
1070
|
| `@miguelmorales13/nestkit/stripe` | `StripeModule`, `STRIPE_CLIENT`, `createStripeWebhookController` |
|
|
1027
1071
|
| `@miguelmorales13/nestkit/auth` | `AuthUserPort`, `BaseAuthService`, `createAuthController`, `TokenService`, `JwtAuthGuard`, `RolesGuard`, `RequireTenantGuard`, `CurrentUser`, `CurrentTenant`, `Roles` |
|
|
1028
1072
|
| `@miguelmorales13/nestkit/auth/oauth` | `OAuthAuthModule`, `OAuthAuthService`, `createOAuthController`, `googleProvider`, `facebookProvider`, `OAuthStorePort`, `RefreshTokenStorePort` |
|
|
@@ -1033,9 +1077,13 @@ trae los datos.
|
|
|
1033
1077
|
| `@miguelmorales13/nestkit/i18n` | `I18nModule` (wrapper de `nestjs-i18n`), `translateOr` |
|
|
1034
1078
|
| `@miguelmorales13/nestkit/bootstrap` | `applyNestKitDefaults` |
|
|
1035
1079
|
|
|
1080
|
+
## Cambios recientes
|
|
1081
|
+
|
|
1082
|
+
Qué se añadió en cada versión y por qué está hecho así: [CAMBIOS.md](./CAMBIOS.md).
|
|
1083
|
+
|
|
1036
1084
|
## Estado del paquete
|
|
1037
1085
|
|
|
1038
|
-
`0.
|
|
1086
|
+
`0.10.0`. Sin adaptador Mongo real. Sin tests unitarios propios.
|
|
1039
1087
|
|
|
1040
1088
|
`auth/oauth` y `umami` sí tienen un consumidor real: **nutrimx** corre su acceso con Google y
|
|
1041
1089
|
Facebook sobre este módulo en producción. El resto sigue sin ejercitarse de verdad.
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,10 @@ var _chunk3CLYZC3Tcjs = require('./chunk-3CLYZC3T.cjs');
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
var _chunkM3EL5O6Tcjs = require('./chunk-M3EL5O6T.cjs');
|
|
31
35
|
require('./chunk-7SOM7EZP.cjs');
|
|
32
36
|
|
|
33
37
|
|
|
@@ -45,10 +49,6 @@ var _chunkZA56XBCKcjs = require('./chunk-ZA56XBCK.cjs');
|
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
var _chunkR7BVS6CIcjs = require('./chunk-R7BVS6CI.cjs');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
var _chunkM3EL5O6Tcjs = require('./chunk-M3EL5O6T.cjs');
|
|
52
52
|
require('./chunk-2REOCMUD.cjs');
|
|
53
53
|
|
|
54
54
|
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,10 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
BaseResponseDto
|
|
30
30
|
} from "./chunk-XX2HPTRU.js";
|
|
31
|
+
import {
|
|
32
|
+
I18nModule,
|
|
33
|
+
translateOr
|
|
34
|
+
} from "./chunk-IYUUYCP5.js";
|
|
31
35
|
import "./chunk-DQYAIQQ5.js";
|
|
32
36
|
import {
|
|
33
37
|
ConflictAppException,
|
|
@@ -45,10 +49,6 @@ import {
|
|
|
45
49
|
import {
|
|
46
50
|
AppException
|
|
47
51
|
} from "./chunk-YFYHLYHN.js";
|
|
48
|
-
import {
|
|
49
|
-
I18nModule,
|
|
50
|
-
translateOr
|
|
51
|
-
} from "./chunk-IYUUYCP5.js";
|
|
52
52
|
import "./chunk-4MGIQFAJ.js";
|
|
53
53
|
export {
|
|
54
54
|
AppException,
|
|
@@ -0,0 +1,165 @@
|
|
|
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: qué niveles de privacidad admite —varían según si es
|
|
17
|
+
* privada, de empresa o de un menor— y cuánto puede durar el video.
|
|
18
|
+
*
|
|
19
|
+
* **Obligatorio solo para publicar en directo.** Para el modo borrador no
|
|
20
|
+
* hace falta, y llamarlo con un token que solo tiene `video.upload` falla
|
|
21
|
+
* con `scope_not_authorized`, porque este endpoint pide `video.publish`.
|
|
22
|
+
* De ahí que `publish()` solo lo consulte cuando va a publicar directo.
|
|
23
|
+
*/
|
|
24
|
+
async creatorInfo() {
|
|
25
|
+
return this.request("/post/publish/creator_info/query/", {});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Sube un vídeo y devuelve el `publish_id` con el que seguir su estado.
|
|
29
|
+
*
|
|
30
|
+
* Sin `privacyLevel` va a borradores; con él, se publica directo —y para eso
|
|
31
|
+
* hace falta que la aplicación haya pasado la auditoría.
|
|
32
|
+
*/
|
|
33
|
+
async publish(video) {
|
|
34
|
+
const directo = Boolean(video.privacyLevel);
|
|
35
|
+
const ruta = directo ? "/post/publish/video/init/" : "/post/publish/inbox/video/init/";
|
|
36
|
+
const cuerpo = {
|
|
37
|
+
source_info: {
|
|
38
|
+
source: "FILE_UPLOAD",
|
|
39
|
+
video_size: video.data.byteLength,
|
|
40
|
+
// Una sola parte: estos clips son de unos pocos megas y trocearlos
|
|
41
|
+
// solo añadiría puntos donde fallar.
|
|
42
|
+
chunk_size: video.data.byteLength,
|
|
43
|
+
total_chunk_count: 1
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
if (directo) {
|
|
47
|
+
cuerpo.post_info = {
|
|
48
|
+
title: video.title.slice(0, 2200),
|
|
49
|
+
privacy_level: video.privacyLevel
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const inicio = await this.request(
|
|
53
|
+
ruta,
|
|
54
|
+
cuerpo
|
|
55
|
+
);
|
|
56
|
+
const subida = await fetch(inicio.upload_url, {
|
|
57
|
+
method: "PUT",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "video/mp4",
|
|
60
|
+
// TikTok exige el rango incluso subiendo el archivo entero de una vez.
|
|
61
|
+
"Content-Range": `bytes 0-${video.data.byteLength - 1}/${video.data.byteLength}`
|
|
62
|
+
},
|
|
63
|
+
body: new Uint8Array(video.data)
|
|
64
|
+
});
|
|
65
|
+
if (!subida.ok) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`TikTok rechaz\xF3 el archivo (HTTP ${subida.status}): ${(await subida.text()).slice(0, 200)}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return inicio.publish_id;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Quién es el dueño del token.
|
|
74
|
+
*
|
|
75
|
+
* Sirve para comprobar que el token vive sin publicar nada: `creatorInfo`
|
|
76
|
+
* no vale para eso porque pide un permiso que un token de borradores no
|
|
77
|
+
* tiene. Solo necesita `user.info.basic`.
|
|
78
|
+
*/
|
|
79
|
+
async userInfo() {
|
|
80
|
+
const datos = await this.get(
|
|
81
|
+
"/user/info/?fields=open_id,display_name"
|
|
82
|
+
);
|
|
83
|
+
return datos.user;
|
|
84
|
+
}
|
|
85
|
+
/** En qué va el procesado. Termina en `PUBLISH_COMPLETE` o en un fallo. */
|
|
86
|
+
async status(publishId) {
|
|
87
|
+
return this.request(
|
|
88
|
+
"/post/publish/status/fetch/",
|
|
89
|
+
{ publish_id: publishId }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
get(path) {
|
|
93
|
+
return this.llamar(path, "GET");
|
|
94
|
+
}
|
|
95
|
+
request(path, body) {
|
|
96
|
+
return this.llamar(path, "POST", body);
|
|
97
|
+
}
|
|
98
|
+
async llamar(path, method, body) {
|
|
99
|
+
if (!this.credentials.accessToken) {
|
|
100
|
+
throw new Error("TikTokService: no access token configured");
|
|
101
|
+
}
|
|
102
|
+
const response = await fetch(`${API}${path}`, {
|
|
103
|
+
method,
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${this.credentials.accessToken}`,
|
|
106
|
+
"Content-Type": "application/json; charset=UTF-8"
|
|
107
|
+
},
|
|
108
|
+
...method === "POST" ? { body: JSON.stringify(body) } : {}
|
|
109
|
+
});
|
|
110
|
+
const text = await response.text();
|
|
111
|
+
let json;
|
|
112
|
+
try {
|
|
113
|
+
json = JSON.parse(text);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
throw new Error(`TikTok respondi\xF3 ${response.status} y no era JSON: ${text.slice(0, 200)}`);
|
|
116
|
+
}
|
|
117
|
+
if (json.error && json.error.code && json.error.code !== "ok") {
|
|
118
|
+
throw new Error(`TikTok: ${json.error.code} \u2014 ${_nullishCoalesce(json.error.message, () => ( ""))}`.trim());
|
|
119
|
+
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(`TikTok respondi\xF3 HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
122
|
+
}
|
|
123
|
+
if (!json.data) {
|
|
124
|
+
throw new Error(`TikTok devolvi\xF3 una respuesta sin datos: ${text.slice(0, 200)}`);
|
|
125
|
+
}
|
|
126
|
+
return json.data;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
TikTokService = exports.TikTokService = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
130
|
+
_common.Injectable.call(void 0, )
|
|
131
|
+
], TikTokService);
|
|
132
|
+
|
|
133
|
+
// src/tiktok/tiktok.module.ts
|
|
134
|
+
|
|
135
|
+
var TikTokModule = class {
|
|
136
|
+
static forRoot(credentials) {
|
|
137
|
+
return {
|
|
138
|
+
module: TikTokModule,
|
|
139
|
+
providers: [
|
|
140
|
+
{
|
|
141
|
+
provide: TikTokService,
|
|
142
|
+
useFactory: () => {
|
|
143
|
+
const resolved = _nullishCoalesce(credentials, () => ( {
|
|
144
|
+
accessToken: _nullishCoalesce(process.env.TIKTOK_ACCESS_TOKEN, () => ( ""))
|
|
145
|
+
}));
|
|
146
|
+
if (!resolved.accessToken) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
"TikTokModule: no access token. Set TIKTOK_ACCESS_TOKEN or pass credentials to forRoot()."
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return new TikTokService(resolved);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
exports: [TikTokService]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
TikTokModule = exports.TikTokModule = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
|
|
160
|
+
_common.Module.call(void 0, {})
|
|
161
|
+
], TikTokModule);
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
exports.TikTokModule = TikTokModule; exports.TikTokService = TikTokService;
|
|
@@ -0,0 +1,165 @@
|
|
|
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: qué niveles de privacidad admite —varían según si es
|
|
17
|
+
* privada, de empresa o de un menor— y cuánto puede durar el video.
|
|
18
|
+
*
|
|
19
|
+
* **Obligatorio solo para publicar en directo.** Para el modo borrador no
|
|
20
|
+
* hace falta, y llamarlo con un token que solo tiene `video.upload` falla
|
|
21
|
+
* con `scope_not_authorized`, porque este endpoint pide `video.publish`.
|
|
22
|
+
* De ahí que `publish()` solo lo consulte cuando va a publicar directo.
|
|
23
|
+
*/
|
|
24
|
+
async creatorInfo() {
|
|
25
|
+
return this.request("/post/publish/creator_info/query/", {});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Sube un vídeo y devuelve el `publish_id` con el que seguir su estado.
|
|
29
|
+
*
|
|
30
|
+
* Sin `privacyLevel` va a borradores; con él, se publica directo —y para eso
|
|
31
|
+
* hace falta que la aplicación haya pasado la auditoría.
|
|
32
|
+
*/
|
|
33
|
+
async publish(video) {
|
|
34
|
+
const directo = Boolean(video.privacyLevel);
|
|
35
|
+
const ruta = directo ? "/post/publish/video/init/" : "/post/publish/inbox/video/init/";
|
|
36
|
+
const cuerpo = {
|
|
37
|
+
source_info: {
|
|
38
|
+
source: "FILE_UPLOAD",
|
|
39
|
+
video_size: video.data.byteLength,
|
|
40
|
+
// Una sola parte: estos clips son de unos pocos megas y trocearlos
|
|
41
|
+
// solo añadiría puntos donde fallar.
|
|
42
|
+
chunk_size: video.data.byteLength,
|
|
43
|
+
total_chunk_count: 1
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
if (directo) {
|
|
47
|
+
cuerpo.post_info = {
|
|
48
|
+
title: video.title.slice(0, 2200),
|
|
49
|
+
privacy_level: video.privacyLevel
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const inicio = await this.request(
|
|
53
|
+
ruta,
|
|
54
|
+
cuerpo
|
|
55
|
+
);
|
|
56
|
+
const subida = await fetch(inicio.upload_url, {
|
|
57
|
+
method: "PUT",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "video/mp4",
|
|
60
|
+
// TikTok exige el rango incluso subiendo el archivo entero de una vez.
|
|
61
|
+
"Content-Range": `bytes 0-${video.data.byteLength - 1}/${video.data.byteLength}`
|
|
62
|
+
},
|
|
63
|
+
body: new Uint8Array(video.data)
|
|
64
|
+
});
|
|
65
|
+
if (!subida.ok) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`TikTok rechaz\xF3 el archivo (HTTP ${subida.status}): ${(await subida.text()).slice(0, 200)}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return inicio.publish_id;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Quién es el dueño del token.
|
|
74
|
+
*
|
|
75
|
+
* Sirve para comprobar que el token vive sin publicar nada: `creatorInfo`
|
|
76
|
+
* no vale para eso porque pide un permiso que un token de borradores no
|
|
77
|
+
* tiene. Solo necesita `user.info.basic`.
|
|
78
|
+
*/
|
|
79
|
+
async userInfo() {
|
|
80
|
+
const datos = await this.get(
|
|
81
|
+
"/user/info/?fields=open_id,display_name"
|
|
82
|
+
);
|
|
83
|
+
return datos.user;
|
|
84
|
+
}
|
|
85
|
+
/** En qué va el procesado. Termina en `PUBLISH_COMPLETE` o en un fallo. */
|
|
86
|
+
async status(publishId) {
|
|
87
|
+
return this.request(
|
|
88
|
+
"/post/publish/status/fetch/",
|
|
89
|
+
{ publish_id: publishId }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
get(path) {
|
|
93
|
+
return this.llamar(path, "GET");
|
|
94
|
+
}
|
|
95
|
+
request(path, body) {
|
|
96
|
+
return this.llamar(path, "POST", body);
|
|
97
|
+
}
|
|
98
|
+
async llamar(path, method, body) {
|
|
99
|
+
if (!this.credentials.accessToken) {
|
|
100
|
+
throw new Error("TikTokService: no access token configured");
|
|
101
|
+
}
|
|
102
|
+
const response = await fetch(`${API}${path}`, {
|
|
103
|
+
method,
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${this.credentials.accessToken}`,
|
|
106
|
+
"Content-Type": "application/json; charset=UTF-8"
|
|
107
|
+
},
|
|
108
|
+
...method === "POST" ? { body: JSON.stringify(body) } : {}
|
|
109
|
+
});
|
|
110
|
+
const text = await response.text();
|
|
111
|
+
let json;
|
|
112
|
+
try {
|
|
113
|
+
json = JSON.parse(text);
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error(`TikTok respondi\xF3 ${response.status} y no era JSON: ${text.slice(0, 200)}`);
|
|
116
|
+
}
|
|
117
|
+
if (json.error && json.error.code && json.error.code !== "ok") {
|
|
118
|
+
throw new Error(`TikTok: ${json.error.code} \u2014 ${json.error.message ?? ""}`.trim());
|
|
119
|
+
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(`TikTok respondi\xF3 HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
122
|
+
}
|
|
123
|
+
if (!json.data) {
|
|
124
|
+
throw new Error(`TikTok devolvi\xF3 una respuesta sin datos: ${text.slice(0, 200)}`);
|
|
125
|
+
}
|
|
126
|
+
return json.data;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
TikTokService = __decorateClass([
|
|
130
|
+
Injectable()
|
|
131
|
+
], TikTokService);
|
|
132
|
+
|
|
133
|
+
// src/tiktok/tiktok.module.ts
|
|
134
|
+
import { Module } from "@nestjs/common";
|
|
135
|
+
var TikTokModule = class {
|
|
136
|
+
static forRoot(credentials) {
|
|
137
|
+
return {
|
|
138
|
+
module: TikTokModule,
|
|
139
|
+
providers: [
|
|
140
|
+
{
|
|
141
|
+
provide: TikTokService,
|
|
142
|
+
useFactory: () => {
|
|
143
|
+
const resolved = credentials ?? {
|
|
144
|
+
accessToken: process.env.TIKTOK_ACCESS_TOKEN ?? ""
|
|
145
|
+
};
|
|
146
|
+
if (!resolved.accessToken) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
"TikTokModule: no access token. Set TIKTOK_ACCESS_TOKEN or pass credentials to forRoot()."
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return new TikTokService(resolved);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
exports: [TikTokService]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
TikTokModule = __decorateClass([
|
|
160
|
+
Module({})
|
|
161
|
+
], TikTokModule);
|
|
162
|
+
export {
|
|
163
|
+
TikTokModule,
|
|
164
|
+
TikTokService
|
|
165
|
+
};
|
|
@@ -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,92 @@
|
|
|
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
|
+
* Publicar son dos llamadas: inicializar la subida y sondear hasta que TikTok
|
|
33
|
+
* termine de procesar. Hay una tercera, `creatorInfo`, que es obligatoria
|
|
34
|
+
* **solo** para publicar en directo —y que además exige `video.publish`, así
|
|
35
|
+
* que llamarla con un token de borradores falla.
|
|
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: qué niveles de privacidad admite —varían según si es
|
|
58
|
+
* privada, de empresa o de un menor— y cuánto puede durar el video.
|
|
59
|
+
*
|
|
60
|
+
* **Obligatorio solo para publicar en directo.** Para el modo borrador no
|
|
61
|
+
* hace falta, y llamarlo con un token que solo tiene `video.upload` falla
|
|
62
|
+
* con `scope_not_authorized`, porque este endpoint pide `video.publish`.
|
|
63
|
+
* De ahí que `publish()` solo lo consulte cuando va a publicar directo.
|
|
64
|
+
*/
|
|
65
|
+
creatorInfo(): Promise<TikTokCreatorInfo>;
|
|
66
|
+
/**
|
|
67
|
+
* Sube un vídeo y devuelve el `publish_id` con el que seguir su estado.
|
|
68
|
+
*
|
|
69
|
+
* Sin `privacyLevel` va a borradores; con él, se publica directo —y para eso
|
|
70
|
+
* hace falta que la aplicación haya pasado la auditoría.
|
|
71
|
+
*/
|
|
72
|
+
publish(video: TikTokVideo): Promise<string>;
|
|
73
|
+
/**
|
|
74
|
+
* Quién es el dueño del token.
|
|
75
|
+
*
|
|
76
|
+
* Sirve para comprobar que el token vive sin publicar nada: `creatorInfo`
|
|
77
|
+
* no vale para eso porque pide un permiso que un token de borradores no
|
|
78
|
+
* tiene. Solo necesita `user.info.basic`.
|
|
79
|
+
*/
|
|
80
|
+
userInfo(): Promise<{
|
|
81
|
+
open_id: string;
|
|
82
|
+
display_name: string;
|
|
83
|
+
}>;
|
|
84
|
+
/** En qué va el procesado. Termina en `PUBLISH_COMPLETE` o en un fallo. */
|
|
85
|
+
status(publishId: string): Promise<{
|
|
86
|
+
status: string;
|
|
87
|
+
fail_reason?: string;
|
|
88
|
+
}>;
|
|
89
|
+
private get;
|
|
90
|
+
private request;
|
|
91
|
+
private llamar;
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miguelmorales13/nestkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|