/*
  StreamGoose YouTube backend — Innertube API
  
  Created by Luxferre in 2024, released into public domain
*/

package main

import (
  "fmt"
  "log"
  "bytes"
  "strings"
  "strconv"
  "regexp"
  "time"
  "encoding/json"
  "net/http"
  "io/ioutil"
)

var YT_INNER_USERAGENT string

/* InnerTube chat options structure */
type YTIChatOptions struct {
  Key string           `json:"key"`           /* InnerTube API key */
  Continuation string  `json:"continuation"`  /* continuation key */
  ClientName string    `json:"clientName"`    /* client name string */
  ClientVersion string `json:"clientVersion"` /* client version string */
}

/* API request helper function */
func yt_inner_api_req(url string, data []byte) string {
  method := "GET"
  if len(data) > 0 {
    method = "POST"
  }
  req, err := http.NewRequest(method, url, bytes.NewReader(data))
  req.Header.Set("User-Agent", YT_INNER_USERAGENT)
  req.Header.Set("Accept-Language", "en-US")

  client := &http.Client{}
  resp, err := client.Do(req)
  if err != nil {
    log.Fatal(err)
  }
  body, _ := ioutil.ReadAll(resp.Body)
  resp.Body.Close()
  /* print the response if the debug flag is set */
  if YT_DEBUG {
    log.Printf("[youtube] API response: %s", string(body))
  }
  return string(body)
}

/* convert a series of runs into a text message */
func yt_inner_runs_to_text(runs []interface{}) string {
  out_text := ""
  for _, rrun := range runs { /* iterate over the runs */
    run := rrun.(map[string]interface{})
    if _, ok := run["text"]; ok { /* normal text */
      out_text += run["text"].(string)
    } else if _, ok := run["emoji"]; ok { /* this is where the fun begins */
      emoji := run["emoji"].(map[string]interface{})
      if _, ok := emoji["isCustomEmoji"]; ok && emoji["isCustomEmoji"].(bool) == true { /* custom emoji */
        /* TODO: out_text += " #YTEMOTE-" + emoji["image"]["thumbnails"][0]["url"] + "# " */
        if len(emoji["shortcuts"].([]interface{})) > 0 {
          out_text += " " + emoji["shortcuts"].([]interface{})[0].(string) + " "
        }
      } else { /* standard Unicode emoji, pass as text */
        out_text += emoji["emojiId"].(string)
      }
    }
  }
  return out_text
}

/* convert inner chat action into a public chat item */
func yt_inner_action_to_chatitem(action map[string]interface{}) ChatMessage {
  item := action["addChatItemAction"].(map[string]interface{})["item"].(map[string]interface{})
  /* get the renderer */
  var renderer map[string]interface{}
  if _, ok := item["liveChatTextMessageRenderer"]; ok {
    renderer = item["liveChatTextMessageRenderer"].(map[string]interface{})
  } else if _, ok := item["liveChatPaidMessageRenderer"]; ok {
    renderer = item["liveChatPaidMessageRenderer"].(map[string]interface{})
  } else  if _, ok := item["liveChatPaidStickerRenderer"]; ok {
    renderer = item["liveChatPaidStickerRenderer"].(map[string]interface{})
  } else if _, ok := item["liveChatMembershipItemRenderer"]; ok {
    renderer = item["liveChatMembershipItemRenderer"].(map[string]interface{})
  } else {
    renderer = nil
  }
  if renderer == nil { /* invalid renderer field */
    return ChatMessage{}
  }
  /* here, message text is composed as a series of individual runs */
  var mruns []interface{}
  if _, ok := renderer["message"]; ok {
    mruns = renderer["message"].(map[string]interface{})["runs"].([]interface{})
  } else if _, ok := renderer["headerSubtext"]; ok {
    mruns = renderer["headerSubtext"].(map[string]interface{})["runs"].([]interface{})
  }
  /* fetch the author display name */
  author_name := ""
  if _, ok := renderer["authorName"]; ok {
    author_name = renderer["authorName"].(map[string]interface{})["simpleText"].(string)
  }
  /* fetch the author icon URL */
  author_icon := ""
  if _, ok := renderer["authorPhoto"]; ok {
    thumbnails := renderer["authorPhoto"].(map[string]interface{})["thumbnails"].([]interface{})
    if len(thumbnails) > 0 { /* the first thumbnail URL will do */
      author_icon = thumbnails[0].(map[string]interface{})["url"].(string)
    }
  }
  /* fetch the timestamp (microseconds) */
  ts, err := strconv.ParseInt(renderer["timestampUsec"].(string), 10, 0)
  if err != nil { /* fallback to current time if anything */
    ts = time.Now().UnixMicro()
  }
  sent_dt := time.UnixMicro(ts)

  /* prepare output message */
  out_message := ChatMessage {
    Type: "yt",
    NativeId: renderer["id"].(string),
    AuthorId: renderer["authorExternalChannelId"].(string),
    Username: author_name,
    Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
    Text: yt_inner_runs_to_text(mruns),
    Icon: author_icon,
  }

  /* check roles */
  if _, ok := renderer["authorBadges"]; ok {
    for _, entry := range renderer["authorBadges"].([]interface{}) {
      badge := entry.(map[string]interface{})["liveChatAuthorBadgeRenderer"].(map[string]interface{})
      if _, ok := badge["customThumbnail"]; !ok { /* custom badges not supported yet */
        switch badge["icon"].(map[string]interface{})["iconType"].(string) {
          case "OWNER":
            out_message.Type += "^broadcaster"
            break
          case "MODERATOR":
            out_message.Type += "^moderator"
            break
          case "VERIFIED":
            out_message.Type += "^verified"
            break
        }
      }
    }
  }
  if _, ok := renderer["purchaseAmountText"]; ok {
    out_message.Type += "^sponsor"
  }
  
  return out_message
}

