From cef8d920b91fc84eb3fb81e2ffc9a2e8ff29619c Mon Sep 17 00:00:00 2001 From: Yasuaki Uechi Date: Wed, 25 Aug 2021 23:11:36 +0900 Subject: [PATCH] feat!: homebrew sendMessage params --- src/base.ts | 2 +- src/index.ts | 4 +- src/protobuf.ts | 22 ++++++--- src/services/chat/index.ts | 30 ++++++++----- src/services/context/index.ts | 10 ++--- src/services/message/index.ts | 9 +++- tests/message.test.ts | 85 ++++++++++++++++++++--------------- 7 files changed, 102 insertions(+), 60 deletions(-) diff --git a/src/base.ts b/src/base.ts index 628c842f..ea220ce2 100644 --- a/src/base.ts +++ b/src/base.ts @@ -18,7 +18,7 @@ export class Base { public metadata!: Metadata; public continuation!: ReloadContinuationItems; protected isReplay!: boolean; - protected liveChatContext!: LiveChatContext; + // protected liveChatContext!: LiveChatContext; protected get(input: string, init?: RequestInit) { if (!input.startsWith("http")) { diff --git a/src/index.ts b/src/index.ts index 33e4573a..4cd99d5c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,7 @@ export class Masterchat { const videoId = normalizeVideoId(videoIdOrUrl); const mc = new Masterchat(videoId, options); await mc.populateMetadata(); - if (options.credentials) await mc.populateLiveChatContext(); + // if (options.credentials) await mc.populateLiveChatContext(); return mc; } @@ -33,7 +33,7 @@ export class Masterchat { const videoId = normalizeVideoId(videoIdOrUrl); this.videoId = videoId; await this.populateMetadata(); - if (this.credentials) await this.populateLiveChatContext(); + // if (this.credentials) await this.populateLiveChatContext(); } private constructor( diff --git a/src/protobuf.ts b/src/protobuf.ts index 972ecd55..2245d709 100644 --- a/src/protobuf.ts +++ b/src/protobuf.ts @@ -1,3 +1,9 @@ +function encodeWeirdB64(payload: Buffer) { + return Buffer.from(encodeURIComponent(payload.toString("base64"))).toString( + "base64" + ); +} + /** ``` 0a = 00001 010 field=1 wire=2 length-delimited @@ -11,13 +17,18 @@ 0b = 11bytes (video id) 10 = 00010 000 field=2 wire=0 - 02 = decimal=2 (unknown enum) + decimal, 1 if other's chat, 2 if own chat 18 = 00011 000 field=3 wire=0 04 = decimal=4 (unknown enum) ``` */ -export function generateSendMessageParams(channelId: string, videoId: string) { - return Buffer.from([ +export function generateSendMessageParams( + channelId: string, + videoId: string, + magic1: number = 1, + magic2: number = 4 +) { + const buf = Buffer.from([ ...lenDelim( 1, lenDelim( @@ -28,9 +39,10 @@ export function generateSendMessageParams(channelId: string, videoId: string) { ]) ) ), - ...variant(2, 2), - ...variant(3, 4), + ...variant(2, magic1), + ...variant(3, magic2), ]); + return encodeWeirdB64(buf); } function lenDelim(fieldId: number, payload: Buffer) { diff --git a/src/services/chat/index.ts b/src/services/chat/index.ts index fd25711d..623402cc 100644 --- a/src/services/chat/index.ts +++ b/src/services/chat/index.ts @@ -583,20 +583,33 @@ export class ChatService { // {"trackingParams": ...} => ? if ("contents" in obj) { debugLog("continuationNotFound(with contents)", JSON.stringify(obj)); + return { + error: { + status: FetchChatErrorStatus.LiveChatDisabled, + message: + "continuation contents cannot be found. live chat is over, or turned into membership-only stream, or chat got disabled", + }, + }; } if ("trackingParams" in obj) { debugLog( "continuationNotFound(with trackingParams)", JSON.stringify(obj) ); + return { + error: { + status: FetchChatErrorStatus.LiveChatDisabled, + message: + "continuation contents cannot be found. live chat is over, or turned into membership-only stream, or chat got disabled", + }, + }; } + // Live stream ended return { - error: { - status: FetchChatErrorStatus.LiveChatDisabled, - message: - "continuation contents cannot be found. live chat is over, or turned into membership-only stream, or chat got disabled", - }, + actions: [], + continuation: undefined, + error: null, }; } @@ -658,7 +671,6 @@ export class ChatService { // handle errors if (chatResponse.error) { - // TODO: break if live stream is ended yield chatResponse; continue; } @@ -668,11 +680,9 @@ export class ChatService { // refresh continuation token const { continuation } = chatResponse; + if (!continuation) { - // TODO: verify that this scenario actually exists - debugLog( - "[action required] got chatResponse but no continuation event occurred" - ); + debugLog("live stream ended"); break; } diff --git a/src/services/context/index.ts b/src/services/context/index.ts index 4c85cfc4..d24fdf98 100644 --- a/src/services/context/index.ts +++ b/src/services/context/index.ts @@ -74,11 +74,11 @@ export class ContextService { this.apiKey = ctx.apiKey; } - protected async populateLiveChatContext() { - const token = this.continuation.top.token; - const ctx = await this.fetchLiveChatContext(token); - this.liveChatContext = ctx; - } + // protected async populateLiveChatContext() { + // const token = this.continuation.top.token; + // const ctx = await this.fetchLiveChatContext(token); + // this.liveChatContext = ctx; + // } private async fetchContext(id: string): Promise { const res = await this.get("/watch?v=" + id); diff --git a/src/services/message/index.ts b/src/services/message/index.ts index d27b4afc..7bffdfb6 100644 --- a/src/services/message/index.ts +++ b/src/services/message/index.ts @@ -1,4 +1,5 @@ import { Base } from "../../base"; +import { generateSendMessageParams } from "../../protobuf"; import { YTActionResponse, YTLiveChatTextMessageRenderer, @@ -13,8 +14,12 @@ export class MessageService { async sendMessage( message: string ): Promise { - const params = this.liveChatContext?.sendMessageParams; - if (!params) return undefined; + // const params = this.liveChatContext?.sendMessageParams; + // if (!params) return undefined; + const params = generateSendMessageParams( + this.metadata.channelId, + this.videoId + ); const body = withContext({ richMessage: { diff --git a/tests/message.test.ts b/tests/message.test.ts index 97cbb1dc..8e06feae 100644 --- a/tests/message.test.ts +++ b/tests/message.test.ts @@ -5,53 +5,68 @@ const id = process.env.MC_MSG_TEST_ID; const credentialsB64 = process.env.MC_MSG_TEST_CREDENTIALS; const enabled = id && credentialsB64; +const credentials = JSON.parse( + Buffer.from(credentialsB64!, "base64").toString() +) as any; const itif = enabled ? it : it.skip; const record = setupRecorder({ mode: (process.env.NOCK_BACK_MODE as any) || "record", }); -let mc: Masterchat; -let chatId: string; -let chatParams: string; - -beforeAll(async () => { - const { completeRecording } = await record("message_prelude"); - const credentials = JSON.parse( - Buffer.from(credentialsB64!, "base64").toString() - ) as any; - mc = await Masterchat.init(id!, { credentials }); - completeRecording(); -}); - -describe("send message", () => { - itif("can send message", async () => { - const { completeRecording } = await record("message_send"); - const msg = "hello world"; - const res = await mc.sendMessage(msg); +describe("subscribers-only mode", () => { + it("can init", async () => { + const { completeRecording, assertScopesFinished } = await record( + "subscribers_only" + ); + const mc = await Masterchat.init("lqhYHycrsHk", { credentials }); completeRecording(); + // expect((mc as any).liveChatContext.sendMessageParams).toBeUndefined(); + }); +}); - if (!res || "error" in res) { - throw new Error("invalid res"); - } +describe("normal message handling", () => { + let mc: Masterchat; + let chatId: string; + let chatParams: string; - expect(res).toMatchObject({ - message: { runs: [{ text: msg }] }, + describe("send message", () => { + beforeAll(async () => { + const { completeRecording } = await record("message_prelude"); + + mc = await Masterchat.init(id!, { credentials }); + completeRecording(); }); - // Remove - chatId = res.id; - chatParams = - res.contextMenuEndpoint!.liveChatItemContextMenuEndpoint.params; + itif("can send message", async () => { + const { completeRecording } = await record("message_send"); + + const msg = "hello world"; + const res = await mc.sendMessage(msg); + completeRecording(); + + if (!res || "error" in res) { + throw new Error("invalid res"); + } + + expect(res).toMatchObject({ + message: { runs: [{ text: msg }] }, + }); + + // Remove + chatId = res.id; + chatParams = + res.contextMenuEndpoint!.liveChatItemContextMenuEndpoint.params; + }); }); -}); -describe("remove message", () => { - itif("can remove message", async () => { - const { completeRecording } = await record("message_remove"); - const res = await mc.remove(chatParams); - completeRecording(); - const targetId = res?.targetItemId; - expect(targetId).toBe(chatId); + describe("remove message", () => { + itif("can remove message", async () => { + const { completeRecording } = await record("message_remove"); + const res = await mc.remove(chatParams); + completeRecording(); + const targetId = res?.targetItemId; + expect(targetId).toBe(chatId); + }); }); });