@praeko/cli 0.1.0 → 0.2.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/CHANGELOG.md +10 -2
- package/README.md +2 -2
- package/dist/chunks/{adaptadores-X3NUBMLT.mjs → adaptadores-73I3A475.mjs} +5 -5
- package/dist/chunks/{buffer-5TUDOBVN.mjs → buffer-7UXG3MU4.mjs} +5 -5
- package/dist/chunks/{chunk-QDZLRBTP.mjs → chunk-FZ2EWUHA.mjs} +133 -57
- package/dist/chunks/{chunk-SAHLBARV.mjs → chunk-K5RD3QB7.mjs} +12 -12
- package/dist/chunks/{chunk-Z325UM27.mjs → chunk-SL4UDIFM.mjs} +1 -1
- package/dist/chunks/{chunk-IZEX746E.mjs → chunk-SPELUAPP.mjs} +15 -15
- package/dist/chunks/chunk-SSYEOFS5.mjs +56 -0
- package/dist/chunks/{chunk-LLXYUCTT.mjs → chunk-TDQGKU7I.mjs} +9 -8
- package/dist/chunks/{chunk-UK3UKRPR.mjs → chunk-V3GB2IUZ.mjs} +20 -3
- package/dist/chunks/{instagram-GNY73ESY.mjs → instagram-3P4ROM45.mjs} +1 -1
- package/dist/chunks/{instagram-meta-COY6BTNX.mjs → instagram-meta-ANEWEJV7.mjs} +2 -1
- package/dist/chunks/{local-EUJHQ76T.mjs → local-QWOY7WI5.mjs} +3 -3
- package/dist/chunks/{registro-FL2CVMYM.mjs → registro-2EE55WTS.mjs} +1 -1
- package/dist/chunks/{remoto-OAB5Y2MS.mjs → remoto-HPELHT4F.mjs} +2 -2
- package/dist/chunks/{tiktok-QOUHOWDU.mjs → tiktok-DVG5JNJE.mjs} +1 -1
- package/dist/chunks/{tiktok-api-36DV2H43.mjs → tiktok-api-2CYJNS73.mjs} +1 -1
- package/dist/chunks/{youtube-RZ4XTUZW.mjs → youtube-EZL4SNMO.mjs} +4 -2
- package/dist/chunks/{youtube-analytics-3KIW2ZIW.mjs → youtube-analytics-74ZDYCY6.mjs} +2 -2
- package/dist/chunks/{youtube-api-Z2RP5D2V.mjs → youtube-api-QVEOU2L7.mjs} +3 -1
- package/dist/chunks/{youtube-comunidade-OAK5GGK4.mjs → youtube-comunidade-GJBG2XR7.mjs} +2 -2
- package/dist/praeko.mjs +851 -8
- package/package.json +5 -5
- package/dist/arauto.mjs +0 -11
- package/dist/chunks/chunk-6YARHJZ2.mjs +0 -56
- package/dist/chunks/chunk-XQO6MI7C.mjs +0 -851
package/dist/praeko.mjs
CHANGED
|
@@ -1,11 +1,854 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import "./chunks/chunk-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import {
|
|
4
|
+
trocarHashtags
|
|
5
|
+
} from "./chunks/chunk-TDQGKU7I.mjs";
|
|
6
|
+
import "./chunks/chunk-V3GB2IUZ.mjs";
|
|
7
|
+
import {
|
|
8
|
+
acharConfig,
|
|
9
|
+
capa_quadro_exports,
|
|
10
|
+
criarContexto,
|
|
11
|
+
normalizarConfig,
|
|
12
|
+
parseQuando,
|
|
13
|
+
textosPorRede,
|
|
14
|
+
validar
|
|
15
|
+
} from "./chunks/chunk-K5RD3QB7.mjs";
|
|
16
|
+
import {
|
|
17
|
+
caminhoEstado,
|
|
18
|
+
cancelarItem,
|
|
19
|
+
criarPublicador,
|
|
20
|
+
executar,
|
|
21
|
+
lerFila,
|
|
22
|
+
liberarItem,
|
|
23
|
+
pastaEstado,
|
|
24
|
+
plistLaunchd
|
|
25
|
+
} from "./chunks/chunk-SPELUAPP.mjs";
|
|
26
|
+
import {
|
|
27
|
+
registro_exports
|
|
28
|
+
} from "./chunks/chunk-SSYEOFS5.mjs";
|
|
29
|
+
import {
|
|
30
|
+
Praeko,
|
|
31
|
+
migrar,
|
|
32
|
+
segredos
|
|
33
|
+
} from "./chunks/chunk-FZ2EWUHA.mjs";
|
|
34
|
+
import {
|
|
35
|
+
REDES,
|
|
36
|
+
validarPedido
|
|
37
|
+
} from "./chunks/chunk-SL4UDIFM.mjs";
|
|
11
38
|
import "./chunks/chunk-LCFK6X55.mjs";
|
|
39
|
+
|
|
40
|
+
// src/main.ts
|
|
41
|
+
import { spawnSync } from "node:child_process";
|
|
42
|
+
import { createRequire } from "node:module";
|
|
43
|
+
import { dirname, join as join3 } from "node:path";
|
|
44
|
+
|
|
45
|
+
// src/cli.ts
|
|
46
|
+
import { mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
47
|
+
import { homedir } from "node:os";
|
|
48
|
+
import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
|
|
49
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
50
|
+
import { fileURLToPath } from "node:url";
|
|
51
|
+
|
|
52
|
+
// ../core/src/chaves.ts
|
|
53
|
+
var b64u = (u8) => {
|
|
54
|
+
let s = "";
|
|
55
|
+
for (const b of u8) s += String.fromCharCode(b);
|
|
56
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
57
|
+
};
|
|
58
|
+
var hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
59
|
+
var PREFIXO_CHAVE = "praeko_";
|
|
60
|
+
var FORMATO_CHAVE = /^(?:praeko|arauto)_[A-Za-z0-9_-]{20,}$/;
|
|
61
|
+
var gerarChave = () => PREFIXO_CHAVE + b64u(crypto.getRandomValues(new Uint8Array(32)));
|
|
62
|
+
var hashChave = async (chave2) => hex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(chave2)));
|
|
63
|
+
var prefixoChave = (chave2) => chave2.slice(0, 14);
|
|
64
|
+
var idChave = () => "chv_" + hex(crypto.getRandomValues(new Uint8Array(8)));
|
|
65
|
+
|
|
66
|
+
// src/pacote.ts
|
|
67
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
68
|
+
import { basename, join, resolve } from "node:path";
|
|
69
|
+
var ehDir = (p) => existsSync(p) && statSync(p).isDirectory();
|
|
70
|
+
function acharPacote(arg, { raiz, config }) {
|
|
71
|
+
for (const c of [resolve(arg), join(raiz, config.pacotes || "pacotes", arg), config.pasta ? join(raiz, config.pasta, arg) : null]) if (c && ehDir(c)) return c;
|
|
72
|
+
throw new Error(`pacote "${arg}" n\xE3o encontrado (pasta com publicacao.json, v\xEDdeo e capa). Use um caminho ou --meta/--video/--capa`);
|
|
73
|
+
}
|
|
74
|
+
function lerPacote({ arg, flags: flags2, raiz, config }) {
|
|
75
|
+
const dir = arg ? acharPacote(arg, { raiz, config }) : null;
|
|
76
|
+
const metaArq = flags2.meta ? resolve(flags2.meta) : dir && join(dir, "publicacao.json");
|
|
77
|
+
if (!metaArq || !existsSync(metaArq)) throw new Error("sem publicacao.json (use uma pasta-pacote ou --meta arquivo.json)");
|
|
78
|
+
const meta = JSON.parse(readFileSync(metaArq, "utf8"));
|
|
79
|
+
const base = dir || resolve(metaArq, "..");
|
|
80
|
+
const achar = (nome, re) => {
|
|
81
|
+
if (nome && existsSync(join(base, nome))) return join(base, nome);
|
|
82
|
+
const l = readdirSync(base).filter((f) => re.test(f)).sort();
|
|
83
|
+
return l.length ? join(base, l[l.length - 1]) : null;
|
|
84
|
+
};
|
|
85
|
+
const video = flags2.video ? resolve(flags2.video) : achar(meta.arquivos?.video, /\.(mp4|mov)$/i);
|
|
86
|
+
const capa = flags2.cover ? resolve(flags2.cover) : achar(meta.arquivos?.capa, /(capa|cover|thumb).*\.(png|jpe?g)$/i);
|
|
87
|
+
if (!video || !existsSync(video)) throw new Error(`n\xE3o achei o v\xEDdeo (arquivos.video = ${meta.arquivos?.video || "?"}); use --video`);
|
|
88
|
+
return { dir: base, id: flags2.id || basename(base), meta, video, capa: capa && existsSync(capa) ? capa : null };
|
|
89
|
+
}
|
|
90
|
+
function montarPedido({ pacote, config, destinos, opcoes = {} }) {
|
|
91
|
+
const v = validar({ config, ep: pacote.meta });
|
|
92
|
+
const textos = textosPorRede({ config, ep: pacote.meta });
|
|
93
|
+
return {
|
|
94
|
+
erros: v.erros,
|
|
95
|
+
avisos: v.avisos,
|
|
96
|
+
pedido: { id: pacote.id, titulo: pacote.meta.titulo, midia: { video: pacote.video, capa: pacote.capa }, textos, destinos, opcoes: { ...opcoes, formato: pacote.meta.formato || "short" } }
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/cli.ts
|
|
101
|
+
var injetado = (extra) => extra?.praeko ?? extra?.arauto;
|
|
102
|
+
var BIN = resolve2(fileURLToPath(import.meta.url), "..", "..", "bin", "praeko.mjs");
|
|
103
|
+
var NOME = { youtube: "YouTube", instagram: "Instagram", tiktok: "TikTok" };
|
|
104
|
+
var COM_VALOR = /* @__PURE__ */ new Set(["type", "sort", "direction", "limit", "when", "networks", "yt", "ig", "tt", "yt-mode", "tt-mode", "video", "cover", "meta", "instagram", "instagram-id", "id", "url", "via", "config", "minutes", "key", "server", "scope", "name", "timezone", "from", "to", "state", "tag", "channel", "max", "page", "title", "description-file", "swap-hashtags", "text", "buffer-id", "cover-media", "days", "day", "tt-privacy", "tt-frame", "code", "route", "method", "cursor", "key-id", "events", "idempotency-key"]);
|
|
105
|
+
var FLAG_ALIAS = { quando: "when", redes: "networks", "modo-yt": "yt-mode", "modo-tt": "tt-mode", por: "via", enviar: "send", simular: "dry-run", etiqueta: "tag", estado: "state", canal: "channel", titulo: "title", "descricao-arquivo": "description-file", "trocar-hashtags": "swap-hashtags", texto: "text", "sem-resposta": "unanswered", ordem: "sort", direcao: "direction", limite: "limit", pagina: "page", comparar: "compare", dias: "days", dia: "day", servidor: "server", chave: "key", escopo: "scope", nome: "name", fuso: "timezone", de: "from", ate: "to", fila: "queue", rascunho: "draft", lembrete: "reminder", capa: "cover", "capa-midia": "cover-media", "privacidade-tt": "tt-privacy", "quadro-tt": "tt-frame", cancelar: "cancel", "sem-capa-no-quadro": "no-cover-in-frame", minutos: "minutes", tipo: "type", ajuda: "help" };
|
|
106
|
+
var CMD_ALIAS = { postar: "post", agendar: "schedule", fila: "queue", conta: "account", canais: "channels", calendario: "calendar", vagas: "slots", "publicar-agora": "publish-now", "youtube-agendar": "youtube-schedule", "adotar-buffer": "adopt-buffer", importar: "import", conteudo: "content", seguidores: "followers", chave: "key", keys: "key", ajuda: "help" };
|
|
107
|
+
var SUB_ALIAS = {
|
|
108
|
+
queue: { listar: "list", executar: "run", cancelar: "cancel", comentar: "comment", instalar: "install", remover: "remove" },
|
|
109
|
+
account: { conectar: "connect", desconectar: "disconnect", enviar: "send" },
|
|
110
|
+
youtube: { editar: "edit", comentarios: "comments", resolver: "resolve", reabrir: "reopen", responder: "reply" },
|
|
111
|
+
tiktok: { importar: "import", conectar: "connect", desconectar: "disconnect" },
|
|
112
|
+
key: { guardar: "store", criar: "create", listar: "list", revogar: "revoke", migrar: "migrate" }
|
|
113
|
+
};
|
|
114
|
+
var MODO_YT = { private: "privado", scheduled: "agendado", public: "publico" };
|
|
115
|
+
var MODO_TT = { draft: "rascunho", direct: "direto" };
|
|
116
|
+
var ESTADO = { fila: "queued", rascunho: "draft", enviado: "sent", erro: "error", cancelado: "canceled" };
|
|
117
|
+
var ESCOPO = { leitura: "read", escrita: "write" };
|
|
118
|
+
var ESCOPO_PT = { read: "leitura", write: "escrita" };
|
|
119
|
+
var ORDEM = { recentes: "recent", reacoes: "reactions", comentarios: "comments", engajamento: "engagement", compartilhamentos: "shares", salvamentos: "saves", alcance: "reach", publicadoEm: "publishedAt" };
|
|
120
|
+
var DIA = { dom: "sun", seg: "mon", ter: "tue", qua: "wed", qui: "thu", sex: "fri", sab: "sat" };
|
|
121
|
+
var DIA_PT = Object.fromEntries(Object.entries(DIA).map(([k, v]) => [v, k]));
|
|
122
|
+
var ESTADO_PT = { draft: "rascunho", scheduled: "agendado", sent: "enviado", error: "erro", canceled: "cancelado", private: "privado", queued: "fila" };
|
|
123
|
+
var estadoPt = (e) => ESTADO_PT[e] ?? e;
|
|
124
|
+
var AJUDA = `Praeko CLI (formerly "arauto"): personal publisher (YouTube, Instagram, TikTok)
|
|
125
|
+
|
|
126
|
+
post <folder|name> --when "06/11 17:30" [--networks yt,ig,tt] [--yt|--ig|--tt "time"]
|
|
127
|
+
[--yt-mode private|scheduled|public] [--tt-mode draft|direct] [--via local|buffer] [--send]
|
|
128
|
+
[--tt-privacy SELF_ONLY|PUBLIC_TO_EVERYONE|\u2026] [--tt-frame 1500] (only with --server; direct public TikTok needs the app audit)
|
|
129
|
+
publishes through Praeko (queue + direct providers); without --send it only simulates
|
|
130
|
+
schedule <folder|name> \u2026 the same, through Buffer (--via buffer); schedule <name> --status | --cancel [--networks yt]
|
|
131
|
+
schedule --key: stores the Buffer key (echo $KEY | praeko schedule --key)
|
|
132
|
+
queue [list|run [--dry-run]|cancel <id>|comment <id>|install [--minutes N]|remove] the local queue
|
|
133
|
+
account <youtube|instagram|tiktok> [connect|status]
|
|
134
|
+
channels [--via local|buffer] what is connected
|
|
135
|
+
|
|
136
|
+
Praeko service (Cloudflare; formerly Arauto), with --server https://praeko.app (or PRAEKO_SERVER, or "remoto": { "url" } in praeko.json) and the key in PRAEKO_KEY (ARAUTO_CHAVE still works):
|
|
137
|
+
post <package> --server URL [--queue | --when \u2026 | --draft] [--tag a,b] [--yt-mode \u2026] [--send]
|
|
138
|
+
posts [--state queued|draft|sent|error|canceled] [--channel yt] [--tag x] [--limit n] lists the service posts
|
|
139
|
+
calendar [--from ISO] [--to ISO] [--channel yt] scheduled items and free slots
|
|
140
|
+
slots <network> [day time \u2026] [--timezone America/Sao_Paulo] show or replace the weekly slots (e.g. slots youtube fri 17:30 sun 22:00)
|
|
141
|
+
publish-now <id> [--networks yt] takes it out of the queue and publishes now
|
|
142
|
+
youtube videos [--max 20] | video <id> the channel videos (private and scheduled included)
|
|
143
|
+
youtube edit <id> [--title T] [--description-file f.txt] [--swap-hashtags "#a #b"] [--send]
|
|
144
|
+
changes title/description (simulates; --send writes)
|
|
145
|
+
youtube comments [--video id] [--unanswered] [--max 20] comments still without your reply
|
|
146
|
+
youtube resolve|reopen <thread> hides/restores a thread in the pending box (Praeko only)
|
|
147
|
+
youtube reply <thread> --text "\u2026" [--send] replies to a comment (simulates; --send writes)
|
|
148
|
+
adopt-buffer --id name --title "\u2026" --when "07/11 10:00" --buffer-id <Buffer post> shows a Reel already scheduled in Buffer (does not call Buffer)
|
|
149
|
+
insights [--days 30] [--channel yt|ig] [--type reel|short|long] [--tag x] [--compare] [--sort views] [--posts] [--json]
|
|
150
|
+
insights 48h [--days 30] 48 h reading per post, as a table for product-marketing.md
|
|
151
|
+
import [youtube|instagram|tiktok] [--dry-run] (admin) brings the history into Insights
|
|
152
|
+
content [--channel yt] [--limit 30] [--sort recent|views|reactions] everything already published (Praeko + imported)
|
|
153
|
+
followers <instagram|tiktok> [value] [--day YYYY-MM-DD] lists followers; manual entry only for Instagram
|
|
154
|
+
tiktok import [--dry-run] | videos [--limit 30] [--sort views] | status | connect | disconnect
|
|
155
|
+
youtube-schedule <post> --when "07/11 10:00" schedules a video already uploaded to YouTube (private)
|
|
156
|
+
account <network> connect --server URL returns (and opens) the authorization link
|
|
157
|
+
account tiktok connect|status|disconnect --server URL
|
|
158
|
+
account instagram connect --meta [--replace] [--instagram user] [--instagram-id ID] [--probe] --server URL login da Meta (OAuth); --replace s\xF3 se j\xE1 houver conex\xE3o
|
|
159
|
+
account instagram send --server URL sends the Meta token (Keychain) to the server, encrypted
|
|
160
|
+
key store stores the service key (echo $KEY | praeko key store)
|
|
161
|
+
key create --name bardo [--scope write] [--server URL] creates an API key (needs an admin key); scopes: read, write, admin
|
|
162
|
+
key create --name admin --scope admin --sql generates the first key and the SQL for D1 (docs/servidor.md)
|
|
163
|
+
key list | key revoke <id>
|
|
164
|
+
usage [--days 7] [--json] API usage: calls, success rate, p95, peak per minute, per day and per key
|
|
165
|
+
usage log [--key-id id|session] [--code 4xx|404] [--route /v1/posts] [--method GET] [--days 30] [--limit 50] [--cursor c] [--json] request log (30 days, metadata only)
|
|
166
|
+
webhook list | create --url https://\u2026 [--events post.sent,post.failed] | test <id> | deliveries <id> [--limit 20] | retry <id> <delivery>
|
|
167
|
+
webhook pause|resume|remove <id> | rotate-secret <id> | events <id> post.sent,post.failed (admin) outgoing webhooks; the secret is printed once
|
|
168
|
+
keys migrate copies the Keychain items of the old service (arauto) to the new one (praeko); prints names only, never secrets
|
|
169
|
+
help
|
|
170
|
+
|
|
171
|
+
Global flags: --server URL, --key, --config file, --help. --idempotency-key K (publish-now, webhook create): sends the Idempotency-Key header so a repeated command does not repeat the action (docs/idempotencia.md).
|
|
172
|
+
The package folder has publicacao.json, the video and the cover (or use --meta/--video/--cover). Config: praeko.json (or the old arauto.json, or the
|
|
173
|
+
publicacao section of a marca.json) in the current folder or above. Nothing is sent without --send. Human-readable output is in Portuguese.
|
|
174
|
+
|
|
175
|
+
Deprecated aliases (Portuguese), still working: postar agendar fila conta canais calendario vagas publicar-agora youtube-agendar adotar-buffer importar conteudo seguidores chave ajuda;
|
|
176
|
+
subcommands listar executar cancelar comentar instalar remover conectar desconectar enviar editar comentarios resolver reabrir responder importar guardar criar revogar;
|
|
177
|
+
flags --quando --redes --modo-yt --modo-tt --por --enviar --simular --etiqueta --estado --canal --titulo --descricao-arquivo --trocar-hashtags --texto --sem-resposta --ordem --direcao --limite --pagina --comparar --dias --dia --servidor --chave --escopo --nome --fuso --de --ate --fila --rascunho --lembrete --capa --capa-midia --privacidade-tt --quadro-tt --cancelar --sem-capa-no-quadro --minutos --tipo --ajuda;
|
|
178
|
+
old values (privado agendado publico rascunho direto fila enviado leitura escrita \u2026) are accepted too.
|
|
179
|
+
`;
|
|
180
|
+
function flags(a) {
|
|
181
|
+
const f = { _: [] };
|
|
182
|
+
for (let i = 0; i < a.length; i++) {
|
|
183
|
+
if (a[i].startsWith("--")) {
|
|
184
|
+
let k = a[i].slice(2);
|
|
185
|
+
k = FLAG_ALIAS[k] ?? k;
|
|
186
|
+
if (COM_VALOR.has(k) && a[i + 1] !== void 0 && !a[i + 1].startsWith("--")) f[k] = a[++i];
|
|
187
|
+
else f[k] = true;
|
|
188
|
+
} else f._.push(a[i]);
|
|
189
|
+
}
|
|
190
|
+
if (f["yt-mode"] && typeof f["yt-mode"] === "string") f["yt-mode"] = MODO_YT[f["yt-mode"]] ?? f["yt-mode"];
|
|
191
|
+
if (f["tt-mode"] && typeof f["tt-mode"] === "string") f["tt-mode"] = MODO_TT[f["tt-mode"]] ?? f["tt-mode"];
|
|
192
|
+
if (typeof f.state === "string") f.state = ESTADO[f.state] ?? f.state;
|
|
193
|
+
if (typeof f.scope === "string") f.scope = ESCOPO[f.scope] ?? f.scope;
|
|
194
|
+
if (typeof f.sort === "string") f.sort = ORDEM[f.sort] ?? f.sort;
|
|
195
|
+
if (f.type === "longo") f.type = "long";
|
|
196
|
+
return f;
|
|
197
|
+
}
|
|
198
|
+
var redesDe = (txt, padrao) => txt ? String(txt).split(",").map((x) => REDES[x.trim()] || (() => {
|
|
199
|
+
throw new Error(`rede desconhecida: ${x}`);
|
|
200
|
+
})()) : padrao;
|
|
201
|
+
var local = (iso) => new Date(iso).toLocaleString("pt-BR", { timeZone: "America/Sao_Paulo", dateStyle: "short", timeStyle: "short" });
|
|
202
|
+
function carregar(f) {
|
|
203
|
+
const achado = f.config ? { raiz: resolve2(f.config, ".."), config: normalizarConfig(JSON.parse(readFileSync2(resolve2(f.config), "utf8"))) } : acharConfig();
|
|
204
|
+
if (!achado) throw new Error("n\xE3o achei praeko.json (nem arauto.json ou marca.json) na pasta atual ou acima");
|
|
205
|
+
return achado;
|
|
206
|
+
}
|
|
207
|
+
async function executarCli(argv, { log = console.log, erro = console.error, ctxExtra = {} } = {}) {
|
|
208
|
+
const [cmd0, ...resto] = argv;
|
|
209
|
+
const cmd = CMD_ALIAS[cmd0] ?? cmd0;
|
|
210
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
211
|
+
log(false ? "dev" : "0.2.0");
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "--ajuda") {
|
|
215
|
+
log(AJUDA);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
const f = flags(resto);
|
|
219
|
+
if (!f.server && process.env.PRAEKO_SERVER) f.server = process.env.PRAEKO_SERVER;
|
|
220
|
+
const subs = SUB_ALIAS[cmd], idx = cmd === "account" ? 1 : 0;
|
|
221
|
+
if (subs && f._[idx]) f._[idx] = subs[f._[idx]] ?? f._[idx];
|
|
222
|
+
try {
|
|
223
|
+
if (cmd === "post" || cmd === "schedule") return await postar(cmd, f, log, ctxExtra);
|
|
224
|
+
if (cmd === "queue") return await fila(f, log, ctxExtra);
|
|
225
|
+
if (cmd === "account") return await conta(f, log, ctxExtra, resto);
|
|
226
|
+
if (cmd === "channels") return await canais(f, log, ctxExtra);
|
|
227
|
+
if (cmd === "posts") return await postsRemoto(f, log, ctxExtra);
|
|
228
|
+
if (cmd === "calendar") return await calendarioRemoto(f, log, ctxExtra);
|
|
229
|
+
if (cmd === "slots") return await vagasRemoto(f, log, ctxExtra);
|
|
230
|
+
if (cmd === "publish-now") return await publicarAgoraRemoto(f, log, ctxExtra);
|
|
231
|
+
if (cmd === "youtube-schedule") return await youtubeAgendarRemoto(f, log, ctxExtra);
|
|
232
|
+
if (cmd === "adopt-buffer") return await adotarBufferRemoto(f, log, ctxExtra);
|
|
233
|
+
if (cmd === "youtube") return await youtubeCmd(f, log, ctxExtra);
|
|
234
|
+
if (cmd === "tiktok") return await tiktokCmd(f, log, ctxExtra);
|
|
235
|
+
if (cmd === "insights") return await insightsCmd(f, log, ctxExtra);
|
|
236
|
+
if (cmd === "import") return await importarCmd(f, log, ctxExtra);
|
|
237
|
+
if (cmd === "followers") return await seguidoresCmd(f, log, ctxExtra);
|
|
238
|
+
if (cmd === "content") return await conteudoCmd(f, log, ctxExtra);
|
|
239
|
+
if (cmd === "key") return await chave(f, log, ctxExtra);
|
|
240
|
+
if (cmd === "usage") return await usoCmd(f, log, ctxExtra);
|
|
241
|
+
if (cmd === "webhook" || cmd === "webhooks") return await webhookCmd(f, log, ctxExtra);
|
|
242
|
+
log(AJUDA);
|
|
243
|
+
return 2;
|
|
244
|
+
} catch (e) {
|
|
245
|
+
erro("erro: " + e.message);
|
|
246
|
+
return 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function postar(cmd, f, log, extra) {
|
|
250
|
+
if (cmd === "schedule" && f.key !== void 0) {
|
|
251
|
+
let k = typeof f.key === "string" ? f.key : "";
|
|
252
|
+
if (!k) {
|
|
253
|
+
const ch = [];
|
|
254
|
+
for await (const c of process.stdin) ch.push(c);
|
|
255
|
+
k = Buffer.concat(ch).toString().trim();
|
|
256
|
+
}
|
|
257
|
+
if (!k) throw new Error("uso: echo $KEY | praeko schedule --key (publish.buffer.com/settings/api)");
|
|
258
|
+
log(`chave do Buffer guardada (${segredos.guardar("buffer/principal", k)})`);
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
const { raiz, config: config0 } = carregar(f);
|
|
262
|
+
const config = f.server ? { ...config0, remoto: { ...config0.remoto || {}, url: f.server } } : config0, ctx = criarContexto({ config, raiz, log, ...extra });
|
|
263
|
+
const provedor = f.server ? "remoto" : f.via || (cmd === "schedule" ? "buffer" : config.provedor && config.provedor !== "buffer" ? config.provedor : "local");
|
|
264
|
+
const arg = f._[0];
|
|
265
|
+
const pub = await criarPublicador({ config, provedor, ctx: provedor === "remoto" && injetado(extra) ? { ...ctx, praeko: injetado(extra) } : ctx });
|
|
266
|
+
if (f.status || f.cancel) {
|
|
267
|
+
if (!arg) throw new Error("diga o pacote: praeko schedule <name> --status");
|
|
268
|
+
const id = f.id || basename2(resolve2(arg));
|
|
269
|
+
if (f.status) {
|
|
270
|
+
for (const i of (await pub.status(id)).itens) log(`${NOME[i.rede].padEnd(10)} ${i.estado.padEnd(10)} ${i.quando ? local(i.quando) : ""} ${i.id}`);
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
const r2 = await pub.cancelar(id, { redes: f.networks ? redesDe(f.networks) : void 0 });
|
|
274
|
+
log(`\u2713 cancelados: ${r2.cancelados.join(", ") || "nenhum"}${r2.mantidos.length ? `; mantidos: ${r2.mantidos.join(", ")}` : ""}`);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
if (!arg && !f.meta) throw new Error(`uso: praeko ${cmd} <package-folder> --when "06/11 17:30" [--send]`);
|
|
278
|
+
const pacote = lerPacote({ arg, flags: f, raiz, config });
|
|
279
|
+
const padraoRedes = Array.isArray(config.redes) ? config.redes.map((x) => REDES[x]) : pacote.meta.formato === "longo" ? ["youtube", "instagram"] : ["youtube", "instagram", "tiktok"];
|
|
280
|
+
const redes = redesDe(f.networks, padraoRedes);
|
|
281
|
+
const fuso = config.fusoOffset || "-03:00", chaveRede = { youtube: "yt", instagram: "ig", tiktok: "tt" };
|
|
282
|
+
const destinos = redes.map((r2) => {
|
|
283
|
+
const q = f[chaveRede[r2]] || f.when;
|
|
284
|
+
const modo = r2 === "youtube" ? f["yt-mode"] : r2 === "tiktok" ? f["tt-mode"] : void 0;
|
|
285
|
+
const quando = q ? parseQuando(q, { fuso }) : null;
|
|
286
|
+
if (quando && Date.parse(quando) < Date.now() + 5 * 6e4 && provedor === "buffer") throw new Error(`${NOME[r2]}: ${local(quando)} \xE9 daqui a menos de 5 minutos ou j\xE1 passou`);
|
|
287
|
+
return { rede: r2, quando, ...modo ? { modo } : {}, ...f.queue && !quando ? { fila: true } : {}, ...f.draft ? { rascunho: true } : {} };
|
|
288
|
+
});
|
|
289
|
+
const { pedido, erros, avisos } = montarPedido({ pacote, config, destinos, opcoes: { lembrete: !!f.reminder, ...f["no-cover-in-frame"] ? { capaNoQuadro: false } : {}, ...f.url ? { videoUrl: f.url } : {} } });
|
|
290
|
+
if (provedor === "remoto" && destinos.some((d) => d.rede === "tiktok")) {
|
|
291
|
+
const o = { ...config.tiktok?.privacidade ? { privacidade: config.tiktok.privacidade } : {}, ...config.tiktok?.capaMs != null ? { miniaturaMs: config.tiktok.capaMs } : {}, ...f["tt-privacy"] ? { privacidade: f["tt-privacy"] } : {}, ...f["tt-frame"] ? { miniaturaMs: Number(f["tt-frame"]) } : {} };
|
|
292
|
+
if (Object.keys(o).length) pedido.opcoes.tiktok = o;
|
|
293
|
+
const direto = pedido.destinos.find((d) => d.rede === "tiktok")?.modo === "direto";
|
|
294
|
+
avisos.push(direto ? "TikTok direto: enquanto o app n\xE3o for auditado pelo TikTok, s\xF3 vale SELF_ONLY (privado) e s\xF3 contas de teste; p\xFAblico exige auditoria (docs/tiktok.md)" : "TikTok rascunho: o v\xEDdeo chega \xE0 caixa de entrada do app; legenda e capa voc\xEA ajusta no app (a API da caixa de entrada n\xE3o leva as duas)");
|
|
295
|
+
if (pacote.capa) avisos.push("TikTok: a capa em imagem n\xE3o \xE9 usada (o TikTok s\xF3 aceita um quadro do v\xEDdeo: --tt-frame <ms>)");
|
|
296
|
+
}
|
|
297
|
+
if (provedor === "remoto") {
|
|
298
|
+
pedido.id = String(pedido.id).replace(/[^A-Za-z0-9_-]+/g, "-").slice(0, 64);
|
|
299
|
+
if (f.tag) pedido.etiquetas = String(f.tag).split(",").map((x) => x.trim()).filter(Boolean);
|
|
300
|
+
}
|
|
301
|
+
erros.forEach((e) => log(" \u2717 " + e));
|
|
302
|
+
if (erros.length) return 1;
|
|
303
|
+
const errosPedido = validarPedido(pedido);
|
|
304
|
+
if (errosPedido.length) {
|
|
305
|
+
errosPedido.forEach((e) => log(" \u2717 " + e));
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
const enviar = !!f.send;
|
|
309
|
+
if (provedor === "remoto") await capaNosQuadrosInstagram({ pedido, pacote, f, raiz, enviar, log, avisos });
|
|
310
|
+
log(`${enviar ? "PUBLICANDO" : "SIMULA\xC7\xC3O (nada ser\xE1 enviado; use --send)"} [${provedor}]: ${pedido.titulo}`);
|
|
311
|
+
log(`pacote: ${pacote.dir}
|
|
312
|
+
v\xEDdeo: ${basename2(pacote.video)}${pacote.capa ? ` capa: ${basename2(pacote.capa)}` : " (sem capa)"}`);
|
|
313
|
+
for (const d of pedido.destinos) log(` ${NOME[d.rede].padEnd(10)} ${d.fila ? "pr\xF3xima vaga da fila" : d.rascunho ? "rascunho" : !d.quando || provedor === "local" && d.rede === "youtube" && (d.modo || config.youtube?.modo || "privado") !== "agendado" ? "agora" : local(d.quando)}${d.modo ? ` modo ${d.modo}` : ""} texto ${pedido.textos[d.rede].texto.length} caract.`);
|
|
314
|
+
if (provedor === "local") {
|
|
315
|
+
const todos = await registro_exports.carregarTodos();
|
|
316
|
+
for (const d of pedido.destinos) for (const l of await todos[d.rede]?.planejar?.({ item: { slug: pedido.id, rede: d.rede, quando: d.quando || (/* @__PURE__ */ new Date()).toISOString(), video: pacote.video, capa: pacote.capa, textos: pedido.textos[d.rede], opcoes: { ...config[d.rede] || {}, modo: d.modo || config[d.rede]?.modo }, titulo: pedido.titulo }, ctx }) || []) log(" " + l);
|
|
317
|
+
}
|
|
318
|
+
avisos.forEach((x) => log(" \u26A0 " + x));
|
|
319
|
+
(pacote.meta.antesDePublicar || []).forEach((x) => log(" \u2610 " + x));
|
|
320
|
+
if (!enviar) return 0;
|
|
321
|
+
const r = await pub.publicar(pedido);
|
|
322
|
+
for (const i of r.itens) log(`${i.estado === "erro" ? "\u2717" : "\u2713"} ${NOME[i.rede]} ${i.estado}${i.quando && i.estado === "agendado" ? " para " + local(i.quando) : ""}${i.url ? " " + i.url : ""}${i.studio ? " (" + i.studio + ")" : ""}${i.aviso ? " " + i.aviso : ""}`);
|
|
323
|
+
return r.itens.some((i) => i.estado === "erro") ? 1 : 0;
|
|
324
|
+
}
|
|
325
|
+
async function capaNosQuadrosInstagram({ pedido, pacote, f, raiz, enviar, log, avisos }) {
|
|
326
|
+
const d = pedido.destinos.find((x) => x.rede === "instagram");
|
|
327
|
+
if (!d || f["no-cover-in-frame"] || d.midia) return;
|
|
328
|
+
if (!pacote.capa) {
|
|
329
|
+
avisos.push("Instagram: sem capa, a miniatura ser\xE1 um quadro do v\xEDdeo");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (pedido.opcoes?.formato === "longo") {
|
|
333
|
+
avisos.push("Instagram: capa n\xE3o colada no v\xEDdeo longo (a capa 16:9 n\xE3o serve de miniatura de Reel)");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
let v, c;
|
|
337
|
+
try {
|
|
338
|
+
v = capa_quadro_exports.sondar(pacote.video);
|
|
339
|
+
c = capa_quadro_exports.dimensoesImagem(pacote.capa);
|
|
340
|
+
} catch (e) {
|
|
341
|
+
avisos.push(`Instagram: n\xE3o consegui conferir a capa (${e.message}); sem capa nos quadros`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (!capa_quadro_exports.mesmaProporcao(c, v)) {
|
|
345
|
+
avisos.push(`Instagram: a capa ${c.largura}x${c.altura} n\xE3o tem a propor\xE7\xE3o do v\xEDdeo ${v.largura}x${v.altura}; n\xE3o colei nos quadros`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!enviar) {
|
|
349
|
+
log(` Instagram: capa nos 2 primeiros quadros (${basename2(pacote.capa)}); a c\xF3pia \xE9 gerada no --send`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const dir = pastaEstado(raiz);
|
|
353
|
+
mkdirSync(dir, { recursive: true });
|
|
354
|
+
const saida = join2(dir, `${pedido.id}-instagram.mp4`);
|
|
355
|
+
capa_quadro_exports.embutirCapa({ video: pacote.video, capa: pacote.capa, saida });
|
|
356
|
+
d.midia = { video: saida };
|
|
357
|
+
log(` \u2713 capa colada nos 2 primeiros quadros do v\xEDdeo do Instagram: ${basename2(saida)}`);
|
|
358
|
+
}
|
|
359
|
+
async function fila(f, log, extra) {
|
|
360
|
+
const { raiz, config } = carregar(f), sub = f._[0] || "list", ctx = criarContexto({ config, raiz, log, ...extra });
|
|
361
|
+
if (sub === "list") {
|
|
362
|
+
const itens = lerFila(raiz).itens;
|
|
363
|
+
if (!itens.length) {
|
|
364
|
+
log("fila vazia");
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
for (const it of itens) log(`${it.id} ${it.estado.padEnd(10)} ${NOME[it.rede].padEnd(9)} ${it.quando ? local(it.quando) : "manual "} ${it.slug}${it.acao ? ` [${it.acao}${it.manual ? ", manual" : ""}]` : ""}${it.erro ? ` \u2717 ${it.erro.slice(0, 80)}` : ""}${it.resultado?.url ? " " + it.resultado.url : ""}`);
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
if (sub === "run") {
|
|
371
|
+
const r = await executar({ raiz, provedores: await registro_exports.carregarTodos(), ctx, simular: !!f["dry-run"], log });
|
|
372
|
+
if (!r.feitos.length && !f["dry-run"]) log("nada a fazer agora");
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
if (sub === "cancel") {
|
|
376
|
+
const it = cancelarItem(raiz, f._[1]);
|
|
377
|
+
log(`\u2713 cancelado ${it.id} (${NOME[it.rede]}, ${it.slug})`);
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
if (sub === "comment") {
|
|
381
|
+
const it = liberarItem(raiz, f._[1]);
|
|
382
|
+
await executar({ raiz, provedores: await registro_exports.carregarTodos(), ctx, soIds: [it.id], log });
|
|
383
|
+
const novo = lerFila(raiz).itens.find((x) => x.id === it.id);
|
|
384
|
+
log(novo.estado === "enviado" ? `\u2713 coment\xE1rio feito${novo.resultado?.lembrete ? " (" + novo.resultado.lembrete + ")" : ""}` : `\u2717 ${novo.erro}`);
|
|
385
|
+
return novo.estado === "enviado" ? 0 : 1;
|
|
386
|
+
}
|
|
387
|
+
const agentes = join2(homedir(), "Library", "LaunchAgents"), plist = join2(agentes, "br.praeko.fila.plist"), plistAntigo = join2(agentes, "br.arauto.fila.plist");
|
|
388
|
+
const pararAntigo = () => {
|
|
389
|
+
try {
|
|
390
|
+
execFileSync("launchctl", ["bootout", `gui/${process.getuid()}`, plistAntigo], { stdio: "ignore" });
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
rmSync(plistAntigo, { force: true });
|
|
394
|
+
};
|
|
395
|
+
if (sub === "install") {
|
|
396
|
+
mkdirSync(agentes, { recursive: true });
|
|
397
|
+
pararAntigo();
|
|
398
|
+
writeFileSync(plist, plistLaunchd({ node: process.execPath, praeko: BIN, cwd: raiz, minutos: +f.minutes || 5, log: caminhoEstado(raiz, "fila.log") }));
|
|
399
|
+
try {
|
|
400
|
+
execFileSync("launchctl", ["bootout", `gui/${process.getuid()}`, plist], { stdio: "ignore" });
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
execFileSync("launchctl", ["bootstrap", `gui/${process.getuid()}`, plist]);
|
|
404
|
+
log(`\u2713 executor instalado; roda a cada ${+f.minutes || 5} min com o Mac ligado. Log: .praeko/fila.log`);
|
|
405
|
+
return 0;
|
|
406
|
+
}
|
|
407
|
+
if (sub === "remove") {
|
|
408
|
+
try {
|
|
409
|
+
execFileSync("launchctl", ["bootout", `gui/${process.getuid()}`, plist], { stdio: "ignore" });
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
pararAntigo();
|
|
413
|
+
log("\u2713 executor removido");
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
throw new Error("uso: praeko queue [list|run [--dry-run]|cancel <id>|comment <id>|install|remove]");
|
|
417
|
+
}
|
|
418
|
+
async function conta(f, log, extra, resto = []) {
|
|
419
|
+
const rede = REDES[f._[0]], acao = f._[1] || "status";
|
|
420
|
+
if (!rede) throw new Error("uso: praeko account <youtube|instagram|tiktok> [connect|status]");
|
|
421
|
+
if (rede === "instagram" && acao === "send") {
|
|
422
|
+
const bruto = extra.segredos ? extra.segredos.obter("instagram/token") : segredos.obter("instagram/token");
|
|
423
|
+
if (!bruto) throw new Error("n\xE3o achei o token do Instagram no Chaveiro: rode antes `praeko account instagram connect --user-id <id>`");
|
|
424
|
+
const t = JSON.parse(bruto), a = clienteRemoto(f, carregarSeHouver(f).config, extra);
|
|
425
|
+
const r = await a.channels.sendInstagramToken({ token: t.token, userId: t.userId, username: t.username, type: t.tipo, host: t.host });
|
|
426
|
+
log(`\u2713 token da Meta enviado ao servidor: @${r.username} (${r.followers ?? "?"} seguidores, ${r.mediaItems ?? "?"} m\xEDdias)`);
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
if (acao === "connect" && (f.server || injetado(extra))) {
|
|
430
|
+
const meta = rede === "instagram" && f.meta, a = clienteRemoto(f, carregarSeHouver(f).config, extra);
|
|
431
|
+
if (meta && f.replace) log("ATEN\xC7\xC3O: --replace troca o token de publica\xE7\xE3o do Instagram por este login da Meta.");
|
|
432
|
+
const r = await a.channels.connect(meta ? "meta" : rede, meta ? { replace: !!f.replace, instagram: typeof f.instagram === "string" ? f.instagram : void 0, instagramId: typeof f["instagram-id"] === "string" ? f["instagram-id"] : void 0, probe: !!f.probe } : {});
|
|
433
|
+
log(`Abra este endere\xE7o no navegador e autorize:
|
|
434
|
+
${r.url}`);
|
|
435
|
+
if (!injetado(extra)) {
|
|
436
|
+
try {
|
|
437
|
+
spawn("open", [r.url], { stdio: "ignore", detached: true }).unref();
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
if (rede === "tiktok" && (acao === "status" || acao === "disconnect") && (f.server || injetado(extra))) return tiktokCmd({ ...f, _: [acao] }, log, extra);
|
|
444
|
+
let achado;
|
|
445
|
+
try {
|
|
446
|
+
achado = carregar(f);
|
|
447
|
+
} catch {
|
|
448
|
+
achado = { raiz: process.cwd(), config: normalizarConfig({}) };
|
|
449
|
+
}
|
|
450
|
+
const { raiz, config } = achado, ctx = criarContexto({ config, raiz, log, ...extra });
|
|
451
|
+
const prov = await registro_exports.carregar(rede);
|
|
452
|
+
if (acao === "connect") return await prov.conectar({ ctx, args: resto.slice(Math.max(resto.indexOf("connect"), resto.indexOf("conectar")) + 1) }) ?? 0;
|
|
453
|
+
const s = await prov.status({ ctx });
|
|
454
|
+
log(`${NOME[rede]}: ${s.conectado ? "conectado" : "N\xC3O conectado"}${s.detalhe ? ` (${s.detalhe})` : ""}`);
|
|
455
|
+
return s.conectado ? 0 : 1;
|
|
456
|
+
}
|
|
457
|
+
async function canais(f, log, extra) {
|
|
458
|
+
const { raiz, config } = carregar(f), ctx = criarContexto({ config, raiz, log, ...extra });
|
|
459
|
+
const pub = await criarPublicador({ config, provedor: f.via || "local", ctx });
|
|
460
|
+
for (const c of await pub.canais()) log(`${(NOME[c.rede] || c.rede).padEnd(10)} ${c.conectado ? "conectado " : "N\xC3O conectado"} ${c.id ? c.id + " " : ""}${c.detalhe || ""}`);
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
var carregarSeHouver = (f) => {
|
|
464
|
+
try {
|
|
465
|
+
return carregar(f);
|
|
466
|
+
} catch {
|
|
467
|
+
return { raiz: process.cwd(), config: {} };
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
function clienteRemoto(f, config, extra = {}) {
|
|
471
|
+
if (injetado(extra)) return injetado(extra);
|
|
472
|
+
const url = f.server || config?.remoto?.url, chave2 = process.env.PRAEKO_KEY || process.env.ARAUTO_CHAVE || segredos.obter("praeko/chave");
|
|
473
|
+
if (!url) throw new Error('falta o endere\xE7o do servi\xE7o: --server https://\u2026 (ou "remoto": { "url" } no praeko.json)');
|
|
474
|
+
if (!chave2) throw new Error("falta a chave do servi\xE7o: PRAEKO_KEY (ou ARAUTO_CHAVE) ou `echo $CHAVE | praeko chave guardar`");
|
|
475
|
+
return new Praeko({ url, key: chave2 });
|
|
476
|
+
}
|
|
477
|
+
var hora = (iso, fuso = "America/Sao_Paulo") => iso ? new Date(iso).toLocaleString("pt-BR", { timeZone: fuso, dateStyle: "short", timeStyle: "short" }) : "\u2014";
|
|
478
|
+
async function postsRemoto(f, log, extra) {
|
|
479
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra);
|
|
480
|
+
const r = await a.posts.list({ state: f.state, channel: f.channel ? REDES[f.channel] || f.channel : void 0, tag: f.tag, limit: f.limit });
|
|
481
|
+
if (!r.posts.length) {
|
|
482
|
+
log("nenhum post");
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
for (const p of r.posts) log(`${p.id.padEnd(28)} ${estadoPt(p.state).padEnd(9)} ${p.title.slice(0, 40).padEnd(40)} ${p.items.map((i) => `${i.network}:${estadoPt(i.state)}${i.scheduledFor ? " " + hora(i.scheduledFor) : ""}`).join(" \xB7 ")}${p.tags.length ? " #" + p.tags.join(" #") : ""}`);
|
|
486
|
+
log(`${r.posts.length} de ${r.total}`);
|
|
487
|
+
return 0;
|
|
488
|
+
}
|
|
489
|
+
async function calendarioRemoto(f, log, extra) {
|
|
490
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra), c = await a.calendar({ from: f.from, to: f.to, channel: f.channel ? REDES[f.channel] || f.channel : void 0, tag: f.tag });
|
|
491
|
+
const linhas = [...c.items.map((i) => ({ q: i.scheduledFor, t: `${NOME[i.network]} ${estadoPt(i.state)}${i.custom ? " (Custom)" : ""} ${i.title}` })), ...c.freeSlots.map((v) => ({ q: v.scheduledFor, t: `${NOME[v.network]} vaga livre` }))].sort((x, y) => x.q.localeCompare(y.q));
|
|
492
|
+
if (!linhas.length) {
|
|
493
|
+
log("nada no per\xEDodo");
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
for (const l of linhas) log(`${hora(l.q).padEnd(18)} ${l.t}`);
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
async function vagasRemoto(f, log, extra) {
|
|
500
|
+
const rede = REDES[f._[0]];
|
|
501
|
+
if (!rede) throw new Error("uso: praeko slots <youtube|instagram|tiktok> [fri 17:30 sun 22:00] [--timezone America/Sao_Paulo]");
|
|
502
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra), pares = f._.slice(1);
|
|
503
|
+
const dia = (d) => DIA[String(d).toLowerCase()] ?? d, mostra = (d) => DIA_PT[d] ?? d;
|
|
504
|
+
if (pares.length) {
|
|
505
|
+
if (pares.length % 2) throw new Error("as vagas v\xEAm em pares: dia hora (ex.: slots youtube fri 17:30 sun 22:00)");
|
|
506
|
+
const vagas = [];
|
|
507
|
+
for (let i = 0; i < pares.length; i += 2) vagas.push(`${dia(pares[i])} ${pares[i + 1]}`);
|
|
508
|
+
const r2 = await a.channels.setSlots(rede, vagas, { timezone: f.timezone });
|
|
509
|
+
log(`\u2713 vagas de ${NOME[rede]} (${r2.timezone}): ${r2.slots.map((v) => `${mostra(v.day)} ${v.time}`).join(", ") || "nenhuma"}${r2.rescheduled.length ? `; ${r2.rescheduled.length} item(ns) da fila reorganizado(s)` : ""}`);
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
const r = await a.channels.slots(rede);
|
|
513
|
+
log(`${NOME[rede]} (${r.timezone}): ${r.slots.map((v) => `${mostra(v.day)} ${v.time}`).join(", ") || "sem vagas"}`);
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
516
|
+
async function publicarAgoraRemoto(f, log, extra) {
|
|
517
|
+
if (!f._[0]) throw new Error("uso: praeko publish-now <id> [--networks yt]");
|
|
518
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra), r = await a.posts.publishNow(f._[0], { network: f.networks ? REDES[f.networks] : void 0, ...f["idempotency-key"] ? { idempotencyKey: f["idempotency-key"] } : {} });
|
|
519
|
+
for (const i of r.items) log(`${NOME[i.network].padEnd(10)} ${estadoPt(i.state)}`);
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
async function usoCmd(f, log, extra) {
|
|
523
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra);
|
|
524
|
+
const num = (x) => x == null ? "\u2014" : Number(x).toLocaleString("pt-BR");
|
|
525
|
+
if (f._[0] === "log") {
|
|
526
|
+
const r2 = await a.usage.log({ key: typeof f["key-id"] === "string" ? f["key-id"] : void 0, status: typeof f.code === "string" ? f.code : void 0, route: f.route, method: f.method, days: f.days, limit: f.limit, cursor: f.cursor });
|
|
527
|
+
if (f.json) {
|
|
528
|
+
log(JSON.stringify(r2, null, 2));
|
|
529
|
+
return 0;
|
|
530
|
+
}
|
|
531
|
+
if (!r2.records.length) {
|
|
532
|
+
log("(nenhuma chamada com esses filtros)");
|
|
533
|
+
return 0;
|
|
534
|
+
}
|
|
535
|
+
for (const x of r2.records) log(`${local(x.createdAt)} ${x.method.padEnd(6)} ${String(x.status).padEnd(3)} ${String(x.durationMs).padStart(5)} ms ${x.route.padEnd(36)} ${x.keyName}`);
|
|
536
|
+
if (r2.next) log(`(mais: --cursor ${r2.next})`);
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
if (f._[0] !== void 0) throw new Error(`subcomando desconhecido: usage ${f._[0]} (use: usage | usage log)`);
|
|
540
|
+
const r = await a.usage.get({ days: f.days });
|
|
541
|
+
if (f.json) {
|
|
542
|
+
log(JSON.stringify(r, null, 2));
|
|
543
|
+
return 0;
|
|
544
|
+
}
|
|
545
|
+
const pct = r.total ? `${String(Math.round(r.succeeded / r.total * 1e3) / 10).replace(".", ",")}%` : "\u2014";
|
|
546
|
+
log(`Uso da API \xB7 ${r.days === 1 ? "\xFAltimas 24 horas" : `\xFAltimos ${r.days} dias`}`);
|
|
547
|
+
log(` chamadas ${num(r.total)} \xB7 sucesso ${pct} \xB7 erros ${num(r.errors)} \xB7 p95 ${r.p95Ms == null ? "\u2014" : r.p95Ms + " ms"} \xB7 pico ${num(r.peakPerMinute)}/min (limite ${num(r.limitPerMinute)}/min)`);
|
|
548
|
+
for (const d of r.byDay) if (d.total) log(` ${d.day} ${String(d.total).padStart(6)} chamadas${d.errors ? ` \xB7 ${d.errors} erros` : ""}`);
|
|
549
|
+
if (r.byKey.length) {
|
|
550
|
+
log("Por chave:");
|
|
551
|
+
for (const k of r.byKey) log(` ${k.name.padEnd(24)} ${String(k.total).padStart(6)} chamadas${k.errors ? ` \xB7 ${k.errors} erros` : ""} \xB7 \xFAltima ${local(k.lastAt)}`);
|
|
552
|
+
}
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
async function webhookCmd(f, log, extra) {
|
|
556
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra), [sub = "list", id, outro] = f._;
|
|
557
|
+
const linha = (w) => `${w.id} ${w.active ? "ativo " : "pausado"} ${w.url} [${w.events.join(", ")}]${w.disabledReason ? ` (${w.disabledReason})` : ""}`;
|
|
558
|
+
const entrega = (e) => `${e.createdAt ? local(e.createdAt) : ""} ${e.event.padEnd(19)} ${e.state.padEnd(9)} ${e.lastStatus ?? "\u2014"} ${e.attempts}x${e.lastError ? ` ${e.lastError}` : ""} ${e.id}`;
|
|
559
|
+
const precisa = () => {
|
|
560
|
+
if (!id) throw new Error(`diga o webhook: praeko webhook ${sub} <id>`);
|
|
561
|
+
return id;
|
|
562
|
+
};
|
|
563
|
+
if (sub === "list") {
|
|
564
|
+
const r = await a.webhooks.list();
|
|
565
|
+
if (!r.webhooks.length) log("(nenhum webhook)");
|
|
566
|
+
for (const w of r.webhooks) log(linha(w));
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
if (sub === "create") {
|
|
570
|
+
if (typeof f.url !== "string") throw new Error("falta --url https://\u2026");
|
|
571
|
+
const w = await a.webhooks.create({ url: f.url, events: typeof f.events === "string" ? f.events.split(",").map((x) => x.trim()).filter(Boolean) : void 0, ...f["idempotency-key"] ? { idempotencyKey: f["idempotency-key"] } : {} });
|
|
572
|
+
log(`\u2713 ${linha(w)}`);
|
|
573
|
+
log(`Segredo de assinatura (aparece s\xF3 agora): ${w.secret}`);
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
if (sub === "rotate-secret") {
|
|
577
|
+
const r = await a.webhooks.rotateSecret(precisa());
|
|
578
|
+
log(`\u2713 novo segredo (aparece s\xF3 agora; o anterior deixou de valer): ${r.secret}`);
|
|
579
|
+
return 0;
|
|
580
|
+
}
|
|
581
|
+
if (sub === "test") {
|
|
582
|
+
const r = await a.webhooks.test(precisa());
|
|
583
|
+
log(entrega(r.delivery));
|
|
584
|
+
return r.delivery.state === "delivered" ? 0 : 1;
|
|
585
|
+
}
|
|
586
|
+
if (sub === "deliveries") {
|
|
587
|
+
const r = await a.webhooks.deliveries(precisa(), { limit: f.limit });
|
|
588
|
+
if (!r.deliveries.length) log("(nenhuma entrega)");
|
|
589
|
+
for (const e of r.deliveries) log(entrega(e));
|
|
590
|
+
return 0;
|
|
591
|
+
}
|
|
592
|
+
if (sub === "retry") {
|
|
593
|
+
if (!outro) throw new Error("diga a entrega: praeko webhook retry <id> <entrega>");
|
|
594
|
+
const r = await a.webhooks.redeliver(precisa(), outro);
|
|
595
|
+
log(entrega(r.delivery));
|
|
596
|
+
return r.delivery.state === "delivered" ? 0 : 1;
|
|
597
|
+
}
|
|
598
|
+
if (sub === "pause" || sub === "resume") {
|
|
599
|
+
const w = await a.webhooks.update(precisa(), { active: sub === "resume" });
|
|
600
|
+
log(`\u2713 ${linha(w)}`);
|
|
601
|
+
return 0;
|
|
602
|
+
}
|
|
603
|
+
if (sub === "events") {
|
|
604
|
+
if (!outro) throw new Error("diga os eventos: praeko webhook events <id> post.sent,post.failed");
|
|
605
|
+
const w = await a.webhooks.update(precisa(), { events: outro.split(",").map((x) => x.trim()).filter(Boolean) });
|
|
606
|
+
log(`\u2713 ${linha(w)}`);
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
if (sub === "remove") {
|
|
610
|
+
await a.webhooks.remove(precisa());
|
|
611
|
+
log(`\u2713 webhook ${id} apagado`);
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
throw new Error(`subcomando desconhecido: webhook ${sub}`);
|
|
615
|
+
}
|
|
616
|
+
var curto = (t, n = 110) => {
|
|
617
|
+
const x = String(t ?? "").replace(/\s+/g, " ").trim();
|
|
618
|
+
return x.length > n ? x.slice(0, n - 1) + "\u2026" : x;
|
|
619
|
+
};
|
|
620
|
+
async function insightsCmd(f, log, extra) {
|
|
621
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra), dias = Math.max(1, Number(f.days) || 30), ate = Date.now();
|
|
622
|
+
const canal = f.channel ? REDES[f.channel] || f.channel : void 0;
|
|
623
|
+
const r = await a.insights.get({ from: new Date(ate - dias * 864e5).toISOString(), to: new Date(ate).toISOString(), channel: canal, tag: f.tag, type: f.type, compare: !!(f.compare || f.posts), sort: f.sort, direction: f.direction, limit: f.limit });
|
|
624
|
+
if (f.json) {
|
|
625
|
+
log(JSON.stringify(r, null, 2));
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
if (f._[0] === "48h") {
|
|
629
|
+
log(r.reading48hText);
|
|
630
|
+
if (!r.reading48h.length) log(`(nenhum post publicado h\xE1 48 h ou mais nos \xFAltimos ${dias} dias)`);
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
const k = r.cards, n = (x) => x == null ? "\u2014" : String(x), pct = (x) => x == null ? "\u2014" : `${String(x).replace(".", ",")}%`;
|
|
634
|
+
log(`\xDAltimos ${dias} dias${canal ? ` \xB7 ${NOME[canal] || canal}` : ""}${f.tag ? ` \xB7 etiqueta ${f.tag}` : ""}`);
|
|
635
|
+
log(` posts ${n(k.posts)} \xB7 views ${n(k.views)} \xB7 rea\xE7\xF5es ${n(k.reactions)} \xB7 coment\xE1rios ${n(k.comments)} \xB7 engajamento ${pct(k.engagement)} \xB7 seguidores ${n(k.followers)}${k.followersChange != null ? ` (${k.followersChange > 0 ? "+" : ""}${k.followersChange})` : ""}`);
|
|
636
|
+
const maisCartoes = [["shares", "compart."], ["saves", "salvos"], ["reach", "alcance"], ["watchTimeMin", "min assistidos"], ["avgWatchTimeSec", "s m\xE9dios"], ["followersGained", "seguidores ganhos"]].filter(([x]) => k[x] != null).map(([x, t]) => `${t} ${k[x]}${k.change?.[x]?.pct != null ? ` (${k.change[x].pct > 0 ? "+" : ""}${String(k.change[x].pct).replace(".", ",")}%)` : ""}`);
|
|
637
|
+
if (maisCartoes.length) log(` ${maisCartoes.join(" \xB7 ")}`);
|
|
638
|
+
if (r.sources?.bufferAggregate) log(" (n\xFAmeros do agregado do Buffer: " + Object.keys(r.sources.current).join(", ") + ")");
|
|
639
|
+
if (f.posts && r.posts?.items?.length) {
|
|
640
|
+
log(`Posts do per\xEDodo (${r.posts.total}, ordem ${r.posts.sort} ${r.posts.direction}):`);
|
|
641
|
+
r.posts.items.forEach((p, i) => log(` ${String(i + 1).padStart(2)}. ${curto(p.title, 55)} [${NOME[p.network] || p.network}${p.type ? ` \xB7 ${p.type}` : ""}] ${p.reactions} rea\xE7\xF5es \xB7 ${p.comments} coment. \xB7 ${p.views} views \xB7 ${p.engagement == null ? "\u2014" : String(p.engagement).replace(".", ",") + "%"}`));
|
|
642
|
+
}
|
|
643
|
+
if (r.topPosts.length) {
|
|
644
|
+
log("Top posts (rea\xE7\xF5es):");
|
|
645
|
+
r.topPosts.forEach((p, i) => log(` ${i + 1}. ${curto(p.title, 60)} [${NOME[p.network] || p.network}] ${p.reactions} rea\xE7\xF5es \xB7 ${p.comments} coment. \xB7 ${p.views} views${p.noMetrics ? " (sem m\xE9tricas ainda)" : ""}`));
|
|
646
|
+
}
|
|
647
|
+
for (const c of r.channels) log(` ${(NOME[c.network] || c.network).padEnd(10)} posts ${String(c.posts).padStart(3)} \xB7 rea\xE7\xF5es ${String(c.reactions).padStart(5)} \xB7 coment. ${String(c.comments).padStart(4)} \xB7 engajamento ${pct(c.engagement)}`);
|
|
648
|
+
for (const x of r.summary) log(`\u2022 ${x.title}: ${x.text}`);
|
|
649
|
+
if (r.collection.collecting) log("(a coleta de m\xE9tricas ainda n\xE3o rodou para estes posts: 1 h, 6 h, 24 h, 48 h e 7 dias depois de cada publica\xE7\xE3o)");
|
|
650
|
+
return 0;
|
|
651
|
+
}
|
|
652
|
+
async function importarCmd(f, log, extra) {
|
|
653
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra), rede = f._[0] ? REDES[f._[0]] || f._[0] : "all";
|
|
654
|
+
const r = await a.insights.import({ network: rede, dryRun: !!f["dry-run"] });
|
|
655
|
+
for (const x of r.imported) {
|
|
656
|
+
if (x.error) log(`${NOME[x.network]}: \u2717 ${x.error}`);
|
|
657
|
+
else if (x.skipped) log(`${NOME[x.network]}: ignorado (${x.reason})`);
|
|
658
|
+
else log(`${NOME[x.network]}${x.simulated ? " (simula\xE7\xE3o)" : ""}: ${x.found} encontrados \xB7 ${x.created} novos \xB7 ${x.updated} atualizados \xB7 ${x.views} views \xB7 ${x.likes} rea\xE7\xF5es \xB7 ${x.comments} coment\xE1rios`);
|
|
659
|
+
}
|
|
660
|
+
return r.imported.some((x) => x.error) ? 1 : 0;
|
|
661
|
+
}
|
|
662
|
+
async function tiktokCmd(f, log, extra) {
|
|
663
|
+
const sub = f._[0], a = clienteRemoto(f, carregarSeHouver(f).config, extra);
|
|
664
|
+
if (sub === "import") return importarCmd({ ...f, _: ["tiktok"] }, log, extra);
|
|
665
|
+
if (sub === "videos") return conteudoCmd({ ...f, channel: "tiktok" }, log, extra);
|
|
666
|
+
if (sub === "connect") {
|
|
667
|
+
const r = await a.channels.connect("tiktok");
|
|
668
|
+
log(`Abra este endere\xE7o no navegador e autorize:
|
|
669
|
+
${r.url}`);
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
if (sub === "disconnect") {
|
|
673
|
+
const r = await a.channels.disconnect("tiktok");
|
|
674
|
+
log(`\u2713 TikTok desconectado${r.revokedAtTiktok ? " (acesso revogado no TikTok)" : ""}`);
|
|
675
|
+
return 0;
|
|
676
|
+
}
|
|
677
|
+
if (sub === "status") {
|
|
678
|
+
const c = (await a.channels.list({ verify: true })).channels.find((x) => x.network === "tiktok");
|
|
679
|
+
log(`TikTok: ${c?.connected ? "conectado" : "N\xC3O conectado"}${c?.detail ? ` (${c.detail})` : ""}`);
|
|
680
|
+
if (c?.connected) log(" rascunho: o v\xEDdeo vai para a caixa de entrada do app \xB7 direto: privado (SELF_ONLY) at\xE9 o app ser auditado (docs/tiktok.md)");
|
|
681
|
+
return c?.connected ? 0 : 1;
|
|
682
|
+
}
|
|
683
|
+
throw new Error("uso: praeko tiktok import [--dry-run] | videos [--limit 30] | status | connect | disconnect");
|
|
684
|
+
}
|
|
685
|
+
async function seguidoresCmd(f, log, extra) {
|
|
686
|
+
const rede = REDES[f._[0]], a = clienteRemoto(f, carregarSeHouver(f).config, extra);
|
|
687
|
+
if (!rede) throw new Error("uso: praeko followers <instagram|tiktok> [<value>] [--day YYYY-MM-DD]");
|
|
688
|
+
if (f._[1] === void 0) {
|
|
689
|
+
for (const x of (await a.channels.followers(rede)).records.slice(0, 15)) log(`${x.day} ${String(x.followers).padStart(7)} ${x.source}`);
|
|
690
|
+
return 0;
|
|
691
|
+
}
|
|
692
|
+
const valor = Number(f._[1]);
|
|
693
|
+
if (!Number.isInteger(valor)) throw new Error("o valor precisa ser um n\xFAmero inteiro");
|
|
694
|
+
const r = await a.channels.setFollowers(rede, valor, { day: f.day });
|
|
695
|
+
log(`\u2713 ${NOME[rede]}: ${r.followers} seguidores em ${r.day} (manual)`);
|
|
696
|
+
return 0;
|
|
697
|
+
}
|
|
698
|
+
async function conteudoCmd(f, log, extra) {
|
|
699
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra), r = await a.content.list({ network: f.channel ? REDES[f.channel] || f.channel : void 0, limit: f.limit || 30, sort: f.sort });
|
|
700
|
+
for (const i of r.items) log(`${hora(i.publishedAt).padEnd(15)} ${(NOME[i.network] || i.network).padEnd(9)} ${String(i.views ?? "\u2014").padStart(6)} views ${String(i.reactions ?? "\u2014").padStart(4)} rea\xE7\xF5es ${curto(i.title, 70)}`);
|
|
701
|
+
log(`(${r.items.length} de ${r.total})`);
|
|
702
|
+
return 0;
|
|
703
|
+
}
|
|
704
|
+
async function youtubeCmd(f, log, extra) {
|
|
705
|
+
const sub = f._[0], a = clienteRemoto(f, carregarSeHouver(f).config, extra);
|
|
706
|
+
if (sub === "videos") {
|
|
707
|
+
const r = await a.youtube.listVideos({ max: f.max, page: f.page });
|
|
708
|
+
for (const v of r.videos) log(`${v.id} ${hora(v.publishedAt).padEnd(15)} ${(v.privacy + (v.publishAt ? " \u2192 " + hora(v.publishAt) : "")).padEnd(30)} ${String(v.viewCount ?? "\u2014").padStart(6)} views ${curto(v.title, 70)}`);
|
|
709
|
+
if (r.next) log(`(mais: --page ${r.next})`);
|
|
710
|
+
return 0;
|
|
711
|
+
}
|
|
712
|
+
if (sub === "video") {
|
|
713
|
+
if (!f._[1]) throw new Error("uso: praeko youtube video <id>");
|
|
714
|
+
const { video: v } = await a.youtube.getVideo(f._[1]);
|
|
715
|
+
log(`${v.title}
|
|
716
|
+
${v.url} ${v.privacy}${v.publishAt ? " \u2192 " + hora(v.publishAt) : ""} publicado ${hora(v.publishedAt)}
|
|
717
|
+
${v.viewCount ?? "\u2014"} views \xB7 ${v.likes ?? "\u2014"} curtidas \xB7 ${v.comments ?? "\u2014"} coment\xE1rios
|
|
718
|
+
|
|
719
|
+
${v.description}`);
|
|
720
|
+
return 0;
|
|
721
|
+
}
|
|
722
|
+
if (sub === "edit") {
|
|
723
|
+
const id = f._[1];
|
|
724
|
+
if (!id) throw new Error('uso: praeko youtube edit <id> [--title T] [--description-file f.txt] [--swap-hashtags "#a #b"] [--send]');
|
|
725
|
+
const m = {};
|
|
726
|
+
if (f.title) m.title = f.title;
|
|
727
|
+
if (f["description-file"]) m.description = readFileSync2(resolve2(f["description-file"]), "utf8").replace(/\s+$/, "");
|
|
728
|
+
if (f["swap-hashtags"]) {
|
|
729
|
+
const { video: v } = await a.youtube.getVideo(id);
|
|
730
|
+
m.description = trocarHashtags(m.description ?? v.description, f["swap-hashtags"]);
|
|
731
|
+
}
|
|
732
|
+
if (!Object.keys(m).length) throw new Error("nada para mudar: use --title, --description-file ou --swap-hashtags");
|
|
733
|
+
const r = await a.youtube.editVideo(id, { ...m, dryRun: !f.send });
|
|
734
|
+
log(`${f.send ? "EDITANDO" : "SIMULA\xC7\xC3O (use --send)"}: ${id}`);
|
|
735
|
+
if (r.before.title !== r.after.title) log(` t\xEDtulo: ${r.before.title}
|
|
736
|
+
\u2192 ${r.after.title}`);
|
|
737
|
+
if (r.before.description !== r.after.description) {
|
|
738
|
+
const u = (d) => String(d || "").split("\n").filter((x) => x.trim()).slice(-2).join(" | ");
|
|
739
|
+
log(` descri\xE7\xE3o (final): ${curto(u(r.before.description), 140)}
|
|
740
|
+
\u2192 ${curto(u(r.after.description), 140)}`);
|
|
741
|
+
}
|
|
742
|
+
if (!r.changed) log(" (nada muda)");
|
|
743
|
+
else if (r.written) log("\u2713 escrito no YouTube");
|
|
744
|
+
return 0;
|
|
745
|
+
}
|
|
746
|
+
if (sub === "comments") {
|
|
747
|
+
const r = await a.youtube.comments({ video: f.video, unanswered: !!f.unanswered, max: f.max, page: f.page });
|
|
748
|
+
if (!r.comments.length) log("nenhum coment\xE1rio" + (f.unanswered ? " sem resposta" : ""));
|
|
749
|
+
for (const c of r.comments) log(`${c.id} ${hora(c.publishedAt).padEnd(15)} ${c.videoId ?? ""} ${c.fromOwner ? "(voc\xEA)" : c.answered ? "respondido" : "SEM RESPOSTA"} ${c.author}: ${curto(c.text)}`);
|
|
750
|
+
if (r.next) log(`(mais: --page ${r.next})`);
|
|
751
|
+
return 0;
|
|
752
|
+
}
|
|
753
|
+
if (sub === "resolve" || sub === "reopen") {
|
|
754
|
+
const id = f._[1];
|
|
755
|
+
if (!id) throw new Error(`uso: praeko youtube ${sub} <thread>`);
|
|
756
|
+
await (sub === "resolve" ? a.youtube.resolve(id) : a.youtube.reopen(id));
|
|
757
|
+
log(`\u2713 ${sub === "resolve" ? "resolvido" : "reaberto"}: ${id}`);
|
|
758
|
+
return 0;
|
|
759
|
+
}
|
|
760
|
+
if (sub === "reply") {
|
|
761
|
+
const id = f._[1];
|
|
762
|
+
if (!id || !f.text) throw new Error('uso: praeko youtube reply <thread> --text "\u2026" [--send]');
|
|
763
|
+
if (!f.send) {
|
|
764
|
+
log(`SIMULA\xC7\xC3O (use --send): responderia ao coment\xE1rio ${id} com:
|
|
765
|
+
${f.text}`);
|
|
766
|
+
return 0;
|
|
767
|
+
}
|
|
768
|
+
const r = await a.youtube.reply(id, f.text);
|
|
769
|
+
log(`\u2713 respondido (${r.id})`);
|
|
770
|
+
return 0;
|
|
771
|
+
}
|
|
772
|
+
throw new Error("uso: praeko youtube videos | video <id> | edit <id> \u2026 | comments \u2026 | resolve|reopen <thread> | reply <thread> --text \u2026");
|
|
773
|
+
}
|
|
774
|
+
async function adotarBufferRemoto(f, log, extra) {
|
|
775
|
+
if (!f.title || !f.when || !f["buffer-id"]) throw new Error('uso: praeko adopt-buffer --id <name> --title "\u2026" --when "07/11 10:00" --buffer-id <Buffer post id> [--text "caption"] [--cover-media <id>] (registra no painel um Reel que j\xE1 est\xE1 agendado no Buffer; n\xE3o chama o Buffer)');
|
|
776
|
+
const quando = parseQuando(f.when, { fuso: carregarSeHouver(f).config?.fusoOffset || "-03:00" });
|
|
777
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra);
|
|
778
|
+
const p = await a.posts.adopt({ ...f.id ? { id: f.id } : {}, title: f.title, network: "instagram", scheduledFor: quando, external: { provider: "buffer", id: f["buffer-id"] }, ...f.text ? { texts: { instagram: { text: f.text } } } : {}, ...f["cover-media"] ? { media: { cover: f["cover-media"] } } : {} });
|
|
779
|
+
log(`\u2713 adotado: ${p.id} (Instagram, ${estadoPt(p.items[0]?.state)}, ${p.items[0]?.scheduledFor})`);
|
|
780
|
+
return 0;
|
|
781
|
+
}
|
|
782
|
+
async function youtubeAgendarRemoto(f, log, extra) {
|
|
783
|
+
if (!f._[0] || !f.when) throw new Error('uso: praeko youtube-schedule <post> --when "07/11 10:00" (o v\xEDdeo j\xE1 est\xE1 no YouTube, privado)');
|
|
784
|
+
const quando = parseQuando(f.when, { fuso: carregarSeHouver(f).config?.fusoOffset || "-03:00" });
|
|
785
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra), r = await a.posts.scheduleYoutube(f._[0], quando);
|
|
786
|
+
log(`\u2713 ${r.url} agendado para ${new Date(r.publishAt).toLocaleString("pt-BR", { timeZone: "America/Sao_Paulo", dateStyle: "short", timeStyle: "short" })} (privado at\xE9 l\xE1)`);
|
|
787
|
+
return 0;
|
|
788
|
+
}
|
|
789
|
+
async function chave(f, log, extra) {
|
|
790
|
+
const sub = f._[0];
|
|
791
|
+
if (sub === "store") {
|
|
792
|
+
let k = "";
|
|
793
|
+
for await (const c of process.stdin) k += c;
|
|
794
|
+
k = k.trim();
|
|
795
|
+
if (!FORMATO_CHAVE.test(k)) throw new Error("a chave do servi\xE7o come\xE7a com praeko_ (ou arauto_, as chaves antigas) (uso: echo $KEY | praeko key store)");
|
|
796
|
+
log(`chave guardada (${segredos.guardar("praeko/chave", k)})`);
|
|
797
|
+
return 0;
|
|
798
|
+
}
|
|
799
|
+
if (sub === "migrate") {
|
|
800
|
+
const r = migrar(extra.segredos ?? segredos);
|
|
801
|
+
for (const ref of r.copiados) log(`\u2713 ${ref}: copiado para o Chaveiro novo (servi\xE7o praeko)`);
|
|
802
|
+
for (const ref of r.jaNoNovo) log(`\u2022 ${ref}: j\xE1 existe no novo; nada a copiar`);
|
|
803
|
+
for (const ref of r.ausentes) log(`\xB7 ${ref}: n\xE3o havia no antigo`);
|
|
804
|
+
log(r.copiados.length ? `${r.copiados.length} item(ns) copiado(s). Os antigos continuam onde estavam (apague-os \xE0 m\xE3o quando quiser).` : "nada a copiar.");
|
|
805
|
+
return 0;
|
|
806
|
+
}
|
|
807
|
+
if (sub === "create" && f.sql) {
|
|
808
|
+
if (!f.name) throw new Error("diga o nome: praeko key create --name admin --scope admin --sql");
|
|
809
|
+
const escopo = f.scope || "admin", k = gerarChave(), id = idChave(), h = await hashChave(k);
|
|
810
|
+
if (!["read", "write", "admin"].includes(escopo)) throw new Error("scope: read, write or admin");
|
|
811
|
+
const escopoBanco = ESCOPO_PT[escopo] ?? escopo;
|
|
812
|
+
const nomeSql = String(f.name).replace(/'/g, "''");
|
|
813
|
+
log(`Chave (guarde agora, ela n\xE3o aparece de novo):
|
|
814
|
+
${k}
|
|
815
|
+
|
|
816
|
+
Execute no D1 (veja docs/servidor.md; use --local para o wrangler dev):
|
|
817
|
+
npx wrangler d1 execute arauto --remote --command "INSERT INTO chaves_api (id, nome, hash, prefixo, escopo, criado_em) VALUES ('${id}', '${nomeSql}', '${h}', '${prefixoChave(k)}', '${escopoBanco}', '${(/* @__PURE__ */ new Date()).toISOString()}')"`);
|
|
818
|
+
return 0;
|
|
819
|
+
}
|
|
820
|
+
const a = clienteRemoto(f, carregarSeHouver(f).config, extra);
|
|
821
|
+
if (sub === "create") {
|
|
822
|
+
if (!f.name) throw new Error("diga o nome: praeko key create --name bardo [--scope write]");
|
|
823
|
+
const r = await a.apiKeys.create(f.name, f.scope || "write");
|
|
824
|
+
log(`\u2713 chave ${r.id} (${r.scope}) criada. Guarde agora, ela n\xE3o aparece de novo:
|
|
825
|
+
${r.key}`);
|
|
826
|
+
return 0;
|
|
827
|
+
}
|
|
828
|
+
if (sub === "list") {
|
|
829
|
+
for (const k of (await a.apiKeys.list()).keys) log(`${k.id} ${k.scope.padEnd(8)} ${k.name.padEnd(16)} ${k.prefix}\u2026 ${k.revoked ? "REVOGADA" : k.lastUsedAt ? "\xFAltimo uso " + hora(k.lastUsedAt) : "nunca usada"}`);
|
|
830
|
+
return 0;
|
|
831
|
+
}
|
|
832
|
+
if (sub === "revoke") {
|
|
833
|
+
if (!f._[1]) throw new Error("uso: praeko key revoke <id>");
|
|
834
|
+
await a.apiKeys.revoke(f._[1]);
|
|
835
|
+
log(`\u2713 chave ${f._[1]} revogada`);
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
throw new Error("uso: praeko key store | create --name N [--scope S] [--sql] | list | revoke <id> | migrate");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/main.ts
|
|
842
|
+
var args = process.argv.slice(2);
|
|
843
|
+
if (args[0] === "studio") {
|
|
844
|
+
let bin;
|
|
845
|
+
try {
|
|
846
|
+
bin = join3(dirname(createRequire(import.meta.url).resolve("@praeko/studio-cli/package.json")), "bin", "bardo.mjs");
|
|
847
|
+
} catch {
|
|
848
|
+
console.error("praeko studio: o Praeko Studio ainda n\xE3o \xE9 distribu\xEDdo pelo npm (s\xF3 no reposit\xF3rio do Praeko). Os demais comandos funcionam normalmente.");
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
const r = spawnSync(process.execPath, [bin, ...args.slice(1)], { stdio: "inherit", env: { ...process.env, PRAEKO_STUDIO: "1" } });
|
|
852
|
+
process.exit(r.status ?? 1);
|
|
853
|
+
}
|
|
854
|
+
process.exit(await executarCli(args));
|