/* fetch chat options by the broadcast video ID or handle */
func yt_inner_get_chat_options(video_id string) YTIChatOptions {
  var opts YTIChatOptions
  var live_url string
  if strings.HasPrefix(video_id, "@") { /* it's a handle */
    live_url = fmt.Sprintf("https://www.youtube.com/%s/live", video_id)
  } else { /* it's a broadcast video ID */
    live_url = fmt.Sprintf("https://www.youtube.com/live_chat?v=%s", video_id)
  }
  raw_data := yt_inner_api_req(live_url, nil)
  opts.ClientName = "WEB"
  r, _ := regexp.Compile("\"INNERTUBE_API_KEY\":\"(.+?)\"")
  rmatch := r.FindStringSubmatch(raw_data)
  if len(rmatch) > 0 {
    opts.Key = rmatch[1]
  } else {
    log.Fatal("[youtube] Cannot connect to the YouTube livechat (innertube key)")
  }
  r, _ = regexp.Compile("\"continuation\":\"(.+?)\"")
  rmatch = r.FindStringSubmatch(raw_data)
  if len(rmatch) > 0 {
    opts.Continuation = rmatch[1]
  } else {
    log.Fatal("[youtube] Cannot connect to the YouTube livechat (continuation)")
  }
  r, _ = regexp.Compile("\"clientVersion\":\"(.+?)\"")
  rmatch = r.FindStringSubmatch(raw_data)
  if len(rmatch) > 0 {
    opts.ClientVersion = rmatch[1]
  } else {
    log.Fatal("[youtube] Cannot connect to the YouTube livechat (clientversion)")
  }
  return opts
}

/* fetch chat data using the chat options */
/* return the list of ChatMessage items */
/* should update the options structure */
func yt_inner_get_chat_data(opts *YTIChatOptions) []ChatMessage {
  jsondata := make(map[string]interface{})
  jsondata["context"] = make(map[string]interface{})
  jsondata["continuation"] = opts.Continuation
  jsondata["context"].(map[string]interface{})["client"] = make(map[string]string)
  jsondata["context"].(map[string]interface{})["client"].(map[string]string)["userAgent"] = YT_INNER_USERAGENT
  jsondata["context"].(map[string]interface{})["client"].(map[string]string)["clientName"] = opts.ClientName
  jsondata["context"].(map[string]interface{})["client"].(map[string]string)["clientVersion"] = opts.ClientVersion
  jsonstr, _ := json.Marshal(&jsondata)
  /* post the marshalled string and get the result into chatdata */
  target_url := fmt.Sprintf("https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=%s", opts.Key)
  rawresp := yt_inner_api_req(target_url, jsonstr)
  var chatdata map[string]interface{}
  err := json.Unmarshal([]byte(rawresp), &chatdata)
  if err != nil {
    log.Fatal(err)
  }
  var chatitems []ChatMessage
  /* process the upper structure */
  if _, ok := chatdata["continuationContents"]; ok {
    cc := chatdata["continuationContents"].(map[string]interface{})
    if _, ok := cc["liveChatContinuation"]; ok {
      lcc := cc["liveChatContinuation"].(map[string]interface{})
      if lcc["actions"] != nil {
      actions := lcc["actions"].([]interface{})
        conts := lcc["continuations"].([]interface{})
        icd := conts[0].(map[string]interface{})["invalidationContinuationData"].(map[string]interface{})
        opts.Continuation = icd["continuation"].(string)
        for _, action := range actions { /* iterate through the actions */
          if _, ok := action.(map[string]interface{})["addChatItemAction"]; ok {
            chatitem := yt_inner_action_to_chatitem(action.(map[string]interface{}))
            if chatitem.Type != "" {
              chatitems = append(chatitems, chatitem)
            }
          }
        }
      }
    }
  }
  return chatitems
}
