在 Sub-Store 内添加 Webshare 节点

之前写了个在 Sub-Store 里添加 webshare 节点的脚本 ( 适用于 Sub-Store 的 WebShare 节点生成脚本 ),但终归缺少了流量信息,现在补齐,需要在参数设置里设置 apiKey 。效果如图: 默认: • 无 NODE_PREFIX:节点名类似 01 • 无 DIAL...
在 Sub-Store 内添加 Webshare 节点
在 Sub-Store 内添加 Webshare 节点

之前写了个在 Sub-Store 里添加 webshare 节点的脚本 ( 适用于 Sub-Store 的 WebShare 节点生成脚本 ),但终归缺少了流量信息,现在补齐,需要在参数设置里设置 apiKey。效果如图:

1000180512.jpg

默认:
• 无 NODE_PREFIX:节点名类似 :united_states: 01
• 无 DIALER_PROXY
• 必填只需要 apiKey

可选:
• nodePrefix=WEB | 家宽
• dialerProxy=Relay
• nodePrefix=none 表示不要前缀
• dialerProxy=none 表示不要前置代理

async function operator(proxies, targetPlatform, context) {
  proxies = Array.isArray(proxies) ? proxies : [];

  var SUBS_KEY = "subs";
  var A = typeof $arguments !== "undefined" && $arguments ? $arguments : {};
  var $ = typeof $substore !== "undefined" ? $substore : null;
  var PU = typeof ProxyUtils !== "undefined" && ProxyUtils ? ProxyUtils : {};
  var B = PU.Buffer || (typeof Buffer !== "undefined" ? Buffer : null);
  var R = typeof scriptResourceCache !== "undefined" ? scriptResourceCache : null;
  var source = context && context.source ? context.source : null;

  var BASE = "https://proxy.webshare.io";
  var VERSION = 12;
  var TIMEOUT = 8000;

  var NODE_TTL = 12 * 3600 * 1000;
  var NODE_KEEP = 7 * 24 * 3600 * 1000;
  var INFO_TTL = 30 * 60 * 1000;
  var INFO_KEEP = 7 * 24 * 3600 * 1000;

  var PAGE_SIZE = 100;
  var MAX_PAGES = 20;

  function arg(name, def) {
    var v = A ? A[name] : undefined;
    return v === undefined || v === null ? def || "" : String(v).trim();
  }

  function isOff(v) {
    return /^(|0|false|no|none|null|off)$/i.test(String(v == null ? "" : v).trim());
  }

  var API = String(arg("apiKey") || arg("apikey") || arg("token")).trim();
  var NODE_PREFIX = arg("nodePrefix", "");
  var DIALER_PROXY = arg("dialerProxy", "");

  if (isOff(NODE_PREFIX)) NODE_PREFIX = "";
  if (isOff(DIALER_PROXY)) DIALER_PROXY = "";

  if (!$ || !$.http || !$.http.get) throw new Error("当前环境缺少 $substore.http.get");
  if (!API) throw new Error("Sub-Store arguments 未填写:apiKey");

  function log(s) {
    console.log("[Webshare] " + s);
  }

  function warn(s) {
    console.log("[Webshare] WARN: " + s);
  }

  function yes(v) {
    return /^(1|true|yes|y|on)$/i.test(String(v == null ? "" : v).trim());
  }

  function hash(s) {
    var h = 0;
    s = String(s || "");

    for (var i = 0; i < s.length; i++) {
      h = ((h << 5) - h + s.charCodeAt(i)) | 0;
    }

    return (h >>> 0).toString(16);
  }

  var PREFIX = "webshare:v" + VERSION + ":" + hash(API + "\n" + NODE_PREFIX + "\n" + DIALER_PROXY);
  var K_NODES = PREFIX + ":nodes";
  var K_INFO = PREFIX + ":userinfo";

  function redact(s) {
    s = String(s == null ? "" : s);
    if (API) s = s.split(API).join("[ApiKey]");
    return s.replace(/Token\s+[A-Za-z0-9._-]+/gi, "Token [ApiKey]");
  }

  function msg(e) {
    return redact(e && e.message ? e.message : e).slice(0, 500);
  }

  function makeError(message, props) {
    var e = new Error(message);

    if (props) {
      for (var k in props) e[k] = props[k];
    }

    return e;
  }

  function opt() {
    return typeof $options !== "undefined" && $options ? $options : {};
  }

  function flag(names) {
    var o = opt();
    var q = o._req && o._req.query ? o._req.query : {};

    for (var i = 0; i < names.length; i++) {
      var k = names[i];
      if (yes(A[k]) || yes(o[k]) || yes(q[k])) return true;
    }

    return false;
  }

  function bodyText(body) {
    if (body == null) return "";
    if (typeof body === "string") return body;

    if (B && B.from) {
      try {
        return B.from(body).toString("utf8");
      } catch (e) {}
    }

    try {
      return JSON.stringify(body);
    } catch (e) {
      return String(body);
    }
  }

  function parseJson(x) {
    if (!x) return null;
    if (typeof x !== "string") return x;

    try {
      return JSON.parse(x);
    } catch (e) {
      return null;
    }
  }

  function isTransient(status) {
    status = Number(status);
    return [0, 408, 425, 429, 500, 502, 503, 504, 522, 523, 524].indexOf(status) >= 0;
  }

  async function getJson(path) {
    var url = /^https?:\/\//i.test(String(path)) ? String(path) : BASE + path;
    var r;

    try {
      r = await $.http.get.call($.http, {
        url: url,
        timeout: TIMEOUT,
        headers: {
          Authorization: "Token " + API,
          Accept: "application/json",
          "User-Agent": "Mozilla/5.0"
        }
      });
    } catch (e) {
      throw makeError("请求失败:" + msg(e), { transient: true });
    }

    var status = Number((r && (r.statusCode || r.status)) || 0);
    var text = bodyText(r && r.body);
    var data = null;

    try {
      data = text ? JSON.parse(text.replace(/^\uFEFF/, "")) : null;
    } catch (e) {}

    if (status < 200 || status >= 300) {
      throw makeError("HTTP " + status + ":" + redact(text).slice(0, 300), {
        status: status,
        transient: isTransient(status),
        auth: status === 401 || status === 403
      });
    }

    if (data === null) {
      throw makeError("响应不是 JSON", { transient: true });
    }

    return data;
  }

  async function getPaged(path) {
    var out = [];
    var url = path;

    for (var i = 0; url && i < MAX_PAGES; i++) {
      var data = await getJson(url);
      var rows = [];

      if (data && Array.isArray(data.results)) rows = data.results;
      else if (Array.isArray(data)) rows = data;

      for (var j = 0; j < rows.length; j++) out.push(rows[j]);

      url = data && data.next ? String(data.next) : "";
    }

    return out;
  }

  function cacheRead(key, freshMs, keepMs) {
    if (!R) return null;

    var o;

    try {
      o = parseJson(R.get(key));
    } catch (e) {
      return null;
    }

    if (!o || o.v !== VERSION) return null;

    var at = Number(o.at || 0);
    var age = Date.now() - at;

    if (!at || !isFinite(age) || age < 0 || age >= keepMs) {
      cacheDel(key);
      return null;
    }

    o.age = age;
    o.fresh = age < freshMs;

    return o;
  }

  function cacheWrite(key, data, ttl) {
    if (!R) return;

    try {
      var o = {
        v: VERSION,
        at: Date.now()
      };

      for (var k in data) o[k] = data[k];

      R.set(key, JSON.stringify(o), ttl);
    } catch (e) {
      warn("缓存写入失败:" + msg(e));
    }
  }

  function cacheDel(key) {
    if (!R) return;

    try {
      if (typeof R.delete === "function") return R.delete(key);
      if (typeof R.del === "function") return R.del(key);
      if (typeof R.remove === "function") return R.remove(key);

      R.set(key, "", 1000);
    } catch (e) {}
  }

  function clearCache() {
    cacheDel(K_NODES);
    cacheDel(K_INFO);
    log("已清理缓存");
  }

  function padN(n, len) {
    var s = String(n);
    while (s.length < len) s = "0" + s;
    return s;
  }

  function num(x) {
    var n = Number(x);
    return isFinite(n) ? n : 0;
  }

  function cleanIso(s) {
    s = String(s || "").trim();
    if (!s) return "";

    return s.replace(/(\.\d{3})\d+(Z|[+-]\d{2}:?\d{2})$/i, "$1$2");
  }

  function nowIso() {
    return new Date().toISOString();
  }

  function toUnix(x) {
    if (x == null || x === "") return 0;

    if (typeof x === "number") {
      if (!isFinite(x) || x <= 0) return 0;
      return x > 1e12 ? Math.floor(x / 1000) : Math.floor(x);
    }

    var s = String(x).trim();
    if (!s) return 0;

    if (/^\d+$/.test(s)) {
      var n = Number(s);
      if (!isFinite(n) || n <= 0) return 0;
      return n > 1e12 || s.length >= 13 ? Math.floor(n / 1000) : Math.floor(n);
    }

    var t = Date.parse(cleanIso(s));

    return isFinite(t) ? Math.floor(t / 1000) : 0;
  }

  function flagEmoji(cc) {
    cc = String(cc || "US").toUpperCase();

    if (cc === "ZZ") return "🌐";
    if (!/^[A-Z]{2}$/.test(cc)) return "";
    if (typeof String.fromCodePoint !== "function") return "";

    return (
      String.fromCodePoint(0x1f1e6 + cc.charCodeAt(0) - 65) +
      String.fromCodePoint(0x1f1e6 + cc.charCodeAt(1) - 65)
    );
  }

  async function fetchNodes() {
    var rows = await getPaged("/api/v2/proxy/list/?mode=direct&page=1&page_size=" + PAGE_SIZE);
    var valid = [];

    for (var i = 0; i < rows.length; i++) {
      var p = rows[i];

      if (
        p &&
        p.proxy_address &&
        p.port &&
        p.username &&
        p.password &&
        p.valid !== false &&
        String(p.valid).toLowerCase() !== "false"
      ) {
        valid.push(p);
      }
    }

    if (!valid.length) {
      throw makeError("Webshare 没有返回可用代理", { business: true });
    }

    var nodes = [];

    for (var j = 0; j < valid.length; j++) {
      var x = valid[j];
      var icon = flagEmoji(x.country_code || x.country || "US");
      var idx = padN(j + 1, 2);
      var label = NODE_PREFIX ? NODE_PREFIX + " " + idx : idx;
      var name = (icon ? icon + " " : "") + label;

      var node = {
        name: name,
        type: "socks5",
        server: String(x.proxy_address),
        port: Number(x.port),
        username: String(x.username),
        password: String(x.password)
      };

      if (DIALER_PROXY) {
        node["dialer-proxy"] = DIALER_PROXY;
      }

      nodes.push(node);
    }

    return nodes;
  }

  async function fetchInfo() {
    var subscription = await getJson("/api/v2/subscription/");

    var start = cleanIso(subscription && subscription.start_date);
    var end = cleanIso(subscription && subscription.end_date);
    var lte = nowIso();

    if (!start) {
      throw makeError("subscription.start_date 为空,无法生成流量统计区间", { business: true });
    }

    log("统计区间:" + start + " ~ " + lte);

    var statsPath =
      "/api/v2/stats/aggregate/" +
      "?timestamp__gte=" +
      encodeURIComponent(start) +
      "&timestamp__lte=" +
      encodeURIComponent(lte);

    var res = await Promise.all([
      getPaged("/api/v2/subscription/plan/"),
      getJson(statsPath)
    ]);

    var plans = res[0];
    var stats = res[1];

    var limitGb = 0;

    for (var i = 0; i < plans.length; i++) {
      var p = plans[i];

      if (String((p && p.status) || "").toLowerCase() === "active") {
        limitGb += Math.max(0, num(p.bandwidth_limit));
      }
    }

    if (limitGb <= 0) {
      throw makeError("未找到有效 active plan 流量额度", { business: true });
    }

    var used = Math.max(0, Math.round(num(stats && stats.bandwidth_total)));
    var total = Math.round(limitGb * Math.pow(1024, 3));
    var expire = toUnix(end);

    if (!expire) {
      throw makeError("subscription.end_date 解析失败,无法生成到期时间", { business: true });
    }

    return "upload=0;download=" + used + ";total=" + total + ";expire=" + expire;
  }

  function currentSubName() {
    if (!source || typeof source !== "object" || source._collection) return "";

    if (source.name) return source.name;

    for (var k in source) {
      if (k.charAt(0) === "_") continue;

      var sub = source[k];

      if (sub && sub.name) return sub.name;
      if (sub && (sub.url || sub.content)) return k;
    }

    return "";
  }

  function writeInfo(info) {
    if (!$ || !$.read || !$.write || !info) return false;

    try {
      var name = currentSubName();
      if (!name) return false;

      var subs = $.read(SUBS_KEY) || [];

      if (typeof subs === "string") subs = JSON.parse(subs);
      if (!Array.isArray(subs)) return false;

      var sub = null;

      for (var i = 0; i < subs.length; i++) {
        if (subs[i] && subs[i].name === name) {
          sub = subs[i];
          break;
        }
      }

      if (!sub) return false;

      if (sub.subUserinfo !== info) {
        sub.subUserinfo = info;
        $.write(subs, SUBS_KEY);
        log("已写入流量信息:" + info);
      }

      return true;
    } catch (e) {
      warn("写入流量信息失败:" + msg(e));
      return false;
    }
  }

  async function ensureInfo(force) {
    var old = cacheRead(K_INFO, INFO_TTL, INFO_KEEP);

    if (old && old.fresh && !force) {
      writeInfo(old.info);
      return old.info;
    }

    try {
      var info = await fetchInfo();

      cacheWrite(K_INFO, { info: info }, INFO_KEEP);
      writeInfo(info);

      log("生成流量信息:" + info);
      return info;
    } catch (e) {
      if (old && old.info && e && e.transient) {
        warn("流量刷新失败,使用旧缓存:" + msg(e));
        writeInfo(old.info);
        return old.info;
      }

      warn("流量刷新失败:" + msg(e));
      return "";
    }
  }

  function append(nodes) {
    return proxies.concat(nodes);
  }

  if (flag(["clearCache", "clearcache", "clear-cache"])) {
    clearCache();
  }

  var force = flag(["refresh", "force", "noCache", "nocache", "no-cache"]);
  var oldNodes = cacheRead(K_NODES, NODE_TTL, NODE_KEEP);

  if (oldNodes && oldNodes.fresh && !force && Array.isArray(oldNodes.nodes) && oldNodes.nodes.length) {
    log("命中节点缓存,节点数:" + oldNodes.nodes.length);
    await ensureInfo(false);
    return append(oldNodes.nodes);
  }

  try {
    var nodes = await fetchNodes();

    cacheWrite(K_NODES, { nodes: nodes }, NODE_KEEP);
    await ensureInfo(force);

    log("生成节点数:" + nodes.length);
    return append(nodes);
  } catch (e) {
    if (oldNodes && Array.isArray(oldNodes.nodes) && oldNodes.nodes.length && e && e.transient) {
      warn("节点刷新失败,使用旧缓存:" + msg(e));
      await ensureInfo(force);
      return append(oldNodes.nodes);
    }

    throw e;
  }
}

1 个帖子 - 1 位参与者

阅读完整话题

来源: linux.do查看原文