Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to pin an automated comment #360

Open
prkhrv opened this issue Nov 12, 2021 · 6 comments
Open

How to pin an automated comment #360

prkhrv opened this issue Nov 12, 2021 · 6 comments

Comments

@prkhrv
Copy link

prkhrv commented Nov 12, 2021

No description provided.

@EliasOlie
Copy link

Which language are you consuming the API?

@prkhrv
Copy link
Author

prkhrv commented Nov 19, 2021

python

@EliasOlie
Copy link

Well, I don't find any reference for pin a comment on the API guide
https://developers.google.com/youtube/v3/docs/comments

but if you perform a pinned comment on youtube manually and inspect the network tab on developers tools when you pin a comment it makes a POST request on

"https://www.youtube.com/youtubei/v1/comment/perform_comment_action?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"

as long I tested the "key" parameter doesn't change, it could be some reference for the user who commented or the parameter for the action to pin a comment. Automate this probably is easy, if it's key to "action for pin comments " some testing could bring you an idea of how to do this

@DionisisPa
Copy link

Well, I don't find any reference for pin a comment on the API guide https://developers.google.com/youtube/v3/docs/comments

but if you perform a pinned comment on youtube manually and inspect the network tab on developers tools when you pin a comment it makes a POST request on

"https://www.youtube.com/youtubei/v1/comment/perform_comment_action?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"

as long I tested the "key" parameter doesn't change, it could be some reference for the user who commented or the parameter for the action to pin a comment. Automate this probably is easy, if it's key to "action for pin comments " some testing could bring you an idea of how to do this

Hi, I am a bit immature with Υoutube automations but I really need to find a way to pin comments in YouTube. Is there a new idea? Please help

@MarkRosemaker
Copy link

MarkRosemaker commented Jun 23, 2023

Can confirm that I'm getting the same URL or https://www.youtube.com/youtubei/v1/comment/perform_comment_action?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false to be precise.

After reverse engineering for a bit I finally found where the comment ID is passed: It's part of an action string that is base64 encoded and then query escaped. This action string then has to be part of a request body.

The request body the browser sends has a lot of information but luckily, almost everything can be stripped. However, besides the action string, we need at least a client context with the client name and version.
Otherwise, if you strip out more, you get the error:

{
  "error": {
    "code": 400,
    "message": "Precondition check failed.",
    "errors": [
      {
        "message": "Precondition check failed.",
        "domain": "global",
        "reason": "failedPrecondition"
      }
    ],
    "status": "FAILED_PRECONDITION"
  }
}

But this is a valid request body if you replace my-action-string with your actual encoded action string:

{"context":{"client":{"clientName":"WEB","clientVersion":"2.20230622.01.00"}},"actions":["my-action-string"]}

(Yeah, we're lying by saying we use the web client but I'm not sure which other input values would work.)

Last but not least, you'll need to be authenticated in some way.

It's all a bit complicated so here's my go code:

package main

import (
	"bytes"
	"context"
	_ "embed"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/youtube/v3"
)

type PinCommentRequestBody struct {
	Context *Context `json:"context,omitempty"`
	Actions []string `json:"actions,omitempty"`
}

type Context struct {
	Client *ClientContext `json:"client,omitempty"`
}

type ClientContext struct {
	ClientName    string `json:"clientName,omitempty"`
	ClientVersion string `json:"clientVersion,omitempty"`
}

// this is the key to perform the pin action, same for all users
const pinCommentActionKey = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"

// getOauthConfig returns an oauth2 config that can be used to authenticate with Google
// You'll need to set the GOOGLE_CLIENT_SECRET_JSON environment variable to a JSON file
// containing the client secret for your Google app.
func getOauthConfig() (*oauth2.Config, error) {
	googleJSON := os.Getenv("GOOGLE_CLIENT_SECRET_JSON")
	if googleJSON == "" {
		return nil, errors.New("GOOGLE_CLIENT_SECRET_JSON is empty")
	}

	cfg, err := google.ConfigFromJSON([]byte(googleJSON),
		youtube.YoutubeForceSslScope, // write and pin comments
	)
	if err != nil {
		return nil, err
	}

	return cfg, nil
}

