fix sync
This commit is contained in:
@@ -21,7 +21,7 @@ import { sha256 } from "../libs/utils";
|
|||||||
* @param {*} data
|
* @param {*} data
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiSyncData = async (url, key, data, isBg = false) =>
|
export const apiSyncData = async (url, key, data) =>
|
||||||
fetchPolyfill(url, {
|
fetchPolyfill(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
@@ -29,16 +29,14 @@ export const apiSyncData = async (url, key, data, isBg = false) =>
|
|||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
isBg,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载数据
|
* 下载数据
|
||||||
* @param {*} url
|
* @param {*} url
|
||||||
* @param {*} isBg
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiFetch = (url, isBg = false) => fetchPolyfill(url, { isBg });
|
export const apiFetch = (url) => fetchPolyfill(url);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 谷歌翻译
|
* 谷歌翻译
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ browser.runtime.onStartup.addListener(async () => {
|
|||||||
console.log("browser onStartup");
|
console.log("browser onStartup");
|
||||||
|
|
||||||
// 同步数据
|
// 同步数据
|
||||||
await trySyncSettingAndRules(true);
|
await trySyncSettingAndRules();
|
||||||
|
|
||||||
// 清除缓存
|
// 清除缓存
|
||||||
const setting = await getSettingWithDefault();
|
const setting = await getSettingWithDefault();
|
||||||
@@ -41,7 +41,7 @@ browser.runtime.onStartup.addListener(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 同步订阅规则
|
// 同步订阅规则
|
||||||
trySyncAllSubRules(setting, true);
|
trySyncAllSubRules(setting);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -302,10 +302,11 @@ export const DEFAULT_SYNC = {
|
|||||||
syncUrl: "", // 数据同步接口
|
syncUrl: "", // 数据同步接口
|
||||||
syncUser: "", // 数据同步用户名
|
syncUser: "", // 数据同步用户名
|
||||||
syncKey: "", // 数据同步密钥
|
syncKey: "", // 数据同步密钥
|
||||||
settingUpdateAt: 0,
|
syncMeta: {}, // 数据更新及同步信息
|
||||||
settingSyncAt: 0,
|
// settingUpdateAt: 0,
|
||||||
rulesUpdateAt: 0,
|
// settingSyncAt: 0,
|
||||||
rulesSyncAt: 0,
|
// rulesUpdateAt: 0,
|
||||||
|
// rulesSyncAt: 0,
|
||||||
subRulesSyncAt: 0, // 订阅规则同步时间
|
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||||
dataCaches: {}, // 缓存同步时间
|
dataCaches: {}, // 缓存同步时间
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { STOKEY_RULES, DEFAULT_RULES } from "../config";
|
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
import { trySyncRules } from "../libs/sync";
|
import { trySyncRules } from "../libs/sync";
|
||||||
import { checkRules } from "../libs/rules";
|
import { checkRules } from "../libs/rules";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useSyncMeta } from "./Sync";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 规则 hook
|
* 规则 hook
|
||||||
@@ -10,13 +11,15 @@ import { useCallback } from "react";
|
|||||||
*/
|
*/
|
||||||
export function useRules() {
|
export function useRules() {
|
||||||
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
||||||
|
const { updateSyncMeta } = useSyncMeta();
|
||||||
|
|
||||||
const updateRules = useCallback(
|
const updateRules = useCallback(
|
||||||
async (rules) => {
|
async (rules) => {
|
||||||
await save(rules);
|
await save(rules);
|
||||||
trySyncRules(false, true);
|
await updateSyncMeta(KV_RULES_KEY);
|
||||||
|
trySyncRules();
|
||||||
},
|
},
|
||||||
[save]
|
[save, updateSyncMeta]
|
||||||
);
|
);
|
||||||
|
|
||||||
const add = useCallback(
|
const add = useCallback(
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { STOKEY_SETTING, DEFAULT_SETTING } from "../config";
|
import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
import { trySyncSetting } from "../libs/sync";
|
import { trySyncSetting } from "../libs/sync";
|
||||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||||
import { debounce } from "../libs/utils";
|
import { debounce } from "../libs/utils";
|
||||||
|
import { useSyncMeta } from "./Sync";
|
||||||
|
|
||||||
const SettingContext = createContext({
|
const SettingContext = createContext({
|
||||||
setting: null,
|
setting: null,
|
||||||
@@ -12,11 +13,12 @@ const SettingContext = createContext({
|
|||||||
|
|
||||||
export function SettingProvider({ children }) {
|
export function SettingProvider({ children }) {
|
||||||
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
|
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
|
||||||
|
const { updateSyncMeta } = useSyncMeta();
|
||||||
|
|
||||||
const syncSetting = useMemo(
|
const syncSetting = useMemo(
|
||||||
() =>
|
() =>
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
trySyncSetting(false, true);
|
trySyncSetting();
|
||||||
}, [2000]),
|
}, [2000]),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -24,9 +26,10 @@ export function SettingProvider({ children }) {
|
|||||||
const updateSetting = useCallback(
|
const updateSetting = useCallback(
|
||||||
async (obj) => {
|
async (obj) => {
|
||||||
await update(obj);
|
await update(obj);
|
||||||
|
await updateSyncMeta(KV_SETTING_KEY);
|
||||||
syncSetting();
|
syncSetting();
|
||||||
},
|
},
|
||||||
[update, syncSetting]
|
[update, syncSetting, updateSyncMeta]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -11,6 +11,23 @@ export function useSync() {
|
|||||||
return { sync: data, updateSync: update, reloadSync: reload };
|
return { sync: data, updateSync: update, reloadSync: reload };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update syncmeta hook
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useSyncMeta() {
|
||||||
|
const { sync, updateSync } = useSync();
|
||||||
|
const updateSyncMeta = useCallback(
|
||||||
|
async (key) => {
|
||||||
|
const syncMeta = sync?.syncMeta || {};
|
||||||
|
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
|
||||||
|
await updateSync({ syncMeta });
|
||||||
|
},
|
||||||
|
[sync, updateSync]
|
||||||
|
);
|
||||||
|
return { updateSyncMeta };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* caches sync hook
|
* caches sync hook
|
||||||
* @param {*} url
|
* @param {*} url
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
DEFAULT_FETCH_LIMIT,
|
DEFAULT_FETCH_LIMIT,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { msAuth } from "./auth";
|
import { msAuth } from "./auth";
|
||||||
|
import { isBg } from "./browser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 油猴脚本的请求封装
|
* 油猴脚本的请求封装
|
||||||
@@ -176,13 +177,13 @@ export const fetchData = async (
|
|||||||
* @param {*} opts
|
* @param {*} opts
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchPolyfill = async (input, { isBg = false, ...opts } = {}) => {
|
export const fetchPolyfill = async (input, opts) => {
|
||||||
if (!input.trim()) {
|
if (!input.trim()) {
|
||||||
throw new Error("URL is empty");
|
throw new Error("URL is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件
|
// 插件
|
||||||
if (isExt && !isBg) {
|
if (isExt && !isBg()) {
|
||||||
const res = await sendBgMsg(MSG_FETCH, { input, opts });
|
const res = await sendBgMsg(MSG_FETCH, { input, opts });
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(res.error);
|
throw new Error(res.error);
|
||||||
|
|||||||
@@ -149,5 +149,5 @@ export const saveRule = async (newRule) => {
|
|||||||
rules.unshift(newRule);
|
rules.unshift(newRule);
|
||||||
}
|
}
|
||||||
await setRules(rules);
|
await setRules(rules);
|
||||||
trySyncRules(false, true);
|
trySyncRules();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ const updateSyncDataCache = async (url) => {
|
|||||||
* @param {*} url
|
* @param {*} url
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const syncSubRules = async (url, isBg = false) => {
|
export const syncSubRules = async (url) => {
|
||||||
const res = await apiFetch(url, isBg);
|
const res = await apiFetch(url);
|
||||||
const rules = checkRules(res).filter(
|
const rules = checkRules(res).filter(
|
||||||
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
|
({ pattern }) => !isAllchar(pattern, GLOBAL_KEY)
|
||||||
);
|
);
|
||||||
@@ -41,10 +41,10 @@ export const syncSubRules = async (url, isBg = false) => {
|
|||||||
* @param {*} url
|
* @param {*} url
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const syncAllSubRules = async (subrulesList, isBg = false) => {
|
export const syncAllSubRules = async (subrulesList) => {
|
||||||
for (let subrules of subrulesList) {
|
for (let subrules of subrulesList) {
|
||||||
try {
|
try {
|
||||||
await syncSubRules(subrules.url, isBg);
|
await syncSubRules(subrules.url);
|
||||||
await updateSyncDataCache(subrules.url);
|
await updateSyncDataCache(subrules.url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`[sync subrule error]: ${subrules.url}`, err);
|
console.log(`[sync subrule error]: ${subrules.url}`, err);
|
||||||
@@ -57,14 +57,14 @@ export const syncAllSubRules = async (subrulesList, isBg = false) => {
|
|||||||
* @param {*} url
|
* @param {*} url
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const trySyncAllSubRules = async ({ subrulesList }, isBg = false) => {
|
export const trySyncAllSubRules = async ({ subrulesList }) => {
|
||||||
try {
|
try {
|
||||||
const { subRulesSyncAt } = await getSyncWithDefault();
|
const { subRulesSyncAt } = await getSyncWithDefault();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
const interval = 24 * 60 * 60 * 1000; // 间隔一天
|
||||||
if (now - subRulesSyncAt > interval) {
|
if (now - subRulesSyncAt > interval) {
|
||||||
// 同步订阅规则
|
// 同步订阅规则
|
||||||
await syncAllSubRules(subrulesList, isBg);
|
await syncAllSubRules(subrulesList);
|
||||||
await updateSync({ subRulesSyncAt: now });
|
await updateSync({ subRulesSyncAt: now });
|
||||||
|
|
||||||
// 同步修复规则
|
// 同步修复规则
|
||||||
|
|||||||
191
src/libs/sync.js
191
src/libs/sync.js
@@ -26,117 +26,90 @@ getPatcher().patch("request", (opts) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const syncByWebdav = async ({
|
const syncByWebdav = async (data, { syncUrl, syncUser, syncKey }) => {
|
||||||
key,
|
|
||||||
value,
|
|
||||||
syncUrl,
|
|
||||||
syncUser,
|
|
||||||
syncKey,
|
|
||||||
updateAt = 0,
|
|
||||||
syncAt = 0,
|
|
||||||
isForce = false,
|
|
||||||
}) => {
|
|
||||||
const client = createClient(syncUrl, {
|
const client = createClient(syncUrl, {
|
||||||
username: syncUser,
|
username: syncUser,
|
||||||
password: syncKey,
|
password: syncKey,
|
||||||
});
|
});
|
||||||
const pathname = `/${APP_LCNAME}`;
|
const pathname = `/${APP_LCNAME}`;
|
||||||
const filename = `/${APP_LCNAME}/${key}`;
|
const filename = `/${APP_LCNAME}/${data.key}`;
|
||||||
const data = JSON.stringify(value, null, " ");
|
|
||||||
|
|
||||||
if ((await client.exists(pathname)) === false) {
|
if ((await client.exists(pathname)) === false) {
|
||||||
await client.createDirectory(pathname);
|
await client.createDirectory(pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isExist = await client.exists(filename);
|
const isExist = await client.exists(filename);
|
||||||
if (isExist && !isForce) {
|
if (isExist) {
|
||||||
const { lastmod } = await client.stat(filename);
|
const cont = await client.getFileContents(filename, { format: "text" });
|
||||||
const fileUpdateAt = Date.parse(lastmod);
|
const webData = JSON.parse(cont);
|
||||||
if (syncAt === 0 || fileUpdateAt > updateAt) {
|
if (webData.updateAt >= data.updateAt) {
|
||||||
const data = await client.getFileContents(filename, { format: "text" });
|
return webData;
|
||||||
return { updateAt: fileUpdateAt, value: JSON.parse(data) };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.putFileContents(filename, data);
|
await client.putFileContents(filename, JSON.stringify(data, null, " "));
|
||||||
const { lastmod } = await client.stat(filename);
|
return data;
|
||||||
const fileUpdateAt = Date.parse(lastmod);
|
|
||||||
return { updateAt: fileUpdateAt, value };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncByWorker = async ({
|
const syncByWorker = async (data, { syncUrl, syncKey }) => {
|
||||||
key,
|
return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
|
||||||
value,
|
};
|
||||||
|
|
||||||
|
const syncData = async (key, valueFn) => {
|
||||||
|
const {
|
||||||
|
syncType,
|
||||||
syncUrl,
|
syncUrl,
|
||||||
|
syncUser,
|
||||||
syncKey,
|
syncKey,
|
||||||
updateAt = 0,
|
syncMeta = {},
|
||||||
syncAt = 0,
|
} = await getSyncWithDefault();
|
||||||
isBg = false,
|
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
|
||||||
isForce = false,
|
throw new Error("sync setting err");
|
||||||
}) => {
|
|
||||||
if (isForce) {
|
|
||||||
updateAt = Date.now();
|
|
||||||
}
|
}
|
||||||
return await apiSyncData(
|
|
||||||
`${syncUrl}/sync`,
|
let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
|
||||||
syncKey,
|
syncAt === 0 && (updateAt = 0);
|
||||||
{
|
|
||||||
|
const value = await valueFn();
|
||||||
|
const data = {
|
||||||
key,
|
key,
|
||||||
value,
|
value: JSON.stringify(value),
|
||||||
updateAt: syncAt === 0 ? 0 : updateAt,
|
updateAt,
|
||||||
},
|
};
|
||||||
isBg
|
const args = {
|
||||||
);
|
syncUrl,
|
||||||
|
syncUser,
|
||||||
|
syncKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res =
|
||||||
|
syncType === OPT_SYNCTYPE_WEBDAV
|
||||||
|
? await syncByWebdav(data, args)
|
||||||
|
: await syncByWorker(data, args);
|
||||||
|
|
||||||
|
syncMeta[key] = {
|
||||||
|
updateAt: res.updateAt,
|
||||||
|
syncAt: Date.now(),
|
||||||
|
};
|
||||||
|
await updateSync({ syncMeta });
|
||||||
|
|
||||||
|
return [JSON.parse(res.value), res.updateAt > updateAt];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步设置
|
* 同步设置
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const syncSetting = async (isBg = false, isForce = false) => {
|
const syncSetting = async () => {
|
||||||
const {
|
const [value, isNew] = await syncData(KV_SETTING_KEY, getSettingWithDefault);
|
||||||
syncType,
|
if (isNew) {
|
||||||
syncUrl,
|
await setSetting(value);
|
||||||
syncUser,
|
|
||||||
syncKey,
|
|
||||||
settingUpdateAt = 0,
|
|
||||||
settingSyncAt = 0,
|
|
||||||
} = await getSyncWithDefault();
|
|
||||||
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setting = await getSettingWithDefault();
|
|
||||||
const args = {
|
|
||||||
key: KV_SETTING_KEY,
|
|
||||||
value: setting,
|
|
||||||
syncUrl,
|
|
||||||
syncUser,
|
|
||||||
syncKey,
|
|
||||||
updateAt: settingUpdateAt,
|
|
||||||
syncAt: settingSyncAt,
|
|
||||||
isBg,
|
|
||||||
isForce,
|
|
||||||
};
|
|
||||||
const res =
|
|
||||||
syncType === OPT_SYNCTYPE_WEBDAV
|
|
||||||
? await syncByWebdav(args)
|
|
||||||
: await syncByWorker(args);
|
|
||||||
|
|
||||||
if (res.updateAt > settingUpdateAt) {
|
|
||||||
await setSetting(res.value);
|
|
||||||
}
|
|
||||||
await updateSync({
|
|
||||||
settingUpdateAt: res.updateAt,
|
|
||||||
settingSyncAt: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.value;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const trySyncSetting = async (isBg = false, isForce = false) => {
|
export const trySyncSetting = async () => {
|
||||||
try {
|
try {
|
||||||
return await syncSetting(isBg, isForce);
|
await syncSetting();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[sync setting]", err);
|
console.log("[sync setting]", err);
|
||||||
}
|
}
|
||||||
@@ -146,50 +119,16 @@ export const trySyncSetting = async (isBg = false, isForce = false) => {
|
|||||||
* 同步规则
|
* 同步规则
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const syncRules = async (isBg = false, isForce = false) => {
|
const syncRules = async () => {
|
||||||
const {
|
const [value, isNew] = await syncData(KV_RULES_KEY, getRulesWithDefault);
|
||||||
syncType,
|
if (isNew) {
|
||||||
syncUrl,
|
await setRules(value);
|
||||||
syncUser,
|
|
||||||
syncKey,
|
|
||||||
rulesUpdateAt = 0,
|
|
||||||
rulesSyncAt = 0,
|
|
||||||
} = await getSyncWithDefault();
|
|
||||||
if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules = await getRulesWithDefault();
|
|
||||||
const args = {
|
|
||||||
key: KV_RULES_KEY,
|
|
||||||
value: rules,
|
|
||||||
syncUrl,
|
|
||||||
syncUser,
|
|
||||||
syncKey,
|
|
||||||
updateAt: rulesUpdateAt,
|
|
||||||
syncAt: rulesSyncAt,
|
|
||||||
isBg,
|
|
||||||
isForce,
|
|
||||||
};
|
|
||||||
const res =
|
|
||||||
syncType === OPT_SYNCTYPE_WEBDAV
|
|
||||||
? await syncByWebdav(args)
|
|
||||||
: await syncByWorker(args);
|
|
||||||
|
|
||||||
if (res.updateAt > rulesUpdateAt) {
|
|
||||||
await setRules(res.value);
|
|
||||||
}
|
|
||||||
await updateSync({
|
|
||||||
rulesUpdateAt: res.updateAt,
|
|
||||||
rulesSyncAt: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.value;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const trySyncRules = async (isBg = false, isForce = false) => {
|
export const trySyncRules = async () => {
|
||||||
try {
|
try {
|
||||||
return await syncRules(isBg, isForce);
|
await syncRules();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[sync user rules]", err);
|
console.log("[sync user rules]", err);
|
||||||
}
|
}
|
||||||
@@ -219,10 +158,12 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => {
|
|||||||
* 同步个人设置和规则
|
* 同步个人设置和规则
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const syncSettingAndRules = async (isBg = false) => {
|
export const syncSettingAndRules = async () => {
|
||||||
return [await syncSetting(isBg), await syncRules(isBg)];
|
await syncSetting();
|
||||||
|
await syncRules();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const trySyncSettingAndRules = async (isBg = false) => {
|
export const trySyncSettingAndRules = async () => {
|
||||||
return [await trySyncSetting(isBg), await trySyncRules(isBg)];
|
await trySyncSetting();
|
||||||
|
await trySyncRules();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user