onepisya / GPT-4 Mobile

// ==UserScript==
// @name         GPT-4 Mobile
// @name:zh-CN   GPT-4 Mobile
// @version      0.9
// @description  Due to some special reasons, this script will no longer be updated and will be transferred to [talk-to-gpt-4-mobile](https://greasyfork.org/en/scripts/467538-talk-to-gpt-4-mobile) for code updates, together with Unintendedz.
// @description:zh-CN 因为一些特殊原因,此脚本不再更新,将转到 [talk-to-gpt-4-mobile](https://greasyfork.org/en/scripts/467538-talk-to-gpt-4-mobile)与 Unintendedz 一起更新代码
// @author       Unintendedz and enzheng128 and onepisYa
// @match        https://chat.openai.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-idle
// @license      MIT
// @namespace    https://greasyfork.org/en/scripts/467802-gpt-4-mobile
// ==/UserScript==
/*
 * Portions of this script are modifications based on work created and
 * shared by Unintendedz, enzheng128 and used according to terms described in the MIT and WTFPL License.
 *
 * Copyright (c) 2023 Unintendedz, enzheng128
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */


(function () {
  'use strict';
  // 用户类型事件监听
  window.addEventListener('UserTypeEvent', function (e) {
    GM_setValue('userType', e.detail.userType);
  });

  // ==========================================
  // =============== 脚本菜单处理 ===============
  // ==========================================
  // set default
  const BUTTONS_GROUPS = ['GPT-3.5'];
  const isPlus = GM_getValue('userType');
  let DEFAULT_BUTTON = 'GPT-3.5 Mobile';
  if (isPlus) {
    DEFAULT_BUTTON = 'GPT-4 Mobile';
    BUTTONS_GROUPS.push('GPT-4');
  }
  BUTTONS_GROUPS.push(DEFAULT_BUTTON);

  let menus = [];
  let isSwitch = false;

  // 注册脚本菜单
  const registerMenuCommand = () => {
    const onHandle = (value) => {
      GM_setValue('defaultModel', value);
      registerMenuCommand();

      if (isPlus === false) {
        switch (value) {
          case 'GPT-3.5':
            registerMenuCommand();
            window.location.href = "https://chat.openai.com/?model=text-davinci-002-render-sha";
            break;
          default:
            registerMenuCommand();
            window.location.href = "https://chat.openai.com/?model=text-davinci-002-render-sha-mobile";
            break;
        }
      } else {
        registerMenuCommand();
      }
    }
    if (!GM_getValue('defaultModel')) GM_setValue('defaultModel', DEFAULT_BUTTON)
    const defaultValue = GM_getValue('defaultModel')
    menus.forEach(menu => GM_unregisterMenuCommand(menu));
    menus = BUTTONS_GROUPS.map((buttonText) => GM_registerMenuCommand(`切换默认为:${buttonText}${defaultValue === buttonText ? '(当前)' : ''}`, () => onHandle(buttonText)))
  }

  const checkButton = (addedNode) => {
    const model = `${GM_getValue('defaultModel')}`
    console.log("current model button should be", model);
    if (addedNode.nodeType === Node.ELEMENT_NODE) {
      const buttons = addedNode.querySelectorAll('button');
      for (let button of buttons) {
        if (button.textContent === model) {
          button.querySelector('span')?.click();
          button.querySelector('span')?.click();
          button.querySelector('span')?.click();
          return true;
        }
      }
    }
    return false;
  }

  const handleClick = () => {
    isSwitch = true;
  }

  // 监听newChat事件
  const addEventTargetA = () => {
    const buttons = document.querySelectorAll('a')
    for (const button of buttons) {
      if (button.textContent === 'New chat') {
        button.removeEventListener('click', handleClick)
        button.addEventListener('click', handleClick)
        break;
      }
    }
  }

  const callback = (mutationRecords) => {
    for (const mutationRecord of mutationRecords) {
      if (mutationRecord.addedNodes.length) {
        for (const addedNode of mutationRecord.addedNodes) {
          if (checkButton(addedNode)) return;
        }
      }
    }
    addEventTargetA()
  };
  registerMenuCommand()
  addEventTargetA();
  const observer = new MutationObserver(callback);
  observer.observe(document.getElementById('__next'), {
    childList: true,
    subtree: true,
  });
  // 修改pushStatus和replaceStatus
  const pushState = window.history.pushState;
  const replaceState = window.history.replaceState;
  window.history.pushState = function () {
    if (isSwitch) {
      // 等到 openai 发送的请求完毕之后,将它原本的model历史记忆设置完成之后,我们再设置自己想要的默认模型。
      setTimeout(() => checkButton(document.getElementById('__next')), 650)
    }
    pushState.apply(this, arguments);
    isSwitch = false
  }
  window.history.replaceState = function () {
    if (isSwitch) {
      setTimeout(() => checkButton(document.getElementById('__next')), 650)
    }
    replaceState.apply(this, arguments);
    isSwitch = false
  }

  // =========================================================
  // =============== fetch 拦截 添加gpt-4 mobile ==============
  // =========================================================
  // 将代码插入到网页中
  const script = document.createElement('script');
  // add mobile GPT-4
  script.textContent = `
      let waitIsPlus = null;
      let resolveIsPlus = null;
      let isPlus = null;
      waitIsPlus = new Promise((resolve) => {
        resolveIsPlus = resolve;
      });
      const responseHandlers = {
        'https://chat.openai.com/backend-api/models': async function (response) {
          const body = await response.clone().json();
          if (isPlus === null) {
            await waitIsPlus;
          }
          const model = isPlus ? {
            "category": "gpt_4",
            "human_category_name": "GPT-4 Mobile",
            "subscription_level": "plus",
            "default_model": "gpt-4-mobile"
          } : {
            "category": "gpt_4",
            "human_category_name": "GPT-3.5 Mobile",
            "subscription_level": "free",
            "default_model": "text-davinci-002-render-sha-mobile"
          };
          body.categories.push(model);

          let event = new CustomEvent('UserTypeEvent', { detail: { userType: isPlus } });
          window.dispatchEvent(event);
          return new Response(JSON.stringify(body), {
            status: response.status,
            statusText: response.statusText,
            headers: { 'Content-Type': 'application/json' }
          });
        },

        'https://chat.openai.com/backend-api/moderations': async function (response) {
          const body = await response.clone().json();
          body.flagged = false;
          body.blocked = false;

          return new Response(JSON.stringify(body), {
            status: response.status,
            statusText: response.statusText,
            headers: { 'Content-Type': 'application/json' }
          });
        },
        'https://chat.openai.com/backend-api/accounts/check': async function (response) {
          const body = await response.clone().json();
          const subscription_plan = body.accounts.default.entitlement.subscription_plan;
          isPlus = subscription_plan === 'chatgptplusplan';

          return (() => { resolveIsPlus(); return response })();
        },
      };
      window.fetch = new Proxy(window.fetch, {
        apply: async function (target, thisArg, argumentsList) {
          const response = await Reflect.apply(...arguments);
          for (let key in responseHandlers) {
            if (argumentsList[0].includes(key)) {
              return responseHandlers[key](response);
            }
          }
          return response;
        }
      });
    `;
  document.body.appendChild(script);
})();