// getClient returns an http client that can be used to perform authenticated requests.
// The token can be obtained by calling `cfg.AuthCodeURL` and authenticating with Google,
// then using a local server (http://localhost:8080/callback) for the callback to get the code.
func getClient(ctx context.Context, cfg *oauth2.Config, token *oauth2.Token) *http.Client {
	return cfg.Client(ctx, token)
}

// pinComment pins a YouTube comment
// NOTE: The client must authenticate with a Google account that has a YouTube channel
// and have at least the `https://www.googleapis.com/auth/youtube.force-ssl` scope
func pinComment(ctx context.Context, cli *http.Client, commentID string) error {
	// the URL to perform the pin action
	pinURL := fmt.Sprintf("https://www.youtube.com/youtubei/v1/comment/perform_comment_action?key=%s&prettyPrint=false", pinCommentActionKey)

	// it's not clear why this strange string is needed, but it works
	// note the `%s` argument after the first control characters, it's the comment ID
	actionRaw := fmt.Sprintf("\b\v\x10\x02\x1a\x1a%s*\vuKed7esvBEI0\x00J\x15102700233928915755015\xa8\x01\f\xba\x01\x18UCcNBwmeFr0IeLVlP_49glrA\xf0\x01\x00\x8a\x02\x10comments-section", commentID)
	action := url.QueryEscape(base64.StdEncoding.EncodeToString([]byte(actionRaw)))

	// create the request body
	// besides the action, it also needs a client context with at least the client name and version
	body := &PinCommentRequestBody{
		Context: &Context{
			Client: &ClientContext{
				// obviously a lie but it works
				ClientName:    "WEB",
				ClientVersion: "2.20230622.01.00",
			},
		},
		Actions: []string{action},
	}

	// encode the body as JSON
	bodyBytes, err := json.Marshal(body)
	if err != nil {
		return err
	}

	// create the request with the pin URL and the body
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, pinURL, bytes.NewReader(bodyBytes))
	if err != nil {
		return err
	}

	// make the request
	rsp, err := cli.Do(req)
	if err != nil {
		return err
	}
	defer rsp.Body.Close()

	if rsp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(rsp.Body)
		return fmt.Errorf("unexpected status %s: %s", rsp.Status, string(b))
	}

	// b, _ := io.ReadAll(rsp.Body)
	// fmt.Printf("body: %v\n", string(b))

	return nil // success!
}

@foodtourama
Copy link

Can confirm that I'm getting the same URL or https://www.youtube.com/youtubei/v1/comment/perform_comment_action?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false to be precise.

After reverse engineering for a bit I finally found where the comment ID is passed: It's part of an action string that is base64 encoded and then query escaped. This action string then has to be part of a request body.

The request body the browser sends has a lot of information but luckily, almost everything can be stripped. However, besides the action string, we need at least a client context with the client name and version. Otherwise, if you strip out more, you get the error:

{
  "error": {
    "code": 400,
    "message": "Precondition check failed.",
    "errors": [
      {
        "message": "Precondition check failed.",
        "domain": "global",
        "reason": "failedPrecondition"
      }
    ],
    "status": "FAILED_PRECONDITION"
  }
}

But this is a valid request body if you replace my-action-string with your actual encoded action string:

{"context":{"client":{"clientName":"WEB","clientVersion":"2.20230622.01.00"}},"actions":["my-action-string"]}

(Yeah, we're lying by saying we use the web client but I'm not sure which other input values would work.)

Last but not least, you'll need to be authenticated in some way.

It's all a bit complicated so here's my go code:

package main

import (
	"bytes"
	"context"
	_ "embed"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/youtube/v3"
)

type PinCommentRequestBody struct {
	Context *Context `json:"context,omitempty"`
	Actions []string `json:"actions,omitempty"`
}

type Context struct {
	Client *ClientContext `json:"client,omitempty"`
}

