Browse Source
- Worker to process link-ingest jobs - Link preview template w/oembed support - CSS for typical link presentation in light & dark themesdevelop
31 changed files with 979 additions and 102 deletions
@ -0,0 +1,264 @@ |
|||
// image.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import path from 'node:path'; |
|||
import fs from 'node:fs'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
const StreamRayImage = mongoose.model('Image'); |
|||
|
|||
import sharp from 'sharp'; |
|||
|
|||
import { SiteService, SiteAsync } from '../../lib/site-lib.js'; |
|||
|
|||
export default class ImageService extends SiteService { |
|||
|
|||
static get name ( ) { return 'ImageService'; } |
|||
static get slug ( ) { return 'image'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, ImageService); |
|||
} |
|||
|
|||
async start ( ) { |
|||
await super.start(); |
|||
|
|||
const { user: userService } = this.dtp.services; |
|||
this.populateImage = [ |
|||
{ |
|||
path: 'owner', |
|||
select: userService.USER_SELECT, |
|||
}, |
|||
]; |
|||
|
|||
await fs.promises.mkdir(process.env.IMAGE_WORK_PATH, { recursive: true }); |
|||
} |
|||
|
|||
async create (owner, imageDefinition, file) { |
|||
const NOW = new Date(); |
|||
const { chat: chatService, minio: minioService } = this.dtp.services; |
|||
|
|||
this.log.debug('processing uploaded image', { imageDefinition, file }); |
|||
|
|||
const sharpImage = await sharp(file.path); |
|||
const metadata = await sharpImage.metadata(); |
|||
|
|||
// create an Image model instance, but leave it here in application memory.
|
|||
// we don't persist it to the db until MinIO accepts the binary data.
|
|||
const image = new StreamRayImage(); |
|||
image.created = NOW; |
|||
image.owner = owner._id; |
|||
if (imageDefinition.caption) { |
|||
image.caption = chatService.filterText(imageDefinition.caption); |
|||
} |
|||
image.flags.isSensitive = imageDefinition['flags.isSensitive'] === 'on'; |
|||
image.flags.isPendingAttachment = imageDefinition['flags.isPendingAttachment'] === 'on'; |
|||
|
|||
image.type = file.mimetype; |
|||
image.size = file.size; |
|||
|
|||
image.file.bucket = process.env.MINIO_IMAGE_BUCKET; |
|||
image.metadata = this.makeImageMetadata(metadata); |
|||
|
|||
const imageId = image._id.toString(); |
|||
const ownerId = owner._id.toString(); |
|||
const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/${imageId.slice(0, 3)}/${imageId}`; |
|||
image.file.key = fileKey; |
|||
|
|||
// upload the image file to MinIO
|
|||
const response = await minioService.uploadFile({ |
|||
bucket: image.file.bucket, |
|||
key: image.file.key, |
|||
filePath: file.path, |
|||
metadata: { |
|||
'Content-Type': file.mimetype, |
|||
'Content-Length': file.size, |
|||
}, |
|||
}); |
|||
|
|||
// store the eTag from MinIO in the Image model
|
|||
image.file.etag = response.etag; |
|||
|
|||
// save the Image model to the db
|
|||
await image.save(); |
|||
|
|||
this.log.info('processed uploaded image', { ownerId, imageId, fileKey }); |
|||
return image.toObject(); |
|||
} |
|||
|
|||
async getImageById (imageId) { |
|||
const image = await StreamRayImage |
|||
.findById(imageId) |
|||
.populate(this.populateImage); |
|||
return image; |
|||
} |
|||
|
|||
async getRecentImagesForOwner (owner) { |
|||
const images = await StreamRayImage |
|||
.find({ owner: owner._id }) |
|||
.sort({ created: -1 }) |
|||
.limit(10) |
|||
.populate(this.populateImage) |
|||
.lean(); |
|||
return images; |
|||
} |
|||
|
|||
async deleteImage (image) { |
|||
const { minio: minioService } = this.dtp.services; |
|||
try { |
|||
if (image.file && image.file.bucket && image.file.key) { |
|||
// this.log.debug('removing image from storage', { file: image.file });
|
|||
await minioService.removeObject(image.file.bucket, image.file.key); |
|||
} |
|||
} catch (error) { |
|||
this.log.error('failed to remove image from storage', { error }); |
|||
// fall through
|
|||
} |
|||
await StreamRayImage.deleteOne({ _id: image._id }); |
|||
} |
|||
|
|||
async processImageFile (owner, file, outputs, options) { |
|||
this.log.debug('processing image file', { owner, file, outputs }); |
|||
const sharpImage = sharp(file.path); |
|||
return this.processImage(owner, sharpImage, outputs, options); |
|||
} |
|||
|
|||
async processImage (owner, sharpImage, outputs, options) { |
|||
const NOW = new Date(); |
|||
const service = this; |
|||
const { minio: minioService } = this.dtp.services; |
|||
|
|||
options = Object.assign({ |
|||
removeWorkFiles: true, |
|||
}, options); |
|||
|
|||
const imageWorkPath = process.env.IMAGE_WORK_PATH || '/tmp'; |
|||
const metadata = await sharpImage.metadata(); |
|||
|
|||
async function processOutputImage (output) { |
|||
const outputMetadata = service.makeImageMetadata(metadata); |
|||
outputMetadata.width = output.width; |
|||
outputMetadata.height = output.height; |
|||
|
|||
service.log.debug('processing image', { output, outputMetadata }); |
|||
|
|||
const image = new StreamRayImage(); |
|||
image.created = NOW; |
|||
image.owner = owner._id; |
|||
image.type = `image/${output.format}`; |
|||
image.metadata = outputMetadata; |
|||
|
|||
try { |
|||
let chain = sharpImage.clone().resize({ width: output.width, height: output.height }); |
|||
chain = chain[output.format](output.formatParameters); |
|||
|
|||
output.filePath = path.join(imageWorkPath, `${image._id}.${output.width}x${output.height}.${output.format}`); |
|||
output.mimetype = `image/${output.format}`; |
|||
await chain.toFile(output.filePath); |
|||
output.stat = await fs.promises.stat(output.filePath); |
|||
} catch (error) { |
|||
service.log.error('failed to process output image', { output, error }); |
|||
throw error; |
|||
} |
|||
|
|||
try { |
|||
const imageId = image._id.toString(); |
|||
const ownerId = owner._id.toString(); |
|||
const fileKey = `/${ownerId.slice(0, 3)}/${ownerId}/images/${imageId.slice(0, 3)}/${imageId}.${output.format}`; |
|||
|
|||
image.file.bucket = process.env.MINIO_IMAGE_BUCKET; |
|||
image.file.key = fileKey; |
|||
image.size = output.stat.size; |
|||
|
|||
// upload the image file to MinIO
|
|||
const response = await minioService.uploadFile({ |
|||
bucket: image.file.bucket, |
|||
key: image.file.key, |
|||
filePath: output.filePath, |
|||
metadata: { |
|||
'Content-Type': output.mimetype, |
|||
'Content-Length': output.stat.size, |
|||
}, |
|||
}); |
|||
|
|||
// store the eTag from MinIO in the Image model
|
|||
image.file.etag = response.etag; |
|||
|
|||
// save the Image model to the db
|
|||
await image.save(); |
|||
|
|||
service.log.info('processed uploaded image', { ownerId, imageId, fileKey }); |
|||
|
|||
if (options.removeWorkFiles) { |
|||
service.log.debug('removing work file', { path: output.filePath }); |
|||
await fs.promises.unlink(output.filePath); |
|||
delete output.filePath; |
|||
} |
|||
|
|||
output.image = { |
|||
_id: image._id, |
|||
bucket: image.file.bucket, |
|||
key: image.file.key, |
|||
}; |
|||
} catch (error) { |
|||
service.log.error('failed to persist output image', { output, error }); |
|||
if (options.removeWorkFiles) { |
|||
service.log.debug('removing work file', { path: output.filePath }); |
|||
await SiteAsync.each(outputs, async (output) => { |
|||
await fs.promises.unlink(output.filePath); |
|||
delete output.filePath; |
|||
}, 4); |
|||
} |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
await SiteAsync.each(outputs, processOutputImage, 4); |
|||
} |
|||
|
|||
makeImageMetadata (metadata) { |
|||
return { |
|||
format: metadata.format, |
|||
size: metadata.size, |
|||
width: metadata.width, |
|||
height: metadata.height, |
|||
space: metadata.space, |
|||
channels: metadata.channels, |
|||
depth: metadata.depth, |
|||
density: metadata.density, |
|||
hasAlpha: metadata.hasAlpha, |
|||
orientation: metadata.orientation, |
|||
}; |
|||
} |
|||
|
|||
async reportStats ( ) { |
|||
const chart = await StreamRayImage.aggregate([ |
|||
{ |
|||
$match: { }, |
|||
}, |
|||
{ |
|||
$group: { |
|||
_id: { $dateToString: { format: '%Y-%m', date: '$created' } }, |
|||
count: { $sum: 1 }, |
|||
bytes: { $sum: '$size' }, |
|||
}, |
|||
}, |
|||
{ |
|||
$sort: { _id: 1 }, |
|||
}, |
|||
]); |
|||
|
|||
const stats = { }; |
|||
stats.count = chart.reduce((prev, value) => { |
|||
return prev + value.count; |
|||
}, 0); |
|||
stats.bytes = chart.reduce((prev, value) => { |
|||
return prev + value.bytes; |
|||
}, 0); |
|||
|
|||
return { chart, stats }; |
|||
} |
|||
} |
@ -0,0 +1,280 @@ |
|||
// host-services.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import 'dotenv/config'; |
|||
|
|||
import path, { dirname } from 'path'; |
|||
import fs from 'node:fs'; |
|||
import readline from 'node:readline'; |
|||
|
|||
import { SiteRuntime } from '../../lib/site-lib.js'; |
|||
|
|||
import { CronJob } from 'cron'; |
|||
const CRON_TIMEZONE = 'America/New_York'; |
|||
|
|||
import { Readable, pipeline } from 'node:stream'; |
|||
import { promisify } from 'node:util'; |
|||
const streamPipeline = promisify(pipeline); |
|||
|
|||
class ChatLinksService extends SiteRuntime { |
|||
|
|||
static get name ( ) { return 'ChatLinksWorker'; } |
|||
static get slug ( ) { return 'chatLinks'; } |
|||
|
|||
static get BLOCKLIST_URL ( ) { return 'https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn/hosts'; } |
|||
|
|||
constructor (rootPath) { |
|||
super(ChatLinksService, rootPath); |
|||
} |
|||
|
|||
async start ( ) { |
|||
await super.start(); |
|||
|
|||
const mongoose = await import('mongoose'); |
|||
this.Link = mongoose.model('Link'); |
|||
|
|||
this.viewModel = { }; |
|||
await this.populateViewModel(this.viewModel); |
|||
|
|||
this.blacklist = { |
|||
porn: path.join(this.config.root, 'data', 'blacklist', 'porn'), |
|||
}; |
|||
|
|||
/* |
|||
* Bull Queue job processors |
|||
*/ |
|||
|
|||
this.log.info('registering link-ingest job processor', { config: this.config.jobQueues.links }); |
|||
this.linksProcessingQueue = this.services.jobQueue.getJobQueue('links', this.config.jobQueues.links); |
|||
this.linksProcessingQueue.process('link-ingest', 1, this.ingestLink.bind(this)); |
|||
|
|||
/* |
|||
* Cron jobs |
|||
*/ |
|||
|
|||
const cronBlacklistUpdate = '0 0 3 * * *'; // Ever day at 3:00 a.m.
|
|||
this.log.info('created URL blacklist update cron', { cronBlacklistUpdate }); |
|||
this.updateBlacklistJob = new CronJob( |
|||
cronBlacklistUpdate, |
|||
this.updateUrlBlacklist.bind(this), |
|||
null, |
|||
true, |
|||
CRON_TIMEZONE, |
|||
); |
|||
} |
|||
|
|||
async shutdown ( ) { |
|||
this.log.alert('ChatLinksWorker shutting down'); |
|||
await super.shutdown(); |
|||
} |
|||
|
|||
async ingestLink (job) { |
|||
const { link: linkService, user: userService } = this.services; |
|||
this.log.info('received link ingest job', { data: job.data }); |
|||
|
|||
try { |
|||
if (!job.data.submitterId) { |
|||
this.log.error('link ingest submitted without submitterId'); |
|||
return; |
|||
} |
|||
|
|||
job.data.submitter = await userService.getUserAccount(job.data.submitterId); |
|||
if (!job.data.submitter) { |
|||
this.log.error('link submitted with invalid User', { submitterId: job.data.submitterId }); |
|||
return; |
|||
} |
|||
|
|||
/* |
|||
* Is the submitter blocked from sharing links? |
|||
*/ |
|||
if (!job.data.submitter.permissions.canShareLinks) { |
|||
this.log.alert('Submitter is not permitted to share links', { |
|||
submitter: { |
|||
_id: job.data.submitter._id, |
|||
username: job.data.submitter.username, |
|||
}, |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
this.log.info('fetching link from database'); |
|||
job.data.link = await linkService.getById(job.data.linkId); |
|||
if (!job.data.link) { |
|||
this.log.error('link not found in database', { linkId: job.data.linkId }); |
|||
return; |
|||
} |
|||
|
|||
/* |
|||
* Is the domain or URL already known to be blocked? |
|||
*/ |
|||
if (job.data.link.flags && job.data.link.flags.isBlocked) { |
|||
this.log.alert('aborting ingest of blocked link', { |
|||
submitter: { |
|||
_id: job.data.submitter._id, |
|||
username: job.data.submitter.username, |
|||
}, |
|||
domain: job.data.link.domain, |
|||
url: job.data.link.url, |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
/* |
|||
* Is the domain currently blocked? |
|||
*/ |
|||
const isDomainBlocked = await linkService.isDomainBlocked(job.data.link.domain); |
|||
if (isDomainBlocked) { |
|||
/* |
|||
* Make sure the flag is set on the Link |
|||
*/ |
|||
await this.Link.updateOne( |
|||
{ _id: job.data.link._id }, |
|||
{ |
|||
$set: { |
|||
'flags.isBlocked': true, |
|||
}, |
|||
}, |
|||
); |
|||
/* |
|||
* Log the rejection |
|||
*/ |
|||
this.log.alert('prohibiting link from blocked domain', { |
|||
submitter: { |
|||
_id: job.data.submitter._id, |
|||
username: job.data.submitter.username, |
|||
}, |
|||
domain: job.data.link.domain, |
|||
url: job.data.link.url, |
|||
}); |
|||
|
|||
return; // bye!
|
|||
} |
|||
|
|||
this.log.info('fetching link preview', { |
|||
domain: job.data.link.domain, |
|||
url: job.data.link.url, |
|||
}); |
|||
|
|||
job.data.preview = await linkService.generatePagePreview(job.data.link.url); |
|||
if (!job.data.preview) { |
|||
throw new Error('failed to load link preview'); |
|||
} |
|||
|
|||
this.log.info('updating link record in Mongo', { |
|||
link: job.data.link._id, |
|||
preview: job.data.preview, |
|||
}); |
|||
job.data.link = await this.Link.findOneAndUpdate( |
|||
{ _id: job.data.link._id }, |
|||
{ |
|||
$set: { |
|||
lastPreviewFetched: job.data.preview.fetched, |
|||
title: job.data.preview.title, |
|||
siteName: job.data.preview.siteName, |
|||
description: job.data.preview.description, |
|||
tags: job.data.preview.tags, |
|||
mediaType: job.data.preview.mediaType, |
|||
contentType: job.data.preview.contentType, |
|||
images: job.data.preview.images, |
|||
videos: job.data.preview.videos, |
|||
audios: job.data.preview.audios, |
|||
favicons: job.data.preview.favicons, |
|||
oembed: job.data.preview.oembed, |
|||
'flags.havePreview': true, |
|||
}, |
|||
}, |
|||
{ new: true }, |
|||
); |
|||
job.data.link = await this.Link.populate(job.data.link, linkService.populateLink); |
|||
|
|||
this.log.info('link ingest complete', { |
|||
submitter: { |
|||
_id: job.data.submitter._id, |
|||
username: job.data.submitter.username, |
|||
}, |
|||
link: job.data.link, |
|||
}); |
|||
|
|||
if (job.data?.options?.channelId) { |
|||
const viewModel = Object.assign({ link: job.data.link }, this.viewModel); |
|||
const displayList = linkService.createDisplayList('replace-preview'); |
|||
displayList.replaceElement( |
|||
`.link-container[data-link-id="${job.data.link._id}"]`, |
|||
await linkService.renderPreview(viewModel), |
|||
); |
|||
this.emitter.to(job.data.options.channelId).emit('chat-control', { displayList }); |
|||
} |
|||
} catch (error) { |
|||
await this.log.error('failed to ingest link', { |
|||
domain: job.data.link.domain, |
|||
url: job.data.link.url, |
|||
error |
|||
}); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
async updateUrlBlacklist ( ) { |
|||
try { |
|||
/* |
|||
* Fetch latest to local file |
|||
*/ |
|||
this.log.info('fetching updated domain blacklist'); |
|||
const response = await fetch(ChatLinksService.BLOCKLIST_URL); |
|||
if (!response.ok) { |
|||
throw new Error(`unexpected response ${response.statusText}`); |
|||
} |
|||
|
|||
await streamPipeline(Readable.fromWeb(response.body), fs.createWriteStream(this.blacklist.porn)); |
|||
|
|||
/* |
|||
* Read local file line-by-line with filtering and comment removal to insert |
|||
* to Redis set of blocked domains |
|||
*/ |
|||
const fileStream = fs.createReadStream(this.blacklist.porn); |
|||
const rl = readline.createInterface({ |
|||
input: fileStream, |
|||
crlfDelay: Infinity, |
|||
}); |
|||
for await (let line of rl) { |
|||
line = line.trim(); |
|||
if (line[0] === '#') { |
|||
continue; |
|||
} |
|||
const tokens = line.split(' '); |
|||
if (tokens[0] !== '0.0.0.0' || tokens[1] === '0.0.0.0') { |
|||
continue; |
|||
} |
|||
|
|||
const r = await this.redis.sadd(ChatLinksService.DOMAIN_BLACKLIST_KEY, tokens[1]); |
|||
if (r > 0) { |
|||
this.log.info('added domain to Redis blocklist', { domain: tokens[1] }); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
this.log.error('failed to update domain blacklist', { error }); |
|||
// fall through
|
|||
} finally { |
|||
this.log.info('domain block list updated'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
(async ( ) => { |
|||
|
|||
try { |
|||
const { fileURLToPath } = await import('node:url'); |
|||
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
|
|||
|
|||
const worker = new ChatLinksService(path.resolve(__dirname, '..', '..')); |
|||
await worker.start(); |
|||
|
|||
} catch (error) { |
|||
console.error('failed to start Host Cache worker', { error }); |
|||
process.exit(-1); |
|||
} |
|||
|
|||
})(); |
@ -0,0 +1,36 @@ |
|||
|
|||
|
|||
.link-container { |
|||
box-sizing: border-box; |
|||
|
|||
padding: 5px; |
|||
background-color: @link-container-bgcolor; |
|||
|
|||
// border-top: solid 1px red; |
|||
// border-right: solid 1px red; |
|||
// border-bottom: solid 1px red; |
|||
border-left: solid 4px @link-container-border-color; |
|||
|
|||
border-radius: 5px; |
|||
|
|||
.link-preview { |
|||
|
|||
img.link-thumbnail { |
|||
width: 180px; |
|||
height: auto; |
|||
} |
|||
|
|||
.link-description { |
|||
line-height: 1.15em; |
|||
max-height: 4.65em; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
iframe { |
|||
aspect-ratio: 16 / 9; |
|||
height: auto; |
|||
width: 100%; |
|||
max-height: 540px; |
|||
} |
|||
} |
|||
} |
After Width: | Height: | Size: 362 B |
@ -956,11 +956,131 @@ |
|||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" |
|||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== |
|||
|
|||
"@emnapi/runtime@^1.1.0": |
|||
version "1.1.1" |
|||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.1.1.tgz#697d02276ca6f49bafe6fd01c9df0034818afa98" |
|||
integrity sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ== |
|||
dependencies: |
|||
tslib "^2.4.0" |
|||
|
|||
"@fortawesome/fontawesome-free@^6.5.1": |
|||
version "6.5.1" |
|||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz#55cc8410abf1003b726324661ce5b0d1c10de258" |
|||
integrity sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw== |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz#2bbf676be830c5a9ae7d9294f201c9151535badd" |
|||
integrity sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw== |
|||
optionalDependencies: |
|||
"@img/sharp-libvips-darwin-arm64" "1.0.2" |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz#c59567b141eb676e884066f76091a2673120c3f5" |
|||
integrity sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw== |
|||
optionalDependencies: |
|||
"@img/sharp-libvips-darwin-x64" "1.0.2" |
|||
|
|||
"@img/[email protected]": |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz#b69f49fecbe9572378675769b189410721b0fa53" |
|||
integrity sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA== |
|||
|
|||
"@img/[email protected]": |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz#5665da7360d8e5ed7bee314491c8fe736b6a3c39" |
|||
integrity sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw== |
|||
|
|||
"@img/[email protected]": |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz#8a05e5e9e9b760ff46561e32f19bd5e035fa881c" |
|||
integrity sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw== |
|||
|
|||
"@img/[email protected]": |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz#0fd33b9bf3221948ce0ca7a5a725942626577a03" |
|||
integrity sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw== |
|||
|
|||
"@img/[email protected]": |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz#4b89150ec91b256ee2cbb5bb125321bf029a4770" |
|||
integrity sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog== |
|||
|
|||
"@img/[email protected]": |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz#947ccc22ca5bc8c8cfe921b39a5fdaebc5e39f3f" |
|||
integrity sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ== |
|||
|
|||
"@img/[email protected]": |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz#821d58ce774f0f8bed065b69913a62f65d512f2f" |
|||
integrity sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ== |
|||
|
|||
"@img/[email protected]": |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz#4309474bd8b728a61af0b3b4fad0c476b5f3ccbe" |
|||
integrity sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw== |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz#a1f788ddf49ed63509dd37d4b01e571fe7f189d5" |
|||
integrity sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA== |
|||
optionalDependencies: |
|||
"@img/sharp-libvips-linux-arm64" "1.0.2" |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz#661b0671ed7f740fd06821ce15050ba23f1d0523" |
|||
integrity sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w== |
|||
optionalDependencies: |
|||
"@img/sharp-libvips-linux-arm" "1.0.2" |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz#8719341d3931a297df1a956c02ee003736fa8fac" |
|||
integrity sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA== |
|||
optionalDependencies: |
|||
"@img/sharp-libvips-linux-s390x" "1.0.2" |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz#dbd860b4aa16e7e25727c7e05b411132b58d017d" |
|||
integrity sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g== |
|||
optionalDependencies: |
|||
"@img/sharp-libvips-linux-x64" "1.0.2" |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz#25b3fbfe9b6fa32d773422d878d8d84f3f6afceb" |
|||
integrity sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A== |
|||
optionalDependencies: |
|||
"@img/sharp-libvips-linuxmusl-arm64" "1.0.2" |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz#1e533e44abf2e2d427428ed49294ddba4eb11456" |
|||
integrity sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w== |
|||
optionalDependencies: |
|||
"@img/sharp-libvips-linuxmusl-x64" "1.0.2" |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz#340006047a77df0744db84477768bbca6327b4b4" |
|||
integrity sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ== |
|||
dependencies: |
|||
"@emnapi/runtime" "^1.1.0" |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz#0fdc49ab094ed0151ec8347afac7917aa5fc5145" |
|||
integrity sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ== |
|||
|
|||
"@img/[email protected]": |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz#a94e1028f180666f97fd51e35c4ad092d7704ef0" |
|||
integrity sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g== |
|||
|
|||
"@ioredis/commands@^1.1.1": |
|||
version "1.2.0" |
|||
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" |
|||
@ -2024,16 +2144,32 @@ [email protected]: |
|||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" |
|||
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== |
|||
|
|||
color-name@~1.1.4: |
|||
color-name@^1.0.0, color-name@~1.1.4: |
|||
version "1.1.4" |
|||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" |
|||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== |
|||
|
|||
color-string@^1.9.0: |
|||
version "1.9.1" |
|||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" |
|||
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== |
|||
dependencies: |
|||
color-name "^1.0.0" |
|||
simple-swizzle "^0.2.2" |
|||
|
|||
color-support@^1.1.3: |
|||
version "1.1.3" |
|||
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" |
|||
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== |
|||
|
|||
color@^4.2.3: |
|||
version "4.2.3" |
|||
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" |
|||
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== |
|||
dependencies: |
|||
color-convert "^2.0.1" |
|||
color-string "^1.9.0" |
|||
|
|||
colorette@^2.0.10, colorette@^2.0.14: |
|||
version "2.0.20" |
|||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" |
|||
@ -2387,6 +2523,11 @@ destroy@~1.0.4: |
|||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" |
|||
integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== |
|||
|
|||
detect-libc@^2.0.3: |
|||
version "2.0.3" |
|||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" |
|||
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== |
|||
|
|||
dev-ip@^1.0.1: |
|||
version "1.0.1" |
|||
resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0" |
|||
@ -3355,6 +3496,11 @@ is-array-buffer@^3.0.4: |
|||
call-bind "^1.0.2" |
|||
get-intrinsic "^1.2.1" |
|||
|
|||
is-arrayish@^0.3.1: |
|||
version "0.3.2" |
|||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" |
|||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== |
|||
|
|||
is-bigint@^1.0.1: |
|||
version "1.0.4" |
|||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" |
|||
@ -5019,7 +5165,7 @@ semver@^6.3.1: |
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" |
|||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== |
|||
|
|||
semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: |
|||
semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: |
|||
version "7.6.0" |
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" |
|||
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== |
|||
@ -5160,6 +5306,35 @@ shallow-clone@^3.0.0: |
|||
dependencies: |
|||
kind-of "^6.0.2" |
|||
|
|||
sharp@^0.33.3: |
|||
version "0.33.3" |
|||
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.3.tgz#3342fe0aa5ed45a363e6578fa575c7af366216c2" |
|||
integrity sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A== |
|||
dependencies: |
|||
color "^4.2.3" |
|||
detect-libc "^2.0.3" |
|||
semver "^7.6.0" |
|||
optionalDependencies: |
|||
"@img/sharp-darwin-arm64" "0.33.3" |
|||
"@img/sharp-darwin-x64" "0.33.3" |
|||
"@img/sharp-libvips-darwin-arm64" "1.0.2" |
|||
"@img/sharp-libvips-darwin-x64" "1.0.2" |
|||
"@img/sharp-libvips-linux-arm" "1.0.2" |
|||
"@img/sharp-libvips-linux-arm64" "1.0.2" |
|||
"@img/sharp-libvips-linux-s390x" "1.0.2" |
|||
"@img/sharp-libvips-linux-x64" "1.0.2" |
|||
"@img/sharp-libvips-linuxmusl-arm64" "1.0.2" |
|||
"@img/sharp-libvips-linuxmusl-x64" "1.0.2" |
|||
"@img/sharp-linux-arm" "0.33.3" |
|||
"@img/sharp-linux-arm64" "0.33.3" |
|||
"@img/sharp-linux-s390x" "0.33.3" |
|||
"@img/sharp-linux-x64" "0.33.3" |
|||
"@img/sharp-linuxmusl-arm64" "0.33.3" |
|||
"@img/sharp-linuxmusl-x64" "0.33.3" |
|||
"@img/sharp-wasm32" "0.33.3" |
|||
"@img/sharp-win32-ia32" "0.33.3" |
|||
"@img/sharp-win32-x64" "0.33.3" |
|||
|
|||
shebang-command@^2.0.0: |
|||
version "2.0.0" |
|||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" |
|||
@ -5194,6 +5369,13 @@ [email protected]: |
|||
resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053" |
|||
integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== |
|||
|
|||
simple-swizzle@^0.2.2: |
|||
version "0.2.2" |
|||
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" |
|||
integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== |
|||
dependencies: |
|||
is-arrayish "^0.3.1" |
|||
|
|||
simple-update-notifier@^2.0.0: |
|||
version "2.0.0" |
|||
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" |
|||
@ -5647,7 +5829,7 @@ tr46@^5.0.0: |
|||
dependencies: |
|||
punycode "^2.3.1" |
|||
|
|||
tslib@^2.0.0, tslib@^2.3.0: |
|||
tslib@^2.0.0, tslib@^2.3.0, tslib@^2.4.0: |
|||
version "2.6.2" |
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" |
|||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== |
|||
|
Loading…
Reference in new issue