API-Speicher Original

Erlaubt die Speicherung und Wiederverwendung von API-Daten des Leitstellenspiels

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greasyfork.org/scripts/566586/1756446/API-Speicher%20Original.js을(를) 사용하여 포함하는 라이브러리입니다.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name        API-Speicher Original
// @namespace   leeSalami.lss
// @version     0.9.4
// @license     All Rights Reserved
// @author      leeSalami
// @match       https://*.leitstellenspiel.de*
// @description Erlaubt die Speicherung und Wiederverwendung von API-Daten des Leitstellenspiels
// @run-at      document-start

// ==/UserScript==

'use strict'

const DB_NAME = 'lss-api-storage';
const DB_VERSION = 6;

async function openDb() {
  return navigator.locks.request('cas-open-db', () => {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(DB_NAME, DB_VERSION);

      request.onsuccess = event => resolve(event.target.result);
      request.onerror = event => reject(event.target);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;

        if (event.oldVersion < 1) {
          db.createObjectStore('lastUpdate', { keyPath: 'type' });
          db.createObjectStore('missions', { keyPath: 'id' });

          const buildingStore = db.createObjectStore('buildings', { keyPath: 'id' });
          buildingStore.createIndex('building_type', 'building_type', { unique: false });
        }

        if (event.oldVersion < 2) {
          db.createObjectStore('buildingTypes', { keyPath: 'id' });
        }

        if (event.oldVersion < 3) {
          const allianceBuildingStore = db.createObjectStore('allianceBuildings', { keyPath: 'id' });
          allianceBuildingStore.createIndex('building_type', 'building_type', { unique: false });

          db.createObjectStore('schoolingTypes', { keyPath: 'id' });

          const allianceSchoolingsStore = db.createObjectStore('allianceSchoolings', { keyPath: 'id' });
          allianceSchoolingsStore.createIndex('building_id', 'building_id', { unique: false });

          const userInfoStore = db.createObjectStore('userInfo', { keyPath: 'user_id' });
          userInfoStore.createIndex('user_name', 'user_name', { unique: true });

          db.createObjectStore('allianceInfo', { keyPath: 'id' });
          db.createObjectStore('allianceUsers', { keyPath: 'id' });

          db.createObjectStore('vehicleTypes', { keyPath: 'id' });
        }

        if (event.oldVersion < 4) {
          if (db.objectStoreNames.contains('buildings')) {
            const store = event.target.transaction.objectStore('buildings');
            if (store.indexNames.contains('enabled')) {
              store.deleteIndex('enabled');
            }
          }
        }

        if (event.oldVersion < 5) {
          const vehiclesStore = db.createObjectStore('vehicles', { keyPath: 'id' });
          vehiclesStore.createIndex('building_id', 'building_id', { unique: false });
          vehiclesStore.createIndex('vehicle_type', 'vehicle_type', { unique: false });
        }

        if (event.oldVersion < 6) {
          if (db.objectStoreNames.contains('buildings')) {
            const store = event.target.transaction.objectStore('buildings');
            if (store.indexNames.contains('leitstelle_building_id')) {
              store.deleteIndex('leitstelle_building_id');
            }
            store.createIndex('leitstelle_building_id', 'leitstelle_building_id', { unique: false });
          }
        }

        event.target.transaction.oncomplete = () => resolve(db);
        event.target.transaction.onerror = event => reject(event.target);
      };
    });
  });
}

async function storeData(db, storageName, data, lastUpdateType = null) {
  let success = true;

  return new Promise((resolve, reject) => {
    const store = db
      .transaction(storageName, 'readwrite')
      .objectStore(storageName);

    /**
     * @type {string}
     */
    const keyPath = store.keyPath;

    if (Array.isArray(data)) {
      data.forEach((dataSet) => {
        if (Object.keys(dataSet).length > 0) {
          const request = store.put(dataSet);

          request.onerror = () => {
            success = false;
          };
        }
      });
    } else if (data.hasOwnProperty(keyPath)) {
      const request = store.put(data);

      request.onerror = () => {
        success = false;
      };
    } else {
      for (const [key, value] of Object.entries(data)) {
        if (!value.hasOwnProperty(keyPath)) {
          value[keyPath] = !isNaN(Number(key)) ? parseInt(key) : key;
        }
        const request = store.put(value);

        request.onerror = () => {
          success = false;
        };
      }
    }

    if (success) {
      updateLastUpdateTimestamp(db, lastUpdateType ? lastUpdateType : storageName)
      resolve('Data stored');
    } else {
      reject('Unable to store data');
    }
  });
}