type ClientContext struct {
	ClientName    string `json:"clientName,omitempty"`
	ClientVersion string `json:"clientVersion,omitempty"`
}

// this is the key to perform the pin action, same for all users
const pinCommentActionKey = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"

// getOauthConfig returns an oauth2 config that can be used to authenticate with Google
// You'll need to set the GOOGLE_CLIENT_SECRET_JSON environment variable to a JSON file
// containing the client secret for your Google app.
func getOauthConfig() (*oauth2.Config, error) {
	googleJSON := os.Getenv("GOOGLE_CLIENT_SECRET_JSON")
	if googleJSON == "" {
		return nil, errors.New("GOOGLE_CLIENT_SECRET_JSON is empty")
	}

	cfg, err := google.ConfigFromJSON([]byte(googleJSON),
		youtube.YoutubeForceSslScope, // write and pin comments
	)
	if err != nil {
		return nil, err
	}

	return cfg, nil
}

// getClient returns an http client that can be used to perform authenticated requests.
// The token can be obtained by calling `cfg.AuthCodeURL` and authenticating with Google,
// then using a local server (http://localhost:8080/callback) for the callback to get the code.
func getClient(ctx context.Context, cfg *oauth2.Config, token *oauth2.Token) *http.Client {
	return cfg.Client(ctx, token)
}

// pinComment pins a YouTube comment
// NOTE: The client must authenticate with a Google account that has a YouTube channel
// and have at least the `https://www.googleapis.com/auth/youtube.force-ssl` scope
func pinComment(ctx context.Context, cli *http.Client, commentID string) error {
	// the URL to perform the pin action
	pinURL := fmt.Sprintf("https://www.youtube.com/youtubei/v1/comment/perform_comment_action?key=%s&prettyPrint=false", pinCommentActionKey)

	// it's not clear why this strange string is needed, but it works
	// note the `%s` argument after the first control characters, it's the comment ID
	actionRaw := fmt.Sprintf("\b\v\x10\x02\x1a\x1a%s*\vuKed7esvBEI0\x00J\x15102700233928915755015\xa8\x01\f\xba\x01\x18UCcNBwmeFr0IeLVlP_49glrA\xf0\x01\x00\x8a\x02\x10comments-section", commentID)
	action := url.QueryEscape(base64.StdEncoding.EncodeToString([]byte(actionRaw)))

	// create the request body
	// besides the action, it also needs a client context with at least the client name and version
	body := &PinCommentRequestBody{
		Context: &Context{
			Client: &ClientContext{
				// obviously a lie but it works
				ClientName:    "WEB",
				ClientVersion: "2.20230622.01.00",
			},
		},
		Actions: []string{action},
	}

	// encode the body as JSON
	bodyBytes, err := json.Marshal(body)
	if err != nil {
		return err
	}

	// create the request with the pin URL and the body
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, pinURL, bytes.NewReader(bodyBytes))
	if err != nil {
		return err
	}

	// make the request
	rsp, err := cli.Do(req)
	if err != nil {
		return err
	}
	defer rsp.Body.Close()

	if rsp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(rsp.Body)
		return fmt.Errorf("unexpected status %s: %s", rsp.Status, string(b))
	}

	// b, _ := io.ReadAll(rsp.Body)
	// fmt.Printf("body: %v\n", string(b))

	return nil // success!
}

Hi!
Is this code still working for you?
Couldn't replicate your code in Python...
I think YouTube has changed something.
I also tried to replace parts of the "actionRaw" that you didn't update, like the videoID and channelD
There's also a "mystery number" in the actionRaw that changes and I don't know what it means.
The "pinCommentActionKey" seems to be gone from the URL now...
I keep getting this response:
<Response [400]> 400
{'error': {'code': 400, 'message': 'Request contains an invalid argument.', 'errors': [{'message': 'Request contains an invalid argument.', 'domain': 'global', 'reason': 'badRequest'}], 'status': 'INVALID_ARGUMENT'}}

Hope your code is still working... Maybe I'll try to replicate it in Go...
My channel DOES have the ability to PIN comments.
Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants