Skip to content

Commit

Permalink
feat!: homebrew sendMessage params
Browse files Browse the repository at this point in the history
  • Loading branch information
uetchy committed Aug 25, 2021
1 parent 053f055 commit cef8d92
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 60 deletions.
2 changes: 1 addition & 1 deletion src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ 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;
}

public async setVideoId(videoIdOrUrl: string) {
const videoId = normalizeVideoId(videoIdOrUrl);
this.videoId = videoId;
await this.populateMetadata();
if (this.credentials) await this.populateLiveChatContext();
// if (this.credentials) await this.populateLiveChatContext();
}

private constructor(
Expand Down
22 changes: 17 additions & 5 deletions src/protobuf.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,13 +17,18 @@
0b = 11bytes
<omitted> (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(
Expand All @@ -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) {
Expand Down
30 changes: 20 additions & 10 deletions src/services/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -658,7 +671,6 @@ export class ChatService {

// handle errors
if (chatResponse.error) {
// TODO: break if live stream is ended
yield chatResponse;
continue;
}
Expand All @@ -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;
}

Expand Down
10 changes: 5 additions & 5 deletions src/services/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Context> {
const res = await this.get("/watch?v=" + id);
Expand Down
9 changes: 7 additions & 2 deletions src/services/message/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Base } from "../../base";
import { generateSendMessageParams } from "../../protobuf";
import {
YTActionResponse,
YTLiveChatTextMessageRenderer,
Expand All @@ -13,8 +14,12 @@ export class MessageService {
async sendMessage(
message: string
): Promise<YTLiveChatTextMessageRenderer | undefined> {
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: {
Expand Down
85 changes: 50 additions & 35 deletions tests/message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});

0 comments on commit cef8d92

Please sign in to comment.