async function clearData(db, storageName) {
  let success = true;

  return new Promise((resolve, reject) => {
    const store = db
      .transaction(storageName, 'readwrite')
      .objectStore(storageName);

    const request = store.clear();

    request.onerror = () => {
      success = false;
    };

    if (success) {
      resolve('Data cleared');
    } else {
      reject('Unable to clear data');
    }
  });
}

async function updateLastUpdateTimestamp(db, storageName) {
  db.transaction('lastUpdate', 'readwrite')
    .objectStore('lastUpdate')
    .put({ type: storageName, timestamp: Date.now() });
}

function updateData(db, type, endpoint, maxAge, indexOption = null, limit = null, alternative = false){
  return navigator.locks.request(type, async () => {
    let index = null;
    let indexKey = null;
    let lastUpdateType = null;

    if (indexOption) {
      index = Object.keys(indexOption)[0];
      indexKey = indexOption[index];
      lastUpdateType = `${type}_${index}_${indexKey}`;
    }

    if (await dataNeedsUpdate(db, lastUpdateType ?? type, maxAge)) {
      return await updateDateFromEndpoint(db, type, endpoint, index, indexKey, limit, lastUpdateType, alternative);
    } else {
      return false;
    }
  });
}

async function updateDateFromEndpoint(db, type, endpoint, index, indexKey, limit, lastUpdateType, alternative) {
  let currentEndpoint = endpoint;
  let hasNextPage = false;
  let success = true;
  let page = 1;

  if (limit !== null) {
    currentEndpoint += `?limit=${limit}`
  }

  do {
    hasNextPage = false;
    let data = await fetchData(currentEndpoint);

    if (data === false) {
      if (type === 'vehicles' && alternative === true) {
        return await updateDateFromEndpoint(db, type, '/api/v2/vehicles', index, indexKey, 2000, lastUpdateType, false);
      }

      success = false;
      break;
    }

    let result = data;

    if (result.hasOwnProperty('result')) {
      result = result.result;
    }

    if (page === 1) {
      if (index && indexKey) {
        await deleteByIndex(db, type, index, IDBKeyRange.only(indexKey));
      } else {
        await clearData(db, type);
      }
    }

    if (type === 'allianceInfo' && result && result.hasOwnProperty('users')) {
      await storeData(db, 'allianceUsers', result.users);
      delete result.users;
    }

    if (result) {
      await storeData(db, type, result, lastUpdateType);
    }

    if (data.hasOwnProperty('paging') && data.paging.hasOwnProperty('next_page')) {
      currentEndpoint = data.paging.next_page;
      hasNextPage = true;
      page++;
    }
  } while (hasNextPage);

  return success;
}

async function fetchData(endpoint, retries = 0) {
  const response = await fetch(endpoint);

  if (!response.ok) {
    if (retries <= 1) {
      return await fetchData(endpoint, ++retries);
    }

    return false;
  }

  return await response.json();
}

function updateMissions(db, maxAge = 3_600) {
  return updateData(db, 'missions', '/einsaetze.json', maxAge);
}

function updateBuildings(db, maxAge = 300) {
  return updateData(db, 'buildings', '/api/buildings', maxAge);
}

function updateBuildingById(db, buildingId,  maxAge = 60) {
  return updateData(db, 'buildings', `/api/buildings/${buildingId}`, maxAge, {'building_id': buildingId});
}

function updateBuildingTypes(db, maxAge = 3_600) {
  return updateData(db, 'buildingTypes', `https://api.lss-manager.de/${I18n.locale}/buildings`, maxAge);
}

function updateSchoolingTypes(db, maxAge = 3_600) {
  return updateData(db, 'schoolingTypes', `https://api.lss-manager.de/${I18n.locale}/schoolings`, maxAge);
}

function updateVehicleTypes(db, maxAge = 3_600) {
  return updateData(db, 'vehicleTypes', `https://api.lss-manager.de/${I18n.locale}/vehicles`, maxAge);
}

function updateAllianceBuildings(db, maxAge = 300) {
  return updateData(db, 'allianceBuildings', '/api/alliance_buildings', maxAge);
}

