⚗️ 增加SauceNAO搜图
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
node_modules/
|
||||
data/
|
||||
data/
|
||||
*_test.js
|
||||
test.*
|
||||
21
apps/picSearch.js
Normal file
21
apps/picSearch.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { common, PicSearch } from '../model/index.js'
|
||||
export class newPicSearch extends plugin {
|
||||
constructor () {
|
||||
super({
|
||||
name: '椰奶图片搜索',
|
||||
event: 'message',
|
||||
priority: 2000,
|
||||
rule: [
|
||||
{
|
||||
reg: '^#?(椰奶)?搜图.*$',
|
||||
fnc: 'search'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
async search (e) {
|
||||
let msg = await PicSearch.SauceNAO(e.img[0])
|
||||
common.getforwardMsg(e, msg)
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,11 @@ class Config {
|
||||
return this.getDefOrConfig('other')
|
||||
}
|
||||
|
||||
/** 搜图配置 */
|
||||
get picSearch () {
|
||||
return this.getDefOrConfig('picSearch')
|
||||
}
|
||||
|
||||
/** 默认配置和用户配置 */
|
||||
getDefOrConfig (name) {
|
||||
let def = this.getdefSet(name)
|
||||
|
||||
1
config/config/picSearch.yaml
Normal file
1
config/config/picSearch.yaml
Normal file
@@ -0,0 +1 @@
|
||||
SauceNAOApiKey:
|
||||
1
config/default_config/picSearch.yaml
Normal file
1
config/default_config/picSearch.yaml
Normal file
@@ -0,0 +1 @@
|
||||
SauceNAOApiKey:
|
||||
46
model/PicSearch.js
Normal file
46
model/PicSearch.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/* eslint-disable no-void */
|
||||
import fetch from 'node-fetch'
|
||||
import { Config } from '../components/index.js'
|
||||
import { common } from './index.js'
|
||||
import sites from '../tools/sites.js'
|
||||
|
||||
export default new class {
|
||||
async SauceNAO (url) {
|
||||
let apiKey = Config.picSearch.SauceNAOApiKey
|
||||
if (!apiKey) return { error: '未配置SauceNAOApiKey,无法使用SauceNAO搜图,请在 https://saucenao.com/user.php?page=search-api 进行获取' }
|
||||
|
||||
let params = {
|
||||
url,
|
||||
db: 999,
|
||||
api_key: apiKey,
|
||||
output_type: 2,
|
||||
numres: 3
|
||||
}
|
||||
let res = await this.request('https://saucenao.com/search.php', params)
|
||||
if (!res) return { error: 'SauceNAO搜图请求失败' }
|
||||
let msg = await Promise.all(sites(res).map(async item => [
|
||||
`SauceNAO (${item.similarity})\n`,
|
||||
await common.proxyRequestImg(item.thumbnail),
|
||||
`\nsite:${item.site}\n`,
|
||||
`作者:${item.authorName}\n`,
|
||||
`作者主页:${item.authorUrl}\n`,
|
||||
`作品链接:${item.url[0]}`
|
||||
]))
|
||||
if (res.headers.long_remaining < 10) {
|
||||
msg.push(`SauceNAO 24h 内仅剩 ${res.headers.long_remaining} 次使用次数`)
|
||||
}
|
||||
}
|
||||
|
||||
async request (url, params, headers) {
|
||||
const qs = (obj) => {
|
||||
let res = ''
|
||||
for (const [k, v] of Object.entries(obj)) { res += `${k}=${encodeURIComponent(v)}&` }
|
||||
return res.slice(0, res.length - 1)
|
||||
}
|
||||
let proxy = await common.getAgent()
|
||||
return await fetch(url + '?' + qs(params), {
|
||||
agent: proxy,
|
||||
headers
|
||||
}).then(res => res.json()).catch(err => console.log(err))
|
||||
}
|
||||
}()
|
||||
@@ -9,6 +9,7 @@ import QQInterface from './QQInterface.js'
|
||||
import Interface from './Interface.js'
|
||||
import CronValidate from './CronValidate.js'
|
||||
import Bika from './Bika.js'
|
||||
import PicSearch from './PicSearch.js'
|
||||
export {
|
||||
puppeteer,
|
||||
common,
|
||||
@@ -20,5 +21,6 @@ export {
|
||||
CronValidate,
|
||||
GroupAdmin,
|
||||
QQInterface,
|
||||
Interface
|
||||
Interface,
|
||||
PicSearch
|
||||
}
|
||||
|
||||
381
tools/sites.js
Normal file
381
tools/sites.js
Normal file
@@ -0,0 +1,381 @@
|
||||
/* eslint-disable no-void */
|
||||
'use strict'
|
||||
// Object.defineProperty(exports, '__esModule', { value: true })
|
||||
// const errors_1 = require('./errors')
|
||||
// #region Site data objects
|
||||
const DoujinMangaLexicon = {
|
||||
name: 'The Doujinshi & Manga Lexicon',
|
||||
index: 3,
|
||||
urlMatcher: /(?:http:\/\/)?doujinshi\.mugimugi\.org\/index\.php?p=book&id=\d+/i,
|
||||
backupUrl: ({ data: { ddb_id } }) => `http://doujinshi.mugimugi.org/index.php?P=BOOK&ID=${ddb_id}`
|
||||
}
|
||||
const Pixiv = {
|
||||
name: 'Pixiv',
|
||||
index: 5,
|
||||
urlMatcher: /(?:https?:\/\/)?(?:www\.)?pixiv\.net\/member_illust\.php\?mode=.+&illust_id=\d+/i,
|
||||
backupUrl: ({ data: { pixiv_id } }) => `https://www.pixiv.net/member_illust.php?mode=medium&illust_id=${pixiv_id}`,
|
||||
authorData: ({ member_id, member_name }) => ({
|
||||
authorName: member_name,
|
||||
authorUrl: `https://www.pixiv.net/users/${member_id}`
|
||||
})
|
||||
}
|
||||
const NicoNicoSeiga = {
|
||||
name: 'Nico Nico Seiga',
|
||||
index: 8,
|
||||
urlMatcher: /(?:http:\/\/)?seiga\.nicovideo\.jp\/seiga\/im\d+/i,
|
||||
backupUrl: ({ data: { seiga_id } }) => `http://seiga.nicovideo.jp/seiga/im${seiga_id}`
|
||||
}
|
||||
const Danbooru = {
|
||||
name: 'Danbooru',
|
||||
index: 9,
|
||||
urlMatcher: /(?:https?:\/\/)?danbooru\.donmai\.us\/(?:posts|post\/show)\/\d+/i,
|
||||
backupUrl: ({ data: { danbooru_id } }) => `https://danbooru.donmai.us/posts/${danbooru_id}`
|
||||
}
|
||||
const Drawr = {
|
||||
name: 'drawr',
|
||||
index: 10,
|
||||
urlMatcher: /(?:http:\/\/)?(?:www\.)?drawr\.net\/show\.php\?id=\d+/i,
|
||||
backupUrl: ({ data: { drawr_id } }) => `http://drawr.net/show.php?id=${drawr_id}`
|
||||
}
|
||||
const Nijie = {
|
||||
name: 'Nijie',
|
||||
index: 11,
|
||||
urlMatcher: /(?:http:\/\/)?nijie\.info\/view\.php\?id=\d+/i,
|
||||
backupUrl: (data) => `http://nijie.info/view.php?id=${data.data.nijie_id}`
|
||||
}
|
||||
const Yandere = {
|
||||
name: 'Yande.re',
|
||||
index: 12,
|
||||
urlMatcher: /(?:https?:\/\/)?yande\.re\/post\/show\/\d+/i,
|
||||
backupUrl: (data) => `https://yande.re/post/show/${data.data.yandere_id}`
|
||||
}
|
||||
const OpeningsMoe = {
|
||||
name: 'Openings.moe',
|
||||
index: 13,
|
||||
urlMatcher: /(?:https?:\/\/)?openings\.moe\/\?video=.*/,
|
||||
backupUrl: (data) => `https://openings.moe/?video=${data.data.file}`
|
||||
}
|
||||
const Fakku = {
|
||||
name: 'FAKKU',
|
||||
index: 16,
|
||||
urlMatcher: /(?:https?:\/\/)?(www\.)?fakku\.net\/hentai\/[a-z-]+\d+}/i,
|
||||
backupUrl: (data) => { let _a; return `https://www.fakku.net/hentai/${(_a = data.data.source) === null || _a === void 0 ? void 0 : _a.toLowerCase().replace(' ', '-')}` }
|
||||
}
|
||||
const NHentai = {
|
||||
name: 'nHentai',
|
||||
index: 18,
|
||||
urlMatcher: /https?:\/\/nhentai.net\/g\/\d+/i,
|
||||
backupUrl: (data) => { let _a; return `https://nhentai.net/g/${(_a = data.header.thumbnail.match(/nhentai\/(\d+)/)) === null || _a === void 0 ? void 0 : _a[1]}` }
|
||||
}
|
||||
const TwoDMarket = {
|
||||
name: '2D-Market',
|
||||
index: 19,
|
||||
urlMatcher: /https?:\/\/2d-market\.com\/comic\/\d+/i,
|
||||
backupUrl: (data) => {
|
||||
let _a, _b
|
||||
return `http://2d-market.com/Comic/${(_a = data.header.thumbnail.match(/2d_market\/(\d+)/i)) === null || _a === void 0 ? void 0 : _a[1]}-${(_b = data.data.source) === null || _b === void 0 ? void 0 : _b.replace(' ', '-')}`
|
||||
}
|
||||
}
|
||||
const MediBang = {
|
||||
name: 'MediBang',
|
||||
index: 20,
|
||||
urlMatcher: /(?:https?:\/\/)?medibang\.com\/picture\/[\da-z]+/i,
|
||||
backupUrl: (data) => data.data.url
|
||||
}
|
||||
const AniDB = {
|
||||
name: 'AniDB',
|
||||
index: 21,
|
||||
urlMatcher: /(?:https?:\/\/)?anidb\.net\/perl-bin\/animedb\.pl\?show=.+&aid=\d+/i,
|
||||
backupUrl: (data) => `https://anidb.net/perl-bin/animedb.pl?show=anime&aid=${data.data.anidb_aid}`
|
||||
}
|
||||
const IMDb = {
|
||||
name: 'IMDb',
|
||||
index: 23,
|
||||
urlMatcher: /(?:https?:\/\/)?(?:www\.)?imdb\.com\/title\/.+/i,
|
||||
backupUrl: (data) => `https://www.imdb.com/title/${data.data.imdb_id}`
|
||||
}
|
||||
const Gelbooru = {
|
||||
name: 'Gelbooru',
|
||||
index: 25,
|
||||
urlMatcher: /(?:https?:\/\/)gelbooru\.com\/index\.php\?page=post&s=view&id=\d+/i,
|
||||
backupUrl: (data) => `https://gelbooru.com/index.php?page=post&s=view&id=${data.data.gelbooru_id}`
|
||||
}
|
||||
const Konachan = {
|
||||
name: 'Konachan',
|
||||
index: 26,
|
||||
urlMatcher: /(?:http:\/\/)?konachan\.com\/post\/show\/\d+/i,
|
||||
backupUrl: (data) => `https://konachan.com/post/show/${data.data.konachan_id}`
|
||||
}
|
||||
const SankakuChannel = {
|
||||
name: 'Sankaku Channel',
|
||||
index: 27,
|
||||
urlMatcher: /(?:https?:\/\/)?chan\.sankakucomplex\.com\/post\/show\/\d+/i,
|
||||
backupUrl: (data) => `https://chan.sankakucomplex.com/post/show/${data.data.sankaku_id}`
|
||||
}
|
||||
const AnimePictures = {
|
||||
name: 'Anime-Pictures',
|
||||
index: 28,
|
||||
urlMatcher: /(?:https?:\/\/)?anime-pictures\.net\/pictures\/view_post\/\d+/i,
|
||||
backupUrl: (data) => `https://anime-pictures.net/pictures/view_post/${data.data['anime-pictures_id']}`
|
||||
}
|
||||
const E621 = {
|
||||
name: 'e621',
|
||||
index: 29,
|
||||
urlMatcher: /(?:https?:\/\/)?e621\.net\/post\/show\/\d+/i,
|
||||
backupUrl: (data) => `https://e621.net/post/show/${data.data.e621_id}`
|
||||
}
|
||||
const IdolComplex = {
|
||||
name: 'Idol Complex',
|
||||
index: 30,
|
||||
urlMatcher: /(?:https?:\/\/)?idol\.sankakucomplex\.com\/post\/show\/\d+/i,
|
||||
backupUrl: (data) => `https://idol.sankakucomplex.com/post/show/${data.data.idol_id}`
|
||||
}
|
||||
const bcyIllust = {
|
||||
name: 'bcy.net Illust',
|
||||
index: 31,
|
||||
urlMatcher: /(?:http:\/\/)?bcy.net\/illust\/detail\/\d+/i,
|
||||
backupUrl: (data) => `https://bcy.net/${data.data.bcy_type}/detail/${data.data.member_link_id}/${data.data.bcy_id}`,
|
||||
authorData: ({ member_id, member_name }) => ({
|
||||
authorName: member_name,
|
||||
authorUrl: `https://bcy.net/u/${member_id}`
|
||||
})
|
||||
}
|
||||
const bcyCosplay = {
|
||||
name: 'bcy.net Cosplay',
|
||||
index: 32,
|
||||
urlMatcher: /(?:http:\/\/)?bcy.net\/coser\/detail\/\d{5}/i,
|
||||
backupUrl: (data) => `https://bcy.net/${data.data.bcy_type}/detail/${data.data.member_link_id}/${data.data.bcy_id}`
|
||||
}
|
||||
const PortalGraphics = {
|
||||
name: 'PortalGraphics',
|
||||
index: 33,
|
||||
urlMatcher: /(?:http:\/\/)?web\.archive\.org\/web\/http:\/\/www\.portalgraphics\.net\/pg\/illust\/\?image_id=\d+/i,
|
||||
backupUrl: (data) => `http://web.archive.org/web/http://www.portalgraphics.net/pg/illust/?image_id=${data.data.pg_id}`
|
||||
}
|
||||
const DeviantArt = {
|
||||
name: 'deviantArt',
|
||||
index: 34,
|
||||
urlMatcher: /(?:https:\/\/)?deviantart\.com\/view\/\d+/i,
|
||||
backupUrl: (data) => `https://deviantart.com/view/${data.data.da_id}`,
|
||||
authorData: ({ author_name: authorName, author_url: authorUrl }) => ({
|
||||
authorName,
|
||||
authorUrl
|
||||
})
|
||||
}
|
||||
const Pawoo = {
|
||||
name: 'Pawoo',
|
||||
index: 35,
|
||||
urlMatcher: /(?:https?:\/\/)?pawoo\.net\/@.+/i,
|
||||
backupUrl: (data) => `https://pawoo.net/@${data.data.user_acct}/${data.data.pawoo_id}`
|
||||
}
|
||||
const MangaUpdates = {
|
||||
name: 'Manga Updates',
|
||||
index: 36,
|
||||
urlMatcher: /(?:https:\/\/)?www\.mangaupdates\.com\/series\.html\?id=\d+/gi,
|
||||
backupUrl: (data) => `https://www.mangaupdates.com/series.html?id=${data.data.mu_id}`
|
||||
}
|
||||
const MangaDex = {
|
||||
name: 'MangaDex',
|
||||
index: 37,
|
||||
urlMatcher: /(?:https?:\/\/)?mangadex\.org\/chapter\/(\w|-)+\/(?:\d+)?/gi,
|
||||
backupUrl: (data) => `https://mangadex.org/chapter/${data.data.md_id}`,
|
||||
authorData: (data) => ({
|
||||
authorName: data.author,
|
||||
authorUrl: null
|
||||
})
|
||||
}
|
||||
const ArtStation = {
|
||||
name: 'FurAffinity',
|
||||
index: 39,
|
||||
urlMatcher: /(?:https?:\/\/)?www\.artstation\.com\/artwork\/\w+/i,
|
||||
backupUrl: (data) => `https://www.artstation.com/artwork/${data.data.as_project}`,
|
||||
authorData: (data) => ({
|
||||
authorName: data.author_name,
|
||||
authorUrl: data.author_url
|
||||
})
|
||||
}
|
||||
const FurAffinity = {
|
||||
name: 'FurAffinity',
|
||||
index: 40,
|
||||
urlMatcher: /(?:https?:\/\/)?furaffinity\.net\/view\/\d+/i,
|
||||
backupUrl: (data) => `https://furaffinity.net/view/${data.data.fa_id}`,
|
||||
authorData: (data) => ({
|
||||
authorName: data.author_name,
|
||||
authorUrl: data.author_url
|
||||
})
|
||||
}
|
||||
const Twitter = {
|
||||
name: 'Twitter',
|
||||
index: 41,
|
||||
urlMatcher: /(?:https?:\/\/)?twitter\.com\/.+/i,
|
||||
backupUrl: (data) => `https://twitter.com/i/web/status/${data.data.tweet_id}`,
|
||||
authorData: (data) => ({
|
||||
authorName: data.twitter_user_handle,
|
||||
authorUrl: `https://twitter.com/i/user/${data.twitter_user_id}`
|
||||
})
|
||||
}
|
||||
const FurryNetwork = {
|
||||
name: 'Furry Network',
|
||||
index: 42,
|
||||
urlMatcher: /(?:https?:\/\/)?furrynetwork\.com\/artwork\/\d+/i,
|
||||
backupUrl: (data) => `https://furrynetwork.com/artwork/${data.data.fn_id}`,
|
||||
authorData: (data) => ({
|
||||
authorName: data.author_name,
|
||||
authorUrl: data.author_url
|
||||
})
|
||||
}
|
||||
const Kemono = {
|
||||
name: 'Kemono',
|
||||
index: 43,
|
||||
urlMatcher: /|(?:(?:https?:\/\/)?fantia\.jp\/posts\/\d+)|(?:(?:https?:\/\/)?subscribestar\.adult\/posts\/\d+)|(?:(?:https?:\/\/)?gumroad\.com\/l\/\w+)|(?:(?:https?:\/\/)?patreon\.com\/posts\/\d+)|(?:(?:https?:\/\/)?pixiv\.net\/fanbox\/creator\/\d+\/post\/\d+)|(?:(?:https?:\/\/)?dlsite\.com\/home\/work\/=\/product_id\/\w+\.\w+)/i,
|
||||
backupUrl: (data) => {
|
||||
switch (data.data.service) {
|
||||
case 'fantia':
|
||||
return `https://fantia.jp/posts/${data.data.id}`
|
||||
case 'subscribestar':
|
||||
return `https://subscribestar.adult/posts/${data.data.id}`
|
||||
case 'gumroad':
|
||||
return `https://gumroad.com/l/${data.data.id}`
|
||||
case 'patreon':
|
||||
return `https://patreon.com/posts/${data.data.id}`
|
||||
case 'fanbox':
|
||||
return `https://pixiv.net/fanbox/creator/${data.data.user_id}/post/${data.data.id}`
|
||||
case 'dlsite':
|
||||
return `https://dlsite.com/home/work/=/${data.data.id}`
|
||||
default:
|
||||
// throw new errors_1.SagiriClientError(999, `Unknown service type for Kemono: ${data.data.service}`)
|
||||
console.error(999, `Unknown service type for Kemono: ${data.data.service}`)
|
||||
}
|
||||
},
|
||||
authorData: (data) => {
|
||||
switch (data.service) {
|
||||
case 'fantia':
|
||||
return {
|
||||
authorName: data.user_name,
|
||||
authorUrl: `https://fantia.jp/fanclubs/${data.user_id}`
|
||||
}
|
||||
case 'subscribestar':
|
||||
return {
|
||||
authorName: data.user_name,
|
||||
authorUrl: `https://subscribestar.adult/${data.user_id}`
|
||||
}
|
||||
case 'gumroad':
|
||||
return {
|
||||
authorName: data.user_name,
|
||||
authorUrl: `https://gumroad.com/${data.user_id}`
|
||||
}
|
||||
case 'patreon':
|
||||
return {
|
||||
authorName: data.user_name,
|
||||
authorUrl: `https://patreon.com/user?u=${data.user_id}`
|
||||
}
|
||||
case 'fanbox':
|
||||
return {
|
||||
authorName: data.user_name,
|
||||
authorUrl: `https://pixiv.net/fanbox/creator/${data.user_id}`
|
||||
}
|
||||
case 'dlsite':
|
||||
return {
|
||||
authorName: data.user_name,
|
||||
authorUrl: `https://dlsite.com/eng/cicrle/profile/=/marker_id/${data.user_id}`
|
||||
}
|
||||
default:
|
||||
// throw new errors_1.SagiriClientError(999, `Unknown service type for Kemono: ${data.service}`)
|
||||
console.error(999, `Unknown service type for Kemono: ${data.service}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
const Skeb = {
|
||||
name: 'Skeb',
|
||||
index: 44,
|
||||
urlMatcher: /(?:(?:https?:\/\/)?skeb\.jp\/@\w+\/works\/\d+)/i,
|
||||
backupUrl: (data) => `https://skeb.jp${data.data.path}`,
|
||||
authorData: (data) => ({
|
||||
authorName: data.creator_name,
|
||||
authorUrl: data.author_url
|
||||
})
|
||||
}
|
||||
// #endregion
|
||||
const sites = {
|
||||
3: DoujinMangaLexicon,
|
||||
4: DoujinMangaLexicon,
|
||||
5: Pixiv,
|
||||
6: Pixiv,
|
||||
8: NicoNicoSeiga,
|
||||
9: Danbooru,
|
||||
10: Drawr,
|
||||
11: Nijie,
|
||||
12: Yandere,
|
||||
13: OpeningsMoe,
|
||||
16: Fakku,
|
||||
18: NHentai,
|
||||
19: TwoDMarket,
|
||||
20: MediBang,
|
||||
21: AniDB,
|
||||
22: AniDB,
|
||||
23: IMDb,
|
||||
24: IMDb,
|
||||
25: Gelbooru,
|
||||
26: Konachan,
|
||||
27: SankakuChannel,
|
||||
28: AnimePictures,
|
||||
29: E621,
|
||||
30: IdolComplex,
|
||||
31: bcyIllust,
|
||||
32: bcyCosplay,
|
||||
33: PortalGraphics,
|
||||
34: DeviantArt,
|
||||
35: Pawoo,
|
||||
36: MangaUpdates,
|
||||
37: MangaDex,
|
||||
371: MangaDex,
|
||||
// 38
|
||||
39: ArtStation,
|
||||
40: FurAffinity,
|
||||
41: Twitter,
|
||||
42: FurryNetwork,
|
||||
43: Kemono,
|
||||
44: Skeb
|
||||
}
|
||||
const resolveResult = item => {
|
||||
let _a
|
||||
const { data, header } = item
|
||||
const id = header.index_id
|
||||
if (!sites[id]) { throw new Error(`Cannot resolve data for unknown index ${id}`) }
|
||||
const { name, urlMatcher, backupUrl, authorData } = sites[id]
|
||||
let url
|
||||
if (data.ext_urls && data.ext_urls.length > 1) {
|
||||
url = data.ext_urls.filter((url) => urlMatcher.test(url))
|
||||
} else if (data.ext_urls) {
|
||||
url = data.ext_urls
|
||||
}
|
||||
if (!url) url = backupUrl(item)
|
||||
return Object.assign({
|
||||
id,
|
||||
url,
|
||||
name
|
||||
}, ((_a = authorData === null || authorData === void 0 ? void 0 : authorData(item.data)) !== null && _a !== void 0 ? _a : { authorName: null, authorUrl: null }))
|
||||
}
|
||||
|
||||
export default (response) => {
|
||||
const unknownIds = new Set(response.results.filter((result) => !sites[result.header.index_id]).map((result) => result.header.index_id))
|
||||
const results = response.results
|
||||
.filter((result) => !unknownIds.has(result.header.index_id))
|
||||
.sort((a, b) => b.header.similarity - a.header.similarity)
|
||||
|
||||
return results.map((result) => {
|
||||
const { url, name, id, authorName, authorUrl } = resolveResult(result)
|
||||
const { header: { similarity, thumbnail } } = result
|
||||
return {
|
||||
url,
|
||||
site: name,
|
||||
index: id,
|
||||
similarity: Number(similarity),
|
||||
thumbnail,
|
||||
authorName,
|
||||
authorUrl,
|
||||
raw: result
|
||||
}
|
||||
})
|
||||
}
|
||||
// # sourceMappingURL=sites.js.map
|
||||
Reference in New Issue
Block a user