function updateAllianceSchoolings(db, maxAge = 300) {
  return updateData(db, 'allianceSchoolings', '/api/alliance_schoolings', maxAge);
}

function updateUserInfo(db, maxAge = 300) {
  return updateData(db, 'userInfo', '/api/userinfo', maxAge);
}

function updateAllianceInfo(db, maxAge = 300) {
  return updateData(db, 'allianceInfo', '/api/allianceinfo', maxAge);
}

function updateVehicles(db, maxAge = 300) {
  return updateData(db, 'vehicles', '/api/vehicles', maxAge, null, null, true);
}

function updateVehiclesV2(db, maxAge = 300) {
  return updateData(db, 'vehicles', '/api/v2/vehicles', maxAge, null, 2000);
}

function updateVehiclesByBuildingId(db, buildingId,  maxAge = 60) {
  return updateData(db, 'vehicles', `/api/v2/buildings/${buildingId}/vehicles`, maxAge, {'building_id': buildingId});
}

async function dataNeedsUpdate(db, type, maxAge) {
  let needsUpdate = true;

  if (maxAge) {
    const lastUpdate = await getData(db, 'lastUpdate', type);

    if (lastUpdate !== undefined && maxAge * 1_000 > Date.now() - lastUpdate.timestamp) {
      needsUpdate = false;
    }
  }

  return needsUpdate;
}

async function getData(db, storageName, key) {
  return new Promise((resolve, reject) => {
    const request = db
      .transaction(storageName, 'readonly')
      .objectStore(storageName)
      .get(key);

    request.onsuccess = (event) => {
      resolve(event.target.result);
    };

    request.onerror = () => {
      reject('Error getting data');
    };
  });
}

async function getAllData(db, storageName) {
  return new Promise((resolve, reject) => {
    const request = db
      .transaction(storageName, 'readonly')
      .objectStore(storageName)
      .getAll();

    request.onsuccess = (event) => {
      resolve(event.target.result);
    };

    request.onerror = () => {
      reject('Error getting data');
    };
  });
}

async function getAllKeys(db, storageName) {
  return new Promise((resolve, reject) => {
    const request = db
      .transaction(storageName, 'readonly')
      .objectStore(storageName)
      .getAllKeys();

    request.onsuccess = (event) => {
      resolve(event.target.result);
    };

    request.onerror = () => {
      reject('Error getting data');
    };
  });
}

async function getDataByIndex(db, storageName, indexName, query) {
  return new Promise((resolve, reject) => {
    const results = [];
    const transaction = db.transaction(storageName);
    transaction.oncomplete = () => resolve(results);
    transaction.onerror = event => reject(event.target);
    const request = transaction.objectStore(storageName)
      .index(indexName)
      .openCursor(query);

    request.onsuccess = event => {
      const cursor = event.target.result;
      if (!cursor) {
        return;
      }

      results.push(cursor.value);
      cursor.continue();
    };
  });
}

async function getCount(db, storageName, query = null) {
  return new Promise((resolve, reject) => {
    const request = db
      .transaction(storageName, 'readonly')
      .objectStore(storageName)
      .count(query);

    request.onsuccess = (event) => {
      resolve(event.target.result);
    };

    request.onerror = () => {
      reject('Error getting data');
    };
  });
}

async function getCountByIndex(db, storageName, indexName, query = null) {
  return new Promise((resolve, reject) => {
    let count = 0;
    const transaction = db.transaction(storageName);
    transaction.oncomplete = () => resolve(count);
    transaction.onerror = event => reject(event.target);
    const request = transaction.objectStore(storageName)
      .index(indexName)
      .count(query);

    request.onsuccess = event => {
      count = event.target.result;
    };
  });
}

async function deleteByIndex(db, storageName, indexName, query) {
  let success = true;

  return new Promise((resolve, reject) => {
    const store = db
      .transaction(storageName, 'readwrite')
      .objectStore(storageName);

    const request = store
      .index(indexName)
      .openKeyCursor(query);

    request.onsuccess = event => {
      const cursor = event.target.result;
      if (cursor) {
        store.delete(cursor.primaryKey);
        cursor.continue();
      }
    };

    request.onerror = () => {
      success = false;
    };

    if (success) {
      resolve('Data deleted');
    } else {
      reject('Unable to delete data');
    }
  });
}