mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
Merge remote-tracking branch 'origin/master' into redesign
This commit is contained in:
commit
ec6c51b7e2
91 changed files with 1412 additions and 781 deletions
25
.github/workflows/elixir.yml
vendored
25
.github/workflows/elixir.yml
vendored
|
@ -14,8 +14,19 @@ jobs:
|
|||
with:
|
||||
path: |
|
||||
_build
|
||||
.cargo
|
||||
deps
|
||||
key: ${{ runner.os }}-build-deps-${{ hashFiles('mix.lock') }}
|
||||
key: ${{ runner.os }}-deps-2-${{ hashFiles('mix.lock') }}
|
||||
|
||||
- name: Enable caching
|
||||
run: |
|
||||
# Disable volumes so caching can take effect
|
||||
sed -i -Ee 's/- app_[a-z]+_data:.*$//g' docker-compose.yml
|
||||
|
||||
# Make ourselves the owner
|
||||
echo "RUN addgroup -g $(id -g) -S appgroup && adduser -u $(id -u) -S appuser -G appgroup" >> docker/app/Dockerfile
|
||||
echo "USER appuser" >> docker/app/Dockerfile
|
||||
echo "RUN mix local.hex --force && mix local.rebar --force" >> docker/app/Dockerfile
|
||||
|
||||
- run: docker compose pull
|
||||
- run: docker compose build
|
||||
|
@ -27,6 +38,18 @@ jobs:
|
|||
run: |
|
||||
docker compose run app mix sobelow --config
|
||||
docker compose run app mix deps.audit
|
||||
|
||||
- name: Dialyzer
|
||||
run: |
|
||||
docker compose run app mix dialyzer
|
||||
|
||||
typos:
|
||||
name: 'Check for spelling errors'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@master
|
||||
|
||||
lint-and-test:
|
||||
name: 'JavaScript Linting and Unit Tests'
|
||||
runs-on: ubuntu-latest
|
||||
|
|
10
.typos.toml
Normal file
10
.typos.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[default]
|
||||
extend-ignore-re = [
|
||||
# Ignore development secret key. Production secret key should
|
||||
# be in environment files and not checked into source control.
|
||||
".*secret_key_base.*",
|
||||
|
||||
# Key constraints with encoded names
|
||||
"fk_rails_[a-f0-9]+"
|
||||
]
|
||||
|
161
assets/js/__tests__/imagesclientside.spec.ts
Normal file
161
assets/js/__tests__/imagesclientside.spec.ts
Normal file
|
@ -0,0 +1,161 @@
|
|||
import { filterNode, initImagesClientside } from '../imagesclientside';
|
||||
import { parseSearch } from '../match_query';
|
||||
import { matchNone } from '../query/boolean';
|
||||
import { assertNotNull } from '../utils/assert';
|
||||
import { $ } from '../utils/dom';
|
||||
|
||||
describe('filterNode', () => {
|
||||
beforeEach(() => {
|
||||
window.booru.hiddenTagList = [];
|
||||
window.booru.spoileredTagList = [];
|
||||
window.booru.ignoredTagList = [];
|
||||
window.booru.imagesWithDownvotingDisabled = [];
|
||||
|
||||
window.booru.hiddenFilter = matchNone();
|
||||
window.booru.spoileredFilter = matchNone();
|
||||
});
|
||||
|
||||
function makeMediaContainer() {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = `
|
||||
<div class="image-container" data-image-id="1" data-image-tags="[1]">
|
||||
<div class="js-spoiler-info-overlay"></div>
|
||||
<picture><img src=""/></picture>
|
||||
</div>
|
||||
`;
|
||||
return [ element, assertNotNull($<HTMLDivElement>('.js-spoiler-info-overlay', element)) ];
|
||||
}
|
||||
|
||||
it('should show image media boxes not matching any filter', () => {
|
||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||
|
||||
filterNode(container);
|
||||
expect(spoilerOverlay).not.toContainHTML('(Complex Filter)');
|
||||
expect(spoilerOverlay).not.toContainHTML('(unknown tag)');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).not.toContain('1');
|
||||
});
|
||||
|
||||
it('should spoiler media boxes spoilered by a tag filter', () => {
|
||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||
window.booru.spoileredTagList = [1];
|
||||
|
||||
filterNode(container);
|
||||
expect(spoilerOverlay).toContainHTML('(unknown tag)');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||
});
|
||||
|
||||
it('should spoiler media boxes spoilered by a complex filter', () => {
|
||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||
window.booru.spoileredFilter = parseSearch('id:1');
|
||||
|
||||
filterNode(container);
|
||||
expect(spoilerOverlay).toContainHTML('(Complex Filter)');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||
});
|
||||
|
||||
it('should hide media boxes hidden by a tag filter', () => {
|
||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||
window.booru.hiddenTagList = [1];
|
||||
|
||||
filterNode(container);
|
||||
expect(spoilerOverlay).toContainHTML('[HIDDEN]');
|
||||
expect(spoilerOverlay).toContainHTML('(unknown tag)');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||
});
|
||||
|
||||
it('should hide media boxes hidden by a complex filter', () => {
|
||||
const [ container, spoilerOverlay ] = makeMediaContainer();
|
||||
window.booru.hiddenFilter = parseSearch('id:1');
|
||||
|
||||
filterNode(container);
|
||||
expect(spoilerOverlay).toContainHTML('[HIDDEN]');
|
||||
expect(spoilerOverlay).toContainHTML('(Complex Filter)');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||
});
|
||||
|
||||
function makeImageBlock(): HTMLElement[] {
|
||||
const element = document.createElement('div');
|
||||
element.innerHTML = `
|
||||
<div class="image-show-container" data-image-id="1" data-image-tags="[1]">
|
||||
<div class="image-filtered hidden">
|
||||
<img src=""/>
|
||||
<span class="filter-explanation"></span>
|
||||
</div>
|
||||
<div class="image-show hidden">
|
||||
<picture><img src=""/></picture>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return [
|
||||
element,
|
||||
assertNotNull($<HTMLDivElement>('.image-filtered', element)),
|
||||
assertNotNull($<HTMLDivElement>('.image-show', element)),
|
||||
assertNotNull($<HTMLSpanElement>('.filter-explanation', element))
|
||||
];
|
||||
}
|
||||
|
||||
it('should show image blocks not matching any filter', () => {
|
||||
const [ container, imageFiltered, imageShow ] = makeImageBlock();
|
||||
|
||||
filterNode(container);
|
||||
expect(imageFiltered).toHaveClass('hidden');
|
||||
expect(imageShow).not.toHaveClass('hidden');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).not.toContain('1');
|
||||
});
|
||||
|
||||
it('should spoiler image blocks spoilered by a tag filter', () => {
|
||||
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
||||
window.booru.spoileredTagList = [1];
|
||||
|
||||
filterNode(container);
|
||||
expect(imageFiltered).not.toHaveClass('hidden');
|
||||
expect(imageShow).toHaveClass('hidden');
|
||||
expect(filterExplanation).toContainHTML('spoilered by');
|
||||
expect(filterExplanation).toContainHTML('(unknown tag)');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||
});
|
||||
|
||||
it('should spoiler image blocks spoilered by a complex filter', () => {
|
||||
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
||||
window.booru.spoileredFilter = parseSearch('id:1');
|
||||
|
||||
filterNode(container);
|
||||
expect(imageFiltered).not.toHaveClass('hidden');
|
||||
expect(imageShow).toHaveClass('hidden');
|
||||
expect(filterExplanation).toContainHTML('spoilered by');
|
||||
expect(filterExplanation).toContainHTML('complex tag expression');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||
});
|
||||
|
||||
it('should hide image blocks hidden by a tag filter', () => {
|
||||
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
||||
window.booru.hiddenTagList = [1];
|
||||
|
||||
filterNode(container);
|
||||
expect(imageFiltered).not.toHaveClass('hidden');
|
||||
expect(imageShow).toHaveClass('hidden');
|
||||
expect(filterExplanation).toContainHTML('hidden by');
|
||||
expect(filterExplanation).toContainHTML('(unknown tag)');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||
});
|
||||
|
||||
it('should hide image blocks hidden by a complex filter', () => {
|
||||
const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
|
||||
window.booru.hiddenFilter = parseSearch('id:1');
|
||||
|
||||
filterNode(container);
|
||||
expect(imageFiltered).not.toHaveClass('hidden');
|
||||
expect(imageShow).toHaveClass('hidden');
|
||||
expect(filterExplanation).toContainHTML('hidden by');
|
||||
expect(filterExplanation).toContainHTML('complex tag expression');
|
||||
expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('initImagesClientside', () => {
|
||||
it('should initialize the imagesWithDownvotingDisabled array', () => {
|
||||
initImagesClientside();
|
||||
expect(window.booru.imagesWithDownvotingDisabled).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -1,15 +1,14 @@
|
|||
import { assertNotNull } from './utils/assert';
|
||||
import { delegate, leftClick } from './utils/events';
|
||||
import { clearEl, makeEl } from './utils/dom';
|
||||
|
||||
function insertCaptcha(_event: Event, target: HTMLInputElement) {
|
||||
const { parentElement, dataset: { sitekey } } = target;
|
||||
|
||||
if (!parentElement) { return; }
|
||||
const parentElement = assertNotNull(target.parentElement);
|
||||
|
||||
const script = makeEl('script', {src: 'https://hcaptcha.com/1/api.js', async: true, defer: true});
|
||||
const frame = makeEl('div', {className: 'h-captcha'});
|
||||
|
||||
frame.dataset.sitekey = sitekey;
|
||||
frame.dataset.sitekey = target.dataset.sitekey;
|
||||
|
||||
clearEl(parentElement);
|
||||
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
* Interactive behavior for duplicate reports.
|
||||
*/
|
||||
|
||||
import { assertNotNull } from './utils/assert';
|
||||
import { $, $$ } from './utils/dom';
|
||||
|
||||
function setupDupeReports() {
|
||||
export function setupDupeReports() {
|
||||
const onion = $<SVGSVGElement>('.onion-skin__image');
|
||||
const slider = $<HTMLInputElement>('.onion-skin__slider');
|
||||
const swipe = $<SVGSVGElement>('.swipe__image');
|
||||
|
@ -30,16 +31,12 @@ function setupSwipe(swipe: SVGSVGElement) {
|
|||
}
|
||||
|
||||
function setupOnionSkin(onion: SVGSVGElement, slider: HTMLInputElement) {
|
||||
const target = $<HTMLImageElement>('#target', onion);
|
||||
const target = assertNotNull($<SVGImageElement>('#target', onion));
|
||||
|
||||
function setOpacity() {
|
||||
if (target) {
|
||||
target.setAttribute('opacity', slider.value);
|
||||
}
|
||||
target.setAttribute('opacity', slider.value);
|
||||
}
|
||||
|
||||
setOpacity();
|
||||
slider.addEventListener('input', setOpacity);
|
||||
}
|
||||
|
||||
export { setupDupeReports };
|
||||
|
|
215
assets/js/fp.ts
215
assets/js/fp.ts
|
@ -1,36 +1,39 @@
|
|||
/**
|
||||
* FP version 4
|
||||
*
|
||||
* Not reliant on deprecated properties,
|
||||
* and potentially more accurate at what it's supposed to do.
|
||||
* Not reliant on deprecated properties, and potentially
|
||||
* more accurate at what it's supposed to do.
|
||||
*/
|
||||
|
||||
import { $ } from './utils/dom';
|
||||
import store from './utils/store';
|
||||
|
||||
interface RealKeyboard {
|
||||
getLayoutMap: () => Promise<Map<string, string>>
|
||||
}
|
||||
const storageKey = 'cached_ses_value';
|
||||
|
||||
interface RealUserAgentData {
|
||||
brands: [{brand: string, version: string}],
|
||||
mobile: boolean,
|
||||
platform: string,
|
||||
}
|
||||
declare global {
|
||||
interface Keyboard {
|
||||
getLayoutMap: () => Promise<Map<string, string>>
|
||||
}
|
||||
|
||||
interface RealNavigator extends Navigator {
|
||||
deviceMemory: number | null,
|
||||
keyboard: RealKeyboard | null,
|
||||
userAgentData: RealUserAgentData | null,
|
||||
interface UserAgentData {
|
||||
brands: [{brand: string, version: string}],
|
||||
mobile: boolean,
|
||||
platform: string,
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
deviceMemory: number | undefined,
|
||||
keyboard: Keyboard | undefined,
|
||||
userAgentData: UserAgentData | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a 53-bit long hash of a string.
|
||||
* Creates a 53-bit non-cryptographic hash of a string.
|
||||
*
|
||||
* @param {string} str The string to hash.
|
||||
* @param {number} seed The seed to use for hash generation.
|
||||
* @return {number} The resulting hash as a 53-bit number.
|
||||
* @see {@link https://stackoverflow.com/a/8831937}
|
||||
* @param str The string to hash.
|
||||
* @param seed The seed to use for hash generation.
|
||||
* @return The resulting hash as a 53-bit number.
|
||||
* @see {@link https://stackoverflow.com/a/52171480}
|
||||
*/
|
||||
function cyrb53(str: string, seed: number = 0x16fe7b0a): number {
|
||||
let h1 = 0xdeadbeef ^ seed;
|
||||
|
@ -50,67 +53,104 @@ function cyrb53(str: string, seed: number = 0x16fe7b0a): number {
|
|||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
}
|
||||
|
||||
/** Creates a semi-unique string from browser attributes.
|
||||
/**
|
||||
* Get keyboard layout data from the navigator layout map.
|
||||
*
|
||||
* @async
|
||||
* @return {Promise<string>} Hexadecimally encoded 53 bit number padded to 7 bytes.
|
||||
*/
|
||||
async function createFp(): Promise<string> {
|
||||
const nav = navigator as RealNavigator;
|
||||
let kb = 'none';
|
||||
let mem = '1';
|
||||
let ua = 'none';
|
||||
* @return String containing layout map entries, or `none` when unavailable
|
||||
*/
|
||||
async function getKeyboardData(): Promise<string> {
|
||||
if (navigator.keyboard) {
|
||||
const layoutMap = await navigator.keyboard.getLayoutMap();
|
||||
|
||||
if (nav.keyboard) {
|
||||
kb = Array.from((await nav.keyboard.getLayoutMap()).entries()).sort().map(e => `${e[0]}${e[1]}`).join('');
|
||||
return Array.from(layoutMap.entries())
|
||||
.sort()
|
||||
.map(([k, v]) => `${k}${v}`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
if (nav.deviceMemory) {
|
||||
mem = nav.deviceMemory.toString();
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an approximation of memory available in gigabytes.
|
||||
*
|
||||
* @return String containing device memory data, or `1` when unavailable
|
||||
*/
|
||||
function getMemoryData(): string {
|
||||
if (navigator.deviceMemory) {
|
||||
return navigator.deviceMemory.toString();
|
||||
}
|
||||
|
||||
if (nav.userAgentData) {
|
||||
const uadata = nav.userAgentData;
|
||||
return '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "brands" of the user agent.
|
||||
*
|
||||
* For Chromium-based browsers this returns additional data like "Edge" or "Chrome"
|
||||
* which may also contain additional data beyond the `userAgent` property.
|
||||
*
|
||||
* @return String containing brand data, or `none` when unavailable
|
||||
*/
|
||||
function getUserAgentBrands(): string {
|
||||
const data = navigator.userAgentData;
|
||||
|
||||
if (data) {
|
||||
let brands = 'none';
|
||||
|
||||
if (uadata.brands && uadata.brands.length > 0) {
|
||||
brands = uadata.brands.filter(e => !e.brand.match(/.*ot.*rand.*/gi)).map(e => `${e.brand}${e.version}`).join('');
|
||||
if (data.brands && data.brands.length > 0) {
|
||||
// NB: Chromium implements GREASE protocol to prevent ossification of
|
||||
// the "Not a brand" string - see https://stackoverflow.com/a/64443187
|
||||
brands = data.brands
|
||||
.filter(e => !e.brand.match(/.*ot.*rand.*/gi))
|
||||
.map(e => `${e.brand}${e.version}`)
|
||||
.sort()
|
||||
.join('');
|
||||
}
|
||||
|
||||
ua = `${brands}${uadata.mobile}${uadata.platform}`;
|
||||
return `${brands}${data.mobile}${data.platform}`;
|
||||
}
|
||||
|
||||
let width: string | null = store.get('cached_rem_size');
|
||||
const body = $<HTMLBodyElement>('body');
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (!width && body) {
|
||||
const testElement = document.createElement('span');
|
||||
testElement.style.minWidth = '1rem';
|
||||
testElement.style.maxWidth = '1rem';
|
||||
testElement.style.position = 'absolute';
|
||||
/**
|
||||
* Get the size in rems of the default body font.
|
||||
*
|
||||
* Causes a forced layout. Be sure to cache this value.
|
||||
*
|
||||
* @return String with the rem size
|
||||
*/
|
||||
function getFontRemSize(): string {
|
||||
const testElement = document.createElement('span');
|
||||
testElement.style.minWidth = '1rem';
|
||||
testElement.style.maxWidth = '1rem';
|
||||
testElement.style.position = 'absolute';
|
||||
|
||||
body.appendChild(testElement);
|
||||
document.body.appendChild(testElement);
|
||||
|
||||
width = testElement.clientWidth.toString();
|
||||
const width = testElement.clientWidth.toString();
|
||||
|
||||
body.removeChild(testElement);
|
||||
document.body.removeChild(testElement);
|
||||
|
||||
store.set('cached_rem_size', width);
|
||||
}
|
||||
|
||||
if (!width) {
|
||||
width = '0';
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a semi-unique string from browser attributes.
|
||||
*
|
||||
* @return Hexadecimally encoded 53 bit number padded to 7 bytes.
|
||||
*/
|
||||
async function createFp(): Promise<string> {
|
||||
const prints: string[] = [
|
||||
navigator.userAgent,
|
||||
navigator.hardwareConcurrency.toString(),
|
||||
navigator.maxTouchPoints.toString(),
|
||||
navigator.language,
|
||||
kb,
|
||||
mem,
|
||||
ua,
|
||||
width,
|
||||
await getKeyboardData(),
|
||||
getMemoryData(),
|
||||
getUserAgentBrands(),
|
||||
getFontRemSize(),
|
||||
|
||||
screen.height.toString(),
|
||||
screen.width.toString(),
|
||||
|
@ -121,43 +161,50 @@ async function createFp(): Promise<string> {
|
|||
new Date().getTimezoneOffset().toString(),
|
||||
];
|
||||
|
||||
return cyrb53(prints.join('')).toString(16).padStart(14, '0');
|
||||
return cyrb53(prints.join(''))
|
||||
.toString(16)
|
||||
.padStart(14, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the existing `_ses` value from local storage or cookies.
|
||||
*
|
||||
* @return String `_ses` value or `null`
|
||||
*/
|
||||
function getSesValue(): string | null {
|
||||
// Try storage
|
||||
const storageValue: string | null = store.get(storageKey);
|
||||
if (storageValue) {
|
||||
return storageValue;
|
||||
}
|
||||
|
||||
// Try cookie
|
||||
const match = document.cookie.match(/_ses=([a-f0-9]+)/);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
// Not found
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `_ses` cookie.
|
||||
*
|
||||
* If `cached_ses_value` is present in local storage, uses it to set the `_ses` cookie.
|
||||
* Otherwise if the `_ses` cookie already exits, uses its value instead.
|
||||
* Otherwise attempts to generate a new value for the `_ses` cookie
|
||||
* based on various browser attributes.
|
||||
* Otherwise, if the `_ses` cookie already exists, uses its value instead.
|
||||
* Otherwise, attempts to generate a new value for the `_ses` cookie based on
|
||||
* various browser attributes.
|
||||
* Failing all previous methods, sets the `_ses` cookie to a fallback value.
|
||||
*
|
||||
* @async
|
||||
*/
|
||||
export async function setSesCookie() {
|
||||
let fp: string | null = store.get('cached_ses_value');
|
||||
let sesValue = getSesValue();
|
||||
|
||||
if (!fp) {
|
||||
const m = document.cookie.match(/_ses=([a-f0-9]+)/);
|
||||
|
||||
if (m && m[1]) {
|
||||
fp = m[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!fp || fp.charAt(0) !== 'd' || fp.length !== 15) {
|
||||
if (!sesValue || sesValue.charAt(0) !== 'd' || sesValue.length !== 15) {
|
||||
// The prepended 'd' acts as a crude versioning mechanism.
|
||||
try {
|
||||
fp = `d${await createFp()}`;
|
||||
}
|
||||
// If it fails, use fakeprint "d015c342859dde3" as a last resort.
|
||||
catch {
|
||||
fp = 'd015c342859dde3';
|
||||
}
|
||||
|
||||
store.set('cached_ses_value', fp);
|
||||
sesValue = `d${await createFp()}`;
|
||||
store.set(storageKey, sesValue);
|
||||
}
|
||||
|
||||
document.cookie = `_ses=${fp}; path=/; SameSite=Lax`;
|
||||
document.cookie = `_ses=${sesValue}; path=/; SameSite=Lax`;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
import { arraysEqual } from './utils/array';
|
||||
import { assertNotNull, assertNotUndefined } from './utils/assert';
|
||||
import { $, $$ } from './utils/dom';
|
||||
import { initDraggables } from './utils/draggable';
|
||||
import { fetchJson } from './utils/requests';
|
||||
|
@ -11,14 +12,13 @@ export function setupGalleryEditing() {
|
|||
if (!$<HTMLElement>('.rearrange-button')) return;
|
||||
|
||||
const [ rearrangeEl, saveEl ] = $$<HTMLElement>('.rearrange-button');
|
||||
const sortableEl = $<HTMLDivElement>('#sortable');
|
||||
const containerEl = $<HTMLDivElement>('.media-list');
|
||||
|
||||
if (!sortableEl || !containerEl || !saveEl || !rearrangeEl) { return; }
|
||||
const sortableEl = assertNotNull($<HTMLDivElement>('#sortable'));
|
||||
const containerEl = assertNotNull($<HTMLDivElement>('.js-resizable-media-container'));
|
||||
|
||||
// Copy array
|
||||
let oldImages = window.booru.galleryImages.slice();
|
||||
let newImages = window.booru.galleryImages.slice();
|
||||
const galleryImages = assertNotUndefined(window.booru.galleryImages);
|
||||
let oldImages = galleryImages.slice();
|
||||
let newImages = galleryImages.slice();
|
||||
|
||||
initDraggables();
|
||||
|
||||
|
@ -33,17 +33,17 @@ export function setupGalleryEditing() {
|
|||
sortableEl.classList.remove('editing');
|
||||
containerEl.classList.remove('drag-container');
|
||||
|
||||
newImages = $$<HTMLDivElement>('.image-container', containerEl).map(i => parseInt(i.dataset.imageId || '-1', 10));
|
||||
newImages = $$<HTMLDivElement>('.image-container', containerEl)
|
||||
.map(i => parseInt(assertNotUndefined(i.dataset.imageId), 10));
|
||||
|
||||
// If nothing changed, don't bother.
|
||||
if (arraysEqual(newImages, oldImages)) return;
|
||||
|
||||
if (saveEl.dataset.reorderPath) {
|
||||
fetchJson('PATCH', saveEl.dataset.reorderPath, {
|
||||
image_ids: newImages,
|
||||
const reorderPath = assertNotUndefined(saveEl.dataset.reorderPath);
|
||||
|
||||
// copy the array again so that we have the newly updated set
|
||||
}).then(() => oldImages = newImages.slice());
|
||||
}
|
||||
fetchJson('PATCH', reorderPath, {
|
||||
image_ids: newImages,
|
||||
// copy the array again so that we have the newly updated set
|
||||
}).then(() => oldImages = newImages.slice());
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,131 +2,109 @@
|
|||
* Client-side image filtering/spoilering.
|
||||
*/
|
||||
|
||||
import { assertNotUndefined } from './utils/assert';
|
||||
import { $$, escapeHtml } from './utils/dom';
|
||||
import { setupInteractions } from './interactions';
|
||||
import { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb } from './utils/image';
|
||||
import { TagData, getHiddenTags, getSpoileredTags, imageHitsTags, imageHitsComplex, displayTags } from './utils/tag';
|
||||
import { AstMatcher } from './query/types';
|
||||
import { assertNotUndefined } from './utils/assert';
|
||||
|
||||
type RunFilterCallback = (img: HTMLDivElement, test: TagData[]) => void;
|
||||
type CallbackType = 'tags' | 'complex';
|
||||
type RunCallback = (img: HTMLDivElement, tags: TagData[], type: CallbackType) => void;
|
||||
|
||||
function runFilter(img: HTMLDivElement, test: TagData[] | boolean, runCallback: RunFilterCallback) {
|
||||
if (!test || typeof test !== 'boolean' && test.length === 0) { return false; }
|
||||
function run(
|
||||
img: HTMLDivElement,
|
||||
tags: TagData[],
|
||||
complex: AstMatcher,
|
||||
runCallback: RunCallback
|
||||
): boolean {
|
||||
const hit = (() => {
|
||||
// Check tags array first to provide more precise filter explanations
|
||||
const hitTags = imageHitsTags(img, tags);
|
||||
if (hitTags.length !== 0) {
|
||||
runCallback(img, hitTags, 'tags');
|
||||
return true;
|
||||
}
|
||||
|
||||
runCallback(img, test as TagData[]);
|
||||
// No tags matched, try complex filter AST
|
||||
const hitComplex = imageHitsComplex(img, complex);
|
||||
if (hitComplex) {
|
||||
runCallback(img, hitTags, 'complex');
|
||||
return true;
|
||||
}
|
||||
|
||||
// I don't like this.
|
||||
window.booru.imagesWithDownvotingDisabled.push(assertNotUndefined(img.dataset.imageId));
|
||||
// Nothing matched at all, image can be shown
|
||||
return false;
|
||||
})();
|
||||
|
||||
return true;
|
||||
if (hit) {
|
||||
// Disallow negative interaction on image which is not visible
|
||||
window.booru.imagesWithDownvotingDisabled.push(assertNotUndefined(img.dataset.imageId));
|
||||
}
|
||||
|
||||
return hit;
|
||||
}
|
||||
|
||||
// ---
|
||||
function bannerImage(tagsHit: TagData[]) {
|
||||
if (tagsHit.length > 0) {
|
||||
return tagsHit[0].spoiler_image_uri || window.booru.hiddenTag;
|
||||
}
|
||||
|
||||
function filterThumbSimple(img: HTMLDivElement, tagsHit: TagData[]) {
|
||||
hideThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, `[HIDDEN] ${displayTags(tagsHit)}`);
|
||||
return window.booru.hiddenTag;
|
||||
}
|
||||
|
||||
function spoilerThumbSimple(img: HTMLDivElement, tagsHit: TagData[]) {
|
||||
spoilerThumb(img, tagsHit[0].spoiler_image_uri || window.booru.hiddenTag, displayTags(tagsHit));
|
||||
// TODO: this approach is not suitable for translations because it depends on
|
||||
// markup embedded in the page adjacent to this text
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function hideThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||
const bannerText = type === 'tags' ? `[HIDDEN] ${displayTags(tagsHit)}`
|
||||
: '[HIDDEN] <i>(Complex Filter)</i>';
|
||||
hideThumb(img, bannerImage(tagsHit), bannerText);
|
||||
}
|
||||
|
||||
function filterThumbComplex(img: HTMLDivElement) {
|
||||
hideThumb(img, window.booru.hiddenTag, '[HIDDEN] <i>(Complex Filter)</i>');
|
||||
function spoilerThumbTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||
const bannerText = type === 'tags' ? displayTags(tagsHit)
|
||||
: '<i>(Complex Filter)</i>';
|
||||
spoilerThumb(img, bannerImage(tagsHit), bannerText);
|
||||
}
|
||||
|
||||
function spoilerThumbComplex(img: HTMLDivElement) {
|
||||
spoilerThumb(img, window.booru.hiddenTag, '<i>(Complex Filter)</i>');
|
||||
function hideBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||
const bannerText = type === 'tags' ? `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is hidden by `
|
||||
: 'This image was hidden by a complex tag expression in ';
|
||||
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
||||
}
|
||||
|
||||
function filterBlockSimple(img: HTMLDivElement, tagsHit: TagData[]) {
|
||||
spoilerBlock(
|
||||
img,
|
||||
tagsHit[0].spoiler_image_uri || window.booru.hiddenTag,
|
||||
`This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is hidden by `
|
||||
);
|
||||
function spoilerBlockTyped(img: HTMLDivElement, tagsHit: TagData[], type: CallbackType) {
|
||||
const bannerText = type === 'tags' ? `This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is spoilered by `
|
||||
: 'This image was spoilered by a complex tag expression in ';
|
||||
spoilerBlock(img, bannerImage(tagsHit), bannerText);
|
||||
}
|
||||
|
||||
function spoilerBlockSimple(img: HTMLDivElement, tagsHit: TagData[]) {
|
||||
spoilerBlock(
|
||||
img,
|
||||
tagsHit[0].spoiler_image_uri || window.booru.hiddenTag,
|
||||
`This image is tagged <code>${escapeHtml(tagsHit[0].name)}</code>, which is spoilered by `
|
||||
);
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
|
||||
function filterBlockComplex(img: HTMLDivElement) {
|
||||
spoilerBlock(img, window.booru.hiddenTag, 'This image was hidden by a complex tag expression in ');
|
||||
}
|
||||
|
||||
function spoilerBlockComplex(img: HTMLDivElement) {
|
||||
spoilerBlock(img, window.booru.hiddenTag, 'This image was spoilered by a complex tag expression in ');
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
function thumbTagFilter(tags: TagData[], img: HTMLDivElement) {
|
||||
return runFilter(img, imageHitsTags(img, tags), filterThumbSimple);
|
||||
}
|
||||
|
||||
function thumbComplexFilter(complex: AstMatcher, img: HTMLDivElement) {
|
||||
return runFilter(img, imageHitsComplex(img, complex), filterThumbComplex);
|
||||
}
|
||||
|
||||
function thumbTagSpoiler(tags: TagData[], img: HTMLDivElement) {
|
||||
return runFilter(img, imageHitsTags(img, tags), spoilerThumbSimple);
|
||||
}
|
||||
|
||||
function thumbComplexSpoiler(complex: AstMatcher, img: HTMLDivElement) {
|
||||
return runFilter(img, imageHitsComplex(img, complex), spoilerThumbComplex);
|
||||
}
|
||||
|
||||
function blockTagFilter(tags: TagData[], img: HTMLDivElement) {
|
||||
return runFilter(img, imageHitsTags(img, tags), filterBlockSimple);
|
||||
}
|
||||
|
||||
function blockComplexFilter(complex: AstMatcher, img: HTMLDivElement) {
|
||||
return runFilter(img, imageHitsComplex(img, complex), filterBlockComplex);
|
||||
}
|
||||
|
||||
function blockTagSpoiler(tags: TagData[], img: HTMLDivElement) {
|
||||
return runFilter(img, imageHitsTags(img, tags), spoilerBlockSimple);
|
||||
}
|
||||
|
||||
function blockComplexSpoiler(complex: AstMatcher, img: HTMLDivElement) {
|
||||
return runFilter(img, imageHitsComplex(img, complex), spoilerBlockComplex);
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
function filterNode(node: Pick<Document, 'querySelectorAll'>) {
|
||||
export function filterNode(node: Pick<Document, 'querySelectorAll'>) {
|
||||
const hiddenTags = getHiddenTags(), spoileredTags = getSpoileredTags();
|
||||
const { hiddenFilter, spoileredFilter } = window.booru;
|
||||
|
||||
// Image thumb boxes with vote and fave buttons on them
|
||||
$$<HTMLDivElement>('.image-container', node)
|
||||
.filter(img => !thumbTagFilter(hiddenTags, img))
|
||||
.filter(img => !thumbComplexFilter(hiddenFilter, img))
|
||||
.filter(img => !thumbTagSpoiler(spoileredTags, img))
|
||||
.filter(img => !thumbComplexSpoiler(spoileredFilter, img))
|
||||
.filter(img => !run(img, hiddenTags, hiddenFilter, hideThumbTyped))
|
||||
.filter(img => !run(img, spoileredTags, spoileredFilter, spoilerThumbTyped))
|
||||
.forEach(img => showThumb(img));
|
||||
|
||||
// Individual image pages and images in posts/comments
|
||||
$$<HTMLDivElement>('.image-show-container', node)
|
||||
.filter(img => !blockTagFilter(hiddenTags, img))
|
||||
.filter(img => !blockComplexFilter(hiddenFilter, img))
|
||||
.filter(img => !blockTagSpoiler(spoileredTags, img))
|
||||
.filter(img => !blockComplexSpoiler(spoileredFilter, img))
|
||||
.filter(img => !run(img, hiddenTags, hiddenFilter, hideBlockTyped))
|
||||
.filter(img => !run(img, spoileredTags, spoileredFilter, spoilerBlockTyped))
|
||||
.forEach(img => showBlock(img));
|
||||
}
|
||||
|
||||
function initImagesClientside() {
|
||||
export function initImagesClientside() {
|
||||
window.booru.imagesWithDownvotingDisabled = [];
|
||||
// This fills the imagesWithDownvotingDisabled array
|
||||
filterNode(document);
|
||||
// Once the array is populated, we can initialize interactions
|
||||
setupInteractions();
|
||||
}
|
||||
|
||||
export { initImagesClientside, filterNode };
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
* Warn users that their PM will be reviewed.
|
||||
*/
|
||||
|
||||
import { $ } from './utils/dom';
|
||||
import { $, hideEl, showEl } from './utils/dom';
|
||||
|
||||
function warnAboutPMs() {
|
||||
export function warnAboutPMs() {
|
||||
const textarea = $<HTMLTextAreaElement>('.js-toolbar-input');
|
||||
const warning = $<HTMLDivElement>('.js-hidden-warning');
|
||||
const imageEmbedRegex = /!+\[/g;
|
||||
|
@ -17,12 +17,10 @@ function warnAboutPMs() {
|
|||
const value = textarea.value;
|
||||
|
||||
if (value.match(imageEmbedRegex)) {
|
||||
warning.classList.remove('hidden');
|
||||
showEl(warning);
|
||||
}
|
||||
else if (!warning.classList.contains('hidden')) {
|
||||
warning.classList.add('hidden');
|
||||
hideEl(warning);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { warnAboutPMs };
|
||||
|
|
|
@ -2,15 +2,17 @@
|
|||
* Settings.
|
||||
*/
|
||||
|
||||
import { assertNotNull, assertNotUndefined } from './utils/assert';
|
||||
import { $, $$ } from './utils/dom';
|
||||
import store from './utils/store';
|
||||
|
||||
export function setupSettings() {
|
||||
if (!$<HTMLElement>('#js-setting-table')) return;
|
||||
|
||||
if (!$('#js-setting-table')) return;
|
||||
|
||||
const localCheckboxes = $$<HTMLInputElement>('[data-tab="local"] input[type="checkbox"]');
|
||||
const themeSelect = $<HTMLSelectElement>('#user_theme');
|
||||
const styleSheet = $<HTMLLinkElement>('head link[rel="stylesheet"]');
|
||||
const themeSelect = assertNotNull($<HTMLSelectElement>('#user_theme'));
|
||||
const styleSheet = assertNotNull($<HTMLLinkElement>('head link[rel="stylesheet"]'));
|
||||
|
||||
// Local settings
|
||||
localCheckboxes.forEach(checkbox => {
|
||||
|
@ -22,9 +24,7 @@ export function setupSettings() {
|
|||
// Theme preview
|
||||
if (themeSelect) {
|
||||
themeSelect.addEventListener('change', () => {
|
||||
if (styleSheet) {
|
||||
styleSheet.href = themeSelect.options[themeSelect.selectedIndex].dataset.themePath || '#';
|
||||
}
|
||||
styleSheet.href = assertNotUndefined(themeSelect.options[themeSelect.selectedIndex].dataset.themePath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
import { $ } from './utils/dom';
|
||||
|
||||
interface ShortcutKeycodes {
|
||||
[key: string]: () => void
|
||||
}
|
||||
type ShortcutKeyMap = Record<string, () => void>;
|
||||
|
||||
function getHover(): string | null {
|
||||
const thumbBoxHover = $<HTMLDivElement>('.media-box:hover');
|
||||
|
@ -45,7 +43,7 @@ function isOK(event: KeyboardEvent): boolean {
|
|||
document.activeElement.tagName !== 'TEXTAREA';
|
||||
}
|
||||
|
||||
const keyCodes: ShortcutKeycodes = {
|
||||
const keyCodes: ShortcutKeyMap = {
|
||||
KeyJ() { click('.js-prev'); }, // J - go to previous image
|
||||
KeyI() { click('.js-up'); }, // I - go to index page
|
||||
KeyK() { click('.js-next'); }, // K - go to next image
|
||||
|
@ -55,17 +53,16 @@ const keyCodes: ShortcutKeycodes = {
|
|||
KeyO() { openFullView(); }, // O - open original
|
||||
KeyV() { openFullViewNewTab(); }, // V - open original in a new tab
|
||||
KeyF() { // F - favourite image
|
||||
/* Gotta use a "return" here and in the next function because eslint is silly */
|
||||
return getHover() ? click(`a.interaction--fave[data-image-id="${getHover()}"]`)
|
||||
: click('.block__header a.interaction--fave');
|
||||
click(getHover() ? `a.interaction--fave[data-image-id="${getHover()}"]`
|
||||
: '.block__header a.interaction--fave');
|
||||
},
|
||||
KeyU() { // U - upvote image
|
||||
return getHover() ? click(`a.interaction--upvote[data-image-id="${getHover()}"]`)
|
||||
: click('.block__header a.interaction--upvote');
|
||||
click(getHover() ? `a.interaction--upvote[data-image-id="${getHover()}"]`
|
||||
: '.block__header a.interaction--upvote');
|
||||
},
|
||||
};
|
||||
|
||||
function listenForKeys() {
|
||||
export function listenForKeys() {
|
||||
document.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (isOK(event) && keyCodes[event.code]) {
|
||||
keyCodes[event.code]();
|
||||
|
@ -73,5 +70,3 @@ function listenForKeys() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { listenForKeys };
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { assertNotNull } from './utils/assert';
|
||||
import { $ } from './utils/dom';
|
||||
import { inputDuplicatorCreator } from './input-duplicator';
|
||||
import '../types/ujs';
|
||||
|
||||
export interface TagSourceEvent extends CustomEvent<Response> {
|
||||
target: HTMLElement,
|
||||
|
@ -14,19 +16,17 @@ function setupInputs() {
|
|||
});
|
||||
}
|
||||
|
||||
function imageSourcesCreator() {
|
||||
export function imageSourcesCreator() {
|
||||
setupInputs();
|
||||
|
||||
document.addEventListener('fetchcomplete', (({ target, detail }: TagSourceEvent) => {
|
||||
const sourceSauce = $<HTMLElement>('.js-sourcesauce');
|
||||
document.addEventListener('fetchcomplete', ({ target, detail }) => {
|
||||
if (target.matches('#source-form')) {
|
||||
const sourceSauce = assertNotNull($<HTMLElement>('.js-sourcesauce'));
|
||||
|
||||
if (sourceSauce && target && target.matches('#source-form')) {
|
||||
detail.text().then(text => {
|
||||
sourceSauce.outerHTML = text;
|
||||
setupInputs();
|
||||
});
|
||||
}
|
||||
}) as EventListener);
|
||||
});
|
||||
}
|
||||
|
||||
export { imageSourcesCreator };
|
||||
|
|
|
@ -4,12 +4,10 @@
|
|||
* Hide staff elements if enabled in the settings.
|
||||
*/
|
||||
|
||||
import { $$ } from './utils/dom';
|
||||
import { $$, hideEl } from './utils/dom';
|
||||
|
||||
export function hideStaffTools() {
|
||||
if (window.booru.hideStaffTools === 'true') {
|
||||
$$<HTMLElement>('.js-staff-action').forEach(el => {
|
||||
el.classList.add('hidden');
|
||||
});
|
||||
$$<HTMLElement>('.js-staff-action').forEach(el => hideEl(el));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import { $$, showEl, hideEl } from './utils/dom';
|
||||
import { assertNotUndefined } from './utils/assert';
|
||||
import { TagSourceEvent } from './sources';
|
||||
import '../types/ujs';
|
||||
|
||||
type TagDropdownActionFunction = () => void;
|
||||
type TagDropdownActionList = Record<string, TagDropdownActionFunction>;
|
||||
|
@ -19,8 +19,8 @@ function removeTag(tagId: number, list: number[]) {
|
|||
|
||||
function createTagDropdown(tag: HTMLSpanElement) {
|
||||
const { userIsSignedIn, userCanEditFilter, watchedTagList, spoileredTagList, hiddenTagList } = window.booru;
|
||||
const [ unwatch, watch, unspoiler, spoiler, unhide, hide, signIn, filter ] = $$<HTMLElement>('.tag__dropdown__link');
|
||||
const [ unwatched, watched, spoilered, hidden ] = $$<HTMLSpanElement>('.tag__state');
|
||||
const [ unwatch, watch, unspoiler, spoiler, unhide, hide, signIn, filter ] = $$<HTMLElement>('.tag__dropdown__link', tag);
|
||||
const [ unwatched, watched, spoilered, hidden ] = $$<HTMLSpanElement>('.tag__state', tag);
|
||||
const tagId = parseInt(assertNotUndefined(tag.dataset.tagId), 10);
|
||||
|
||||
const actions: TagDropdownActionList = {
|
||||
|
@ -56,15 +56,14 @@ function createTagDropdown(tag: HTMLSpanElement) {
|
|||
if (userIsSignedIn &&
|
||||
!userCanEditFilter) showEl(filter);
|
||||
|
||||
tag.addEventListener('fetchcomplete', ((event: TagSourceEvent) => {
|
||||
const act = event.target.dataset.tagAction;
|
||||
|
||||
if (act && actions[act]) {
|
||||
actions[act]();
|
||||
}
|
||||
}) as EventListener);
|
||||
tag.addEventListener('fetchcomplete', event => {
|
||||
const act = assertNotUndefined(event.target.dataset.tagAction);
|
||||
actions[act]();
|
||||
});
|
||||
}
|
||||
|
||||
export function initTagDropdown() {
|
||||
[].forEach.call($$<HTMLSpanElement>('.tag.dropdown'), createTagDropdown);
|
||||
for (const tagSpan of $$<HTMLSpanElement>('.tag.dropdown')) {
|
||||
createTagDropdown(tagSpan);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,42 +2,37 @@
|
|||
* Tags Misc
|
||||
*/
|
||||
|
||||
import { assertType, assertNotNull } from './utils/assert';
|
||||
import { $, $$ } from './utils/dom';
|
||||
import store from './utils/store';
|
||||
import { initTagDropdown } from './tags';
|
||||
import { setupTagsInput, reloadTagsInput } from './tagsinput';
|
||||
import { TagSourceEvent } from './sources';
|
||||
import '../types/ujs';
|
||||
|
||||
type TagInputActionFunction = (tagInput: HTMLTextAreaElement | null) => void
|
||||
type TagInputActionList = {
|
||||
save: TagInputActionFunction,
|
||||
load: TagInputActionFunction,
|
||||
clear: TagInputActionFunction,
|
||||
}
|
||||
type TagInputActionFunction = (tagInput: HTMLTextAreaElement) => void;
|
||||
type TagInputActionList = Record<string, TagInputActionFunction>;
|
||||
|
||||
function tagInputButtons(event: MouseEvent) {
|
||||
const target = assertType(event.target, HTMLElement);
|
||||
|
||||
function tagInputButtons({target}: PointerEvent) {
|
||||
const actions: TagInputActionList = {
|
||||
save(tagInput: HTMLTextAreaElement | null) {
|
||||
if (tagInput) store.set('tag_input', tagInput.value);
|
||||
save(tagInput: HTMLTextAreaElement) {
|
||||
store.set('tag_input', tagInput.value);
|
||||
},
|
||||
load(tagInput: HTMLTextAreaElement | null) {
|
||||
if (!tagInput) { return; }
|
||||
|
||||
load(tagInput: HTMLTextAreaElement) {
|
||||
// If entry 'tag_input' does not exist, try to use the current list
|
||||
tagInput.value = store.get('tag_input') || tagInput.value;
|
||||
reloadTagsInput(tagInput);
|
||||
},
|
||||
clear(tagInput: HTMLTextAreaElement | null) {
|
||||
if (!tagInput) { return; }
|
||||
|
||||
clear(tagInput: HTMLTextAreaElement) {
|
||||
tagInput.value = '';
|
||||
reloadTagsInput(tagInput);
|
||||
},
|
||||
};
|
||||
|
||||
for (const action in actions) {
|
||||
if (target && (target as HTMLElement).matches(`#tagsinput-${action}`)) {
|
||||
actions[action as keyof TagInputActionList]($<HTMLTextAreaElement>('image_tag_input'));
|
||||
for (const [ name, action ] of Object.entries(actions)) {
|
||||
if (target && target.matches(`#tagsinput-${name}`)) {
|
||||
action(assertNotNull($<HTMLTextAreaElement>('#image_tag_input')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,10 +44,10 @@ function setupTags() {
|
|||
});
|
||||
}
|
||||
|
||||
function updateTagSauce({target, detail}: TagSourceEvent) {
|
||||
const tagSauce = $<HTMLDivElement>('.js-tagsauce');
|
||||
function updateTagSauce({ target, detail }: FetchcompleteEvent) {
|
||||
if (target.matches('#tags-form')) {
|
||||
const tagSauce = assertNotNull($<HTMLDivElement>('.js-tagsauce'));
|
||||
|
||||
if (tagSauce && target.matches('#tags-form')) {
|
||||
detail.text().then(text => {
|
||||
tagSauce.outerHTML = text;
|
||||
setupTags();
|
||||
|
@ -63,8 +58,8 @@ function updateTagSauce({target, detail}: TagSourceEvent) {
|
|||
|
||||
function setupTagEvents() {
|
||||
setupTags();
|
||||
document.addEventListener('fetchcomplete', updateTagSauce as EventListener);
|
||||
document.addEventListener('click', tagInputButtons as EventListener);
|
||||
document.addEventListener('fetchcomplete', updateTagSauce);
|
||||
document.addEventListener('click', tagInputButtons);
|
||||
}
|
||||
|
||||
export { setupTagEvents };
|
||||
|
|
|
@ -28,12 +28,12 @@ describe('Local Autocompleter', () => {
|
|||
});
|
||||
|
||||
describe('instantiation', () => {
|
||||
it('should be constructable with compatible data', () => {
|
||||
it('should be constructible with compatible data', () => {
|
||||
const result = new LocalAutocompleter(mockData);
|
||||
expect(result).toBeInstanceOf(LocalAutocompleter);
|
||||
});
|
||||
|
||||
it('should NOT be constructable with incompatible data', () => {
|
||||
it('should NOT be constructible with incompatible data', () => {
|
||||
const versionDataOffset = 12;
|
||||
const mockIncompatibleDataArray = new Array(versionDataOffset).fill(0);
|
||||
// Set data version to 1
|
||||
|
@ -45,6 +45,8 @@ describe('Local Autocompleter', () => {
|
|||
});
|
||||
|
||||
describe('topK', () => {
|
||||
const termStem = ['f', 'o'].join('');
|
||||
|
||||
let localAc: LocalAutocompleter;
|
||||
|
||||
beforeAll(() => {
|
||||
|
@ -66,7 +68,7 @@ describe('Local Autocompleter', () => {
|
|||
});
|
||||
|
||||
it('should return suggestions sorted by image count', () => {
|
||||
const result = localAc.topK('fo', defaultK);
|
||||
const result = localAc.topK(termStem, defaultK);
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({ name: 'forest', imageCount: 3 }),
|
||||
expect.objectContaining({ name: 'fog', imageCount: 1 }),
|
||||
|
@ -82,13 +84,13 @@ describe('Local Autocompleter', () => {
|
|||
});
|
||||
|
||||
it('should return only the required number of suggestions', () => {
|
||||
const result = localAc.topK('fo', 1);
|
||||
const result = localAc.topK(termStem, 1);
|
||||
expect(result).toEqual([expect.objectContaining({ name: 'forest', imageCount: 3 })]);
|
||||
});
|
||||
|
||||
it('should NOT return suggestions associated with hidden tags', () => {
|
||||
window.booru.hiddenTagList = [1];
|
||||
const result = localAc.topK('fo', defaultK);
|
||||
const result = localAc.topK(termStem, defaultK);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Functions to execute when the DOM is ready
|
||||
*/
|
||||
|
||||
import { whenReady } from './utils/dom';
|
||||
import { whenReady } from './utils/dom';
|
||||
|
||||
import { listenAutocomplete } from './autocomplete';
|
||||
import { loadBooruData } from './booru';
|
||||
|
|
361
assets/package-lock.json
generated
361
assets/package-lock.json
generated
|
@ -440,9 +440,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
@ -455,9 +455,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
@ -470,9 +470,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -485,9 +485,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -500,9 +500,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -515,9 +515,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -530,9 +530,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -545,9 +545,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -560,9 +560,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
|
||||
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
@ -575,9 +575,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -590,9 +590,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
@ -605,9 +605,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
|
||||
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
|
@ -620,9 +620,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
|
||||
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
|
@ -635,9 +635,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
|
||||
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
@ -650,9 +650,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
|
||||
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
@ -665,9 +665,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
|
||||
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
@ -680,9 +680,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -695,9 +695,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -710,9 +710,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -725,9 +725,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -740,9 +740,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
|
||||
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -755,9 +755,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
|
||||
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
@ -770,9 +770,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
|
||||
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -818,11 +818,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.15.1.tgz",
|
||||
"integrity": "sha512-K4gzNq+yymn/EVsXYmf+SBcBro8MTf+aXJZUphM96CdzUEr+ClGDvAbpmaEK+cGVigVXIgs9gNmvHAlrzzY5JQ==",
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz",
|
||||
"integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.3",
|
||||
"@eslint/object-schema": "^2.1.4",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.0.5"
|
||||
},
|
||||
|
@ -853,9 +853,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.4.0.tgz",
|
||||
"integrity": "sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg==",
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz",
|
||||
"integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
|
@ -1409,9 +1409,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz",
|
||||
"integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==",
|
||||
"version": "20.14.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz",
|
||||
"integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
|
@ -1931,9 +1931,9 @@
|
|||
"deprecated": "Use your platform's native atob() and btoa() methods instead"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
|
||||
"integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
@ -1959,9 +1959,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
|
||||
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
|
||||
"version": "8.3.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
|
||||
"integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
|
@ -2178,9 +2181,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001632",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz",
|
||||
"integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==",
|
||||
"version": "1.0.30001636",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz",
|
||||
"integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -2526,9 +2529,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.799",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.799.tgz",
|
||||
"integrity": "sha512-3D3DwWkRTzrdEpntY0hMLYwj7SeBk1138CkPE8sBDSj3WzrzOiG2rHm3luw8jucpf+WiyLBCZyU9lMHyQI9M9Q=="
|
||||
"version": "1.4.810",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.810.tgz",
|
||||
"integrity": "sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
|
@ -2566,9 +2569,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
|
@ -2577,29 +2580,29 @@
|
|||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.20.2",
|
||||
"@esbuild/android-arm": "0.20.2",
|
||||
"@esbuild/android-arm64": "0.20.2",
|
||||
"@esbuild/android-x64": "0.20.2",
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@esbuild/freebsd-arm64": "0.20.2",
|
||||
"@esbuild/freebsd-x64": "0.20.2",
|
||||
"@esbuild/linux-arm": "0.20.2",
|
||||
"@esbuild/linux-arm64": "0.20.2",
|
||||
"@esbuild/linux-ia32": "0.20.2",
|
||||
"@esbuild/linux-loong64": "0.20.2",
|
||||
"@esbuild/linux-mips64el": "0.20.2",
|
||||
"@esbuild/linux-ppc64": "0.20.2",
|
||||
"@esbuild/linux-riscv64": "0.20.2",
|
||||
"@esbuild/linux-s390x": "0.20.2",
|
||||
"@esbuild/linux-x64": "0.20.2",
|
||||
"@esbuild/netbsd-x64": "0.20.2",
|
||||
"@esbuild/openbsd-x64": "0.20.2",
|
||||
"@esbuild/sunos-x64": "0.20.2",
|
||||
"@esbuild/win32-arm64": "0.20.2",
|
||||
"@esbuild/win32-ia32": "0.20.2",
|
||||
"@esbuild/win32-x64": "0.20.2"
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
|
@ -2642,15 +2645,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.4.0.tgz",
|
||||
"integrity": "sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA==",
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz",
|
||||
"integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/config-array": "^0.15.1",
|
||||
"@eslint/config-array": "^0.16.0",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "9.4.0",
|
||||
"@eslint/js": "9.5.0",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.3.0",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
|
@ -2662,7 +2665,7 @@
|
|||
"eslint-scope": "^8.0.1",
|
||||
"eslint-visitor-keys": "^4.0.0",
|
||||
"espree": "^10.0.1",
|
||||
"esquery": "^1.4.2",
|
||||
"esquery": "^1.5.0",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^8.0.0",
|
||||
|
@ -2688,7 +2691,7 @@
|
|||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
"url": "https://eslint.org/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vitest": {
|
||||
|
@ -2716,13 +2719,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz",
|
||||
"integrity": "sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==",
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz",
|
||||
"integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.13.0",
|
||||
"@typescript-eslint/visitor-keys": "7.13.0"
|
||||
"@typescript-eslint/types": "7.13.1",
|
||||
"@typescript-eslint/visitor-keys": "7.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -2733,9 +2736,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/types": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.0.tgz",
|
||||
"integrity": "sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==",
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz",
|
||||
"integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -2746,13 +2749,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz",
|
||||
"integrity": "sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==",
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz",
|
||||
"integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.13.0",
|
||||
"@typescript-eslint/visitor-keys": "7.13.0",
|
||||
"@typescript-eslint/types": "7.13.1",
|
||||
"@typescript-eslint/visitor-keys": "7.13.1",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
|
@ -2774,15 +2777,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.0.tgz",
|
||||
"integrity": "sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==",
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.13.1.tgz",
|
||||
"integrity": "sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "7.13.0",
|
||||
"@typescript-eslint/types": "7.13.0",
|
||||
"@typescript-eslint/typescript-estree": "7.13.0"
|
||||
"@typescript-eslint/scope-manager": "7.13.1",
|
||||
"@typescript-eslint/types": "7.13.1",
|
||||
"@typescript-eslint/typescript-estree": "7.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -2796,12 +2799,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vitest/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz",
|
||||
"integrity": "sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==",
|
||||
"version": "7.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz",
|
||||
"integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.13.0",
|
||||
"@typescript-eslint/types": "7.13.1",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2875,11 +2878,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
|
||||
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
|
||||
"integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.3",
|
||||
"acorn": "^8.12.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^4.0.0"
|
||||
},
|
||||
|
@ -5111,24 +5114,44 @@
|
|||
}
|
||||
},
|
||||
"node_modules/stylelint-config-recommended": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.0.tgz",
|
||||
"integrity": "sha512-jSkx290CglS8StmrLp2TxAppIajzIBZKYm3IxT89Kg6fGlxbPiTiyH9PS5YUuVAFwaJLl1ikiXX0QWjI0jmgZQ==",
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz",
|
||||
"integrity": "sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/stylelint"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/stylelint"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"stylelint": "^16.0.0"
|
||||
"stylelint": "^16.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stylelint-config-standard": {
|
||||
"version": "36.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-36.0.0.tgz",
|
||||
"integrity": "sha512-3Kjyq4d62bYFp/Aq8PMKDwlgUyPU4nacXsjDLWJdNPRUgpuxALu1KnlAHIj36cdtxViVhXexZij65yM0uNIHug==",
|
||||
"version": "36.0.1",
|
||||
"resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-36.0.1.tgz",
|
||||
"integrity": "sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/stylelint"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/stylelint"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"stylelint-config-recommended": "^14.0.0"
|
||||
"stylelint-config-recommended": "^14.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
|
@ -5428,9 +5451,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
|
||||
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
@ -5533,11 +5556,11 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.2.13",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.13.tgz",
|
||||
"integrity": "sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz",
|
||||
"integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.20.1",
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.38",
|
||||
"rollup": "^4.13.0"
|
||||
},
|
||||
|
@ -5869,9 +5892,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
|
|
@ -9,6 +9,7 @@ window.booru = {
|
|||
csrfToken: 'mockCsrfToken',
|
||||
hiddenTag: '/mock-tagblocked.svg',
|
||||
hiddenTagList: [],
|
||||
hideStaffTools: 'true',
|
||||
ignoredTagList: [],
|
||||
imagesWithDownvotingDisabled: [],
|
||||
spoilerType: 'off',
|
||||
|
@ -20,7 +21,6 @@ window.booru = {
|
|||
spoileredFilter: matchNone(),
|
||||
interactions: [],
|
||||
tagsVersion: 5,
|
||||
hideStaffTools: 'false',
|
||||
galleryImages: []
|
||||
};
|
||||
|
||||
|
|
2
assets/types/booru-object.d.ts
vendored
2
assets/types/booru-object.d.ts
vendored
|
@ -72,7 +72,7 @@ interface BooruObject {
|
|||
/**
|
||||
* List of image IDs in the current gallery.
|
||||
*/
|
||||
galleryImages: number[]
|
||||
galleryImages?: number[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
11
assets/types/ujs.ts
Normal file
11
assets/types/ujs.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
export {};
|
||||
|
||||
declare global {
|
||||
interface FetchcompleteEvent extends CustomEvent<Response> {
|
||||
target: HTMLElement,
|
||||
}
|
||||
|
||||
interface GlobalEventHandlersEventMap {
|
||||
fetchcomplete: FetchcompleteEvent;
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ config :canary,
|
|||
|
||||
# Configures the endpoint
|
||||
config :philomena, PhilomenaWeb.Endpoint,
|
||||
adapter: Bandit.PhoenixAdapter,
|
||||
url: [host: "localhost"],
|
||||
secret_key_base: "xZYTon09JNRrj8snd7KL31wya4x71jmo5aaSSRmw1dGjWLRmEwWMTccwxgsGFGjM",
|
||||
render_errors: [view: PhilomenaWeb.ErrorView, accepts: ~w(html json)],
|
||||
|
@ -46,8 +47,6 @@ config :phoenix, :template_engines,
|
|||
slime: PhoenixSlime.Engine,
|
||||
slimleex: PhoenixSlime.LiveViewEngine
|
||||
|
||||
config :tesla, adapter: Tesla.Adapter.Mint
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
|
|
|
@ -73,8 +73,7 @@ config :philomena, :s3_primary_options,
|
|||
host: System.fetch_env!("S3_HOST"),
|
||||
port: System.fetch_env!("S3_PORT"),
|
||||
access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
|
||||
secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY"),
|
||||
http_opts: [timeout: 180_000, recv_timeout: 180_000]
|
||||
secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")
|
||||
|
||||
config :philomena, :s3_primary_bucket, System.fetch_env!("S3_BUCKET")
|
||||
|
||||
|
@ -84,8 +83,7 @@ config :philomena, :s3_secondary_options,
|
|||
host: System.get_env("ALT_S3_HOST"),
|
||||
port: System.get_env("ALT_S3_PORT"),
|
||||
access_key_id: System.get_env("ALT_AWS_ACCESS_KEY_ID"),
|
||||
secret_access_key: System.get_env("ALT_AWS_SECRET_ACCESS_KEY"),
|
||||
http_opts: [timeout: 180_000, recv_timeout: 180_000]
|
||||
secret_access_key: System.get_env("ALT_AWS_SECRET_ACCESS_KEY")
|
||||
|
||||
config :philomena, :s3_secondary_bucket, System.get_env("ALT_S3_BUCKET")
|
||||
|
||||
|
@ -93,11 +91,7 @@ config :philomena, :s3_secondary_bucket, System.get_env("ALT_S3_BUCKET")
|
|||
config :elastix,
|
||||
httpoison_options: [ssl: [verify: :verify_none]]
|
||||
|
||||
config :ex_aws, :hackney_opts,
|
||||
timeout: 180_000,
|
||||
recv_timeout: 180_000,
|
||||
use_default_pool: false,
|
||||
pool: false
|
||||
config :ex_aws, http_client: PhilomenaMedia.Req
|
||||
|
||||
config :ex_aws, :retries,
|
||||
max_attempts: 20,
|
||||
|
|
|
@ -60,7 +60,7 @@ services:
|
|||
- '5173:5173'
|
||||
|
||||
postgres:
|
||||
image: postgres:16.2-alpine
|
||||
image: postgres:16.3-alpine
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
volumes:
|
||||
|
@ -86,7 +86,7 @@ services:
|
|||
driver: "none"
|
||||
|
||||
files:
|
||||
image: andrewgaul/s3proxy:sha-ec12ae0
|
||||
image: andrewgaul/s3proxy:sha-4175022
|
||||
environment:
|
||||
- JCLOUDS_FILESYSTEM_BASEDIR=/srv/philomena/priv/s3
|
||||
volumes:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM elixir:1.16.2-alpine
|
||||
FROM elixir:1.17-alpine
|
||||
|
||||
ADD https://api.github.com/repos/philomena-dev/FFmpeg/git/refs/heads/release/6.1 /tmp/ffmpeg_version.json
|
||||
RUN (echo "https://github.com/philomena-dev/prebuilt-ffmpeg/raw/master"; cat /etc/apk/repositories) > /tmp/repositories \
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM openresty/openresty:1.25.3.1-2-alpine
|
||||
FROM openresty/openresty:1.25.3.1-4-alpine
|
||||
ARG APP_DIR
|
||||
ARG S3_SCHEME
|
||||
ARG S3_HOST
|
||||
|
|
|
@ -32,10 +32,7 @@ defmodule Philomena.Application do
|
|||
PhilomenaWeb.AdvertUpdater,
|
||||
PhilomenaWeb.UserFingerprintUpdater,
|
||||
PhilomenaWeb.UserIpUpdater,
|
||||
PhilomenaWeb.Endpoint,
|
||||
|
||||
# Connection drainer for SIGTERM
|
||||
{Plug.Cowboy.Drainer, refs: [PhilomenaWeb.Endpoint.HTTP]}
|
||||
PhilomenaWeb.Endpoint
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
|
|
|
@ -12,7 +12,7 @@ defmodule Philomena.ArtistLinks.AutomaticVerifier do
|
|||
end
|
||||
end
|
||||
|
||||
defp contains_verification_code?({:ok, %Tesla.Env{body: body, status: 200}}, code) do
|
||||
defp contains_verification_code?({:ok, %{body: body, status: 200}}, code) do
|
||||
String.contains?(body, code)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
defprotocol Philomena.Attribution do
|
||||
@doc """
|
||||
Provides the "parent object" identifier for this object. This is so
|
||||
that anonymous posts under the same topic id can return the same hash
|
||||
for the same user.
|
||||
Provides the "parent object" identifier for this object. This is so
|
||||
that anonymous posts under the same topic id can return the same hash
|
||||
for the same user.
|
||||
"""
|
||||
@spec object_identifier(struct()) :: String.t()
|
||||
def object_identifier(object)
|
||||
|
||||
@doc """
|
||||
Provides the "best" user identifier for an object. Usually this will be
|
||||
the user_id, but may also be the fingerprint or IP address if other
|
||||
information is unavailable.
|
||||
Provides the "best" user identifier for an object. Usually this will be
|
||||
the user_id, but may also be the fingerprint or IP address if other
|
||||
information is unavailable.
|
||||
"""
|
||||
@spec best_user_identifier(struct()) :: String.t()
|
||||
def best_user_identifier(object)
|
||||
|
||||
@doc """
|
||||
Return whether this object is considered to be anonymous.
|
||||
Return whether this object is considered to be anonymous.
|
||||
"""
|
||||
@spec anonymous?(struct()) :: true | false
|
||||
def anonymous?(object)
|
||||
|
|
|
@ -6,7 +6,7 @@ defmodule Philomena.Channels.PicartoChannel do
|
|||
@api_online
|
||||
|> PhilomenaProxy.Http.get()
|
||||
|> case do
|
||||
{:ok, %Tesla.Env{body: body, status: 200}} ->
|
||||
{:ok, %{body: body, status: 200}} ->
|
||||
body
|
||||
|> Jason.decode!()
|
||||
|> Map.new(&{&1["name"], fetch(&1, now)})
|
||||
|
|
|
@ -6,7 +6,7 @@ defmodule Philomena.Channels.PiczelChannel do
|
|||
@api_online
|
||||
|> PhilomenaProxy.Http.get()
|
||||
|> case do
|
||||
{:ok, %Tesla.Env{body: body, status: 200}} ->
|
||||
{:ok, %{body: body, status: 200}} ->
|
||||
body
|
||||
|> Jason.decode!()
|
||||
|> Map.new(&{&1["slug"], fetch(&1, now)})
|
||||
|
|
|
@ -88,7 +88,7 @@ defmodule Philomena.Comments.Query do
|
|||
|
||||
defp parse(fields, context, query_string) do
|
||||
fields
|
||||
|> Parser.parser()
|
||||
|> Parser.new()
|
||||
|> Parser.parse(query_string, context)
|
||||
end
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ defmodule Philomena.Filters.Query do
|
|||
|
||||
defp parse(fields, context, query_string) do
|
||||
fields
|
||||
|> Parser.parser()
|
||||
|> Parser.new()
|
||||
|> Parser.parse(query_string, context)
|
||||
end
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ defmodule Philomena.Galleries.Query do
|
|||
query_string = query_string || ""
|
||||
|
||||
fields()
|
||||
|> Parser.parser()
|
||||
|> Parser.new()
|
||||
|> Parser.parse(query_string)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -937,7 +937,7 @@ defmodule Philomena.Images do
|
|||
(source.sources ++ target.sources)
|
||||
|> Enum.map(fn s -> %Source{image_id: target.id, source: s.source} end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.take(10)
|
||||
|> Enum.take(15)
|
||||
|
||||
target
|
||||
|> Image.sources_changeset(sources)
|
||||
|
|
|
@ -212,11 +212,13 @@ defmodule Philomena.Images.Image do
|
|||
image
|
||||
|> cast(attrs, [])
|
||||
|> SourceDiffer.diff_input(old_sources, new_sources)
|
||||
|> validate_length(:sources, max: 15)
|
||||
end
|
||||
|
||||
def sources_changeset(image, new_sources) do
|
||||
change(image)
|
||||
|> put_assoc(:sources, new_sources)
|
||||
|> validate_length(:sources, max: 15)
|
||||
end
|
||||
|
||||
def tag_changeset(image, attrs, old_tags, new_tags, excluded_tags \\ []) do
|
||||
|
|
|
@ -140,7 +140,7 @@ defmodule Philomena.Images.Query do
|
|||
|
||||
defp parse(fields, context, query_string) do
|
||||
fields
|
||||
|> Parser.parser()
|
||||
|> Parser.new()
|
||||
|> Parser.parse(query_string, context)
|
||||
end
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ defmodule Philomena.Posts.Query do
|
|||
|
||||
defp parse(fields, context, query_string) do
|
||||
fields
|
||||
|> Parser.parser()
|
||||
|> Parser.new()
|
||||
|> Parser.parse(query_string, context)
|
||||
end
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ defmodule Philomena.Reports.Query do
|
|||
|
||||
def compile(query_string) do
|
||||
fields()
|
||||
|> Parser.parser()
|
||||
|> Parser.new()
|
||||
|> Parser.parse(query_string || "", %{})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -251,7 +251,7 @@ defmodule Philomena.Tags do
|
|||
|> where(tag_id: ^tag.id)
|
||||
|> Repo.delete_all()
|
||||
|
||||
# Update other assocations
|
||||
# Update other associations
|
||||
ArtistLink
|
||||
|> where(tag_id: ^tag.id)
|
||||
|> Repo.update_all(set: [tag_id: target_tag.id])
|
||||
|
|
|
@ -19,7 +19,7 @@ defmodule Philomena.Tags.Query do
|
|||
|
||||
def compile(query_string) do
|
||||
fields()
|
||||
|> Parser.parser()
|
||||
|> Parser.new()
|
||||
|> Parser.parse(query_string || "")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -88,7 +88,7 @@ defmodule Philomena.Users.UserNotifier do
|
|||
|
||||
Your account has been automatically locked due to too many attempts to sign in.
|
||||
|
||||
You can unlock your account by visting the URL below:
|
||||
You can unlock your account by visiting the URL below:
|
||||
|
||||
#{url}
|
||||
|
||||
|
|
|
@ -54,7 +54,8 @@ defmodule PhilomenaMedia.Analyzers do
|
|||
:error = Analyzers.analyze(file)
|
||||
|
||||
"""
|
||||
@spec analyze(Plug.Upload.t() | Path.t()) :: {:ok, Result.t()} | :error
|
||||
@spec analyze(Plug.Upload.t() | Path.t()) ::
|
||||
{:ok, Result.t()} | {:unsupported_mime, Mime.t()} | :error
|
||||
def analyze(%Plug.Upload{path: path}), do: analyze(path)
|
||||
|
||||
def analyze(path) when is_binary(path) do
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
defmodule PhilomenaMedia.Analyzers.Analyzer do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Generate a `m:PhilomenaMedia.Analyzers.Result` for file at the given path.
|
||||
"""
|
||||
@callback analyze(Path.t()) :: PhilomenaMedia.Analyzers.Result.t()
|
||||
end
|
||||
|
|
117
lib/philomena_media/gif_preview.ex
Normal file
117
lib/philomena_media/gif_preview.ex
Normal file
|
@ -0,0 +1,117 @@
|
|||
defmodule PhilomenaMedia.GifPreview do
|
||||
@moduledoc """
|
||||
GIF preview generation for video files.
|
||||
"""
|
||||
|
||||
@type duration :: float()
|
||||
@type dimensions :: {pos_integer(), pos_integer()}
|
||||
|
||||
@type num_images :: integer()
|
||||
@type target_framerate :: 1..50
|
||||
@type opts :: [
|
||||
num_images: num_images(),
|
||||
target_framerate: target_framerate()
|
||||
]
|
||||
|
||||
@doc """
|
||||
Generate a GIF preview of the given video input with evenly-spaced sample points.
|
||||
|
||||
The input should have pre-computed duration `duration`. The `dimensions`
|
||||
are a `{target_width, target_height}` tuple of the largest dimensions desired,
|
||||
and the image will be resized to fit inside the box of those dimensions,
|
||||
preserving aspect ratio.
|
||||
|
||||
Depending on the input file, this may take a long time to process.
|
||||
|
||||
Options:
|
||||
- `:target_framerate` - framerate of the output GIF, must be between 1 and 50.
|
||||
Default 2.
|
||||
- `:num_images` - number of images to sample from the video.
|
||||
Default is determined by the duration:
|
||||
* 90 or above: 20 images
|
||||
* 30 or above: 10 images
|
||||
* 1 or above: 5 images
|
||||
* otherwise: 2 images
|
||||
"""
|
||||
@spec preview(Path.t(), Path.t(), duration(), dimensions(), opts()) :: :ok
|
||||
def preview(video, gif, duration, dimensions, opts \\ []) do
|
||||
target_framerate = Keyword.get(opts, :target_framerate, 2)
|
||||
|
||||
num_images =
|
||||
Keyword.get_lazy(opts, :num_images, fn ->
|
||||
cond do
|
||||
duration >= 90 -> 20
|
||||
duration >= 30 -> 10
|
||||
duration >= 1 -> 5
|
||||
true -> 2
|
||||
end
|
||||
end)
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd(
|
||||
"ffmpeg",
|
||||
commands(video, gif, clamp(duration), dimensions, num_images, target_framerate)
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec commands(Path.t(), Path.t(), duration(), dimensions(), num_images(), target_framerate()) ::
|
||||
[String.t()]
|
||||
defp commands(video, gif, duration, {target_width, target_height}, num_images, target_framerate) do
|
||||
# Compute range [0, num_images)
|
||||
image_range = 0..(num_images - 1)
|
||||
|
||||
# Generate input list in the following form:
|
||||
# -ss 0.0 -i input.webm
|
||||
input_arguments =
|
||||
Enum.flat_map(image_range, &["-ss", "#{&1 * duration / num_images}", "-i", video])
|
||||
|
||||
# Generate graph in the following form:
|
||||
# [0:v] trim=end_frame=1 [t0]; [1:v] trim=end_frame=1 [t1] ...
|
||||
trim_filters =
|
||||
Enum.map_join(image_range, ";", &"[#{&1}:v] trim=end_frame=1 [t#{&1}]")
|
||||
|
||||
# Generate graph in the following form:
|
||||
# [t0][t1]... concat=n=10 [concat]
|
||||
concat_input_pads =
|
||||
Enum.map_join(image_range, "", &"[t#{&1}]")
|
||||
|
||||
concat_filter =
|
||||
"#{concat_input_pads} concat=n=#{num_images}, settb=1/#{target_framerate}, setpts=N [concat]"
|
||||
|
||||
scale_filter =
|
||||
"[concat] scale=width=#{target_width}:height=#{target_height}:" <>
|
||||
"force_original_aspect_ratio=decrease [scale]"
|
||||
|
||||
split_filter = "[scale] split [s0][s1]"
|
||||
|
||||
palettegen_filter =
|
||||
"[s0] palettegen=stats_mode=single:max_colors=255:reserve_transparent=1 [palettegen]"
|
||||
|
||||
paletteuse_filter =
|
||||
"[s1][palettegen] paletteuse=dither=bayer:bayer_scale=5:new=1:alpha_threshold=255"
|
||||
|
||||
filter_graph =
|
||||
[
|
||||
trim_filters,
|
||||
concat_filter,
|
||||
scale_filter,
|
||||
split_filter,
|
||||
palettegen_filter,
|
||||
paletteuse_filter
|
||||
]
|
||||
|> Enum.join(";")
|
||||
|
||||
# Delay in centiseconds - otherwise it will be computed incorrectly
|
||||
final_delay = 100.0 / target_framerate
|
||||
|
||||
["-loglevel", "0", "-y"]
|
||||
|> Kernel.++(input_arguments)
|
||||
|> Kernel.++(["-lavfi", filter_graph])
|
||||
|> Kernel.++(["-f", "gif", "-final_delay", "#{final_delay}", gif])
|
||||
end
|
||||
|
||||
defp clamp(duration) when duration <= 0, do: 1.0
|
||||
defp clamp(duration), do: duration
|
||||
end
|
|
@ -36,7 +36,7 @@ defmodule PhilomenaMedia.Intensities do
|
|||
|
||||
> #### Info {: .info}
|
||||
>
|
||||
> Clients should prefer to use `m:PhilomenaMedia.Processors.intensities/2`, as it handles
|
||||
> Clients should prefer to use `PhilomenaMedia.Processors.intensities/2`, as it handles
|
||||
> media files of any type supported by this library, not just PNG or JPEG.
|
||||
|
||||
## Examples
|
||||
|
|
|
@ -62,29 +62,31 @@ defmodule PhilomenaMedia.Processors do
|
|||
alias PhilomenaMedia.Processors.{Gif, Jpeg, Png, Svg, Webm}
|
||||
alias PhilomenaMedia.Mime
|
||||
|
||||
# The name of a version, like :large
|
||||
@typedoc "The name of a version, like `:large`."
|
||||
@type version_name :: atom()
|
||||
|
||||
@type dimensions :: {integer(), integer()}
|
||||
@type version_list :: [{version_name(), dimensions()}]
|
||||
|
||||
# The file name of a processed version, like "large.png"
|
||||
@typedoc "The file name of a processed version, like `large.png`."
|
||||
@type version_filename :: String.t()
|
||||
|
||||
# A single file to be copied to satisfy a request for a version name
|
||||
@typedoc "A single file to be copied to satisfy a request for a version name."
|
||||
@type copy_request :: {:copy, Path.t(), version_filename()}
|
||||
|
||||
# A list of thumbnail versions to copy into place
|
||||
@typedoc "A list of thumbnail versions to copy into place."
|
||||
@type thumbnails :: {:thumbnails, [copy_request()]}
|
||||
|
||||
# Replace the original file to strip metadata or losslessly optimize
|
||||
@typedoc "Replace the original file to strip metadata or losslessly optimize."
|
||||
@type replace_original :: {:replace_original, Path.t()}
|
||||
|
||||
# Apply the computed corner intensities
|
||||
@typedoc "Apply the computed corner intensities."
|
||||
@type intensities :: {:intensities, Intensities.t()}
|
||||
|
||||
# An edit script, representing the changes to apply to the storage backend
|
||||
# after successful processing
|
||||
@typedoc """
|
||||
An edit script, representing the changes to apply to the storage backend
|
||||
after successful processing.
|
||||
"""
|
||||
@type edit_script :: [thumbnails() | replace_original() | intensities()]
|
||||
|
||||
@doc """
|
||||
|
|
|
@ -5,17 +5,25 @@ defmodule PhilomenaMedia.Processors.Processor do
|
|||
alias PhilomenaMedia.Processors
|
||||
alias PhilomenaMedia.Intensities
|
||||
|
||||
# Generate a list of version filenames for the given version list.
|
||||
@doc """
|
||||
Generate a list of version filenames for the given version list.
|
||||
"""
|
||||
@callback versions(Processors.version_list()) :: [Processors.version_filename()]
|
||||
|
||||
# Process the media at the given path against the given version list, and return an
|
||||
# edit script with the resulting files
|
||||
@doc """
|
||||
Process the media at the given path against the given version list, and return an
|
||||
edit script with the resulting files.
|
||||
"""
|
||||
@callback process(Result.t(), Path.t(), Processors.version_list()) :: Processors.edit_script()
|
||||
|
||||
# Perform post-processing optimization tasks on the file, to reduce its size
|
||||
# and strip non-essential metadata
|
||||
@doc """
|
||||
Perform post-processing optimization tasks on the file, to reduce its size
|
||||
and strip non-essential metadata.
|
||||
"""
|
||||
@callback post_process(Result.t(), Path.t()) :: Processors.edit_script()
|
||||
|
||||
# Generate corner intensities for the given path
|
||||
@doc """
|
||||
Generate corner intensities for the given path.
|
||||
"""
|
||||
@callback intensities(Result.t(), Path.t()) :: Intensities.t()
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule PhilomenaMedia.Processors.Webm do
|
|||
|
||||
alias PhilomenaMedia.Intensities
|
||||
alias PhilomenaMedia.Analyzers.Result
|
||||
alias PhilomenaMedia.GifPreview
|
||||
alias PhilomenaMedia.Processors.Processor
|
||||
alias PhilomenaMedia.Processors
|
||||
import Bitwise
|
||||
|
@ -28,12 +29,11 @@ defmodule PhilomenaMedia.Processors.Webm do
|
|||
duration = analysis.duration
|
||||
stripped = strip(file)
|
||||
preview = preview(duration, stripped)
|
||||
palette = gif_palette(stripped, duration)
|
||||
mp4 = scale_mp4_only(stripped, dimensions, dimensions)
|
||||
|
||||
{:ok, intensities} = Intensities.file(preview)
|
||||
|
||||
scaled = Enum.flat_map(versions, &scale(stripped, palette, duration, dimensions, &1))
|
||||
scaled = Enum.flat_map(versions, &scale(stripped, duration, dimensions, &1))
|
||||
mp4 = [{:copy, mp4, "full.mp4"}]
|
||||
|
||||
[
|
||||
|
@ -82,12 +82,12 @@ defmodule PhilomenaMedia.Processors.Webm do
|
|||
stripped
|
||||
end
|
||||
|
||||
defp scale(file, palette, duration, dimensions, {thumb_name, target_dimensions}) do
|
||||
defp scale(file, duration, dimensions, {thumb_name, target_dimensions}) do
|
||||
{webm, mp4} = scale_videos(file, dimensions, target_dimensions)
|
||||
|
||||
cond do
|
||||
thumb_name in [:thumb, :thumb_small, :thumb_tiny] ->
|
||||
gif = scale_gif(file, palette, duration, target_dimensions)
|
||||
gif = scale_gif(file, duration, target_dimensions)
|
||||
|
||||
[
|
||||
{:copy, webm, "#{thumb_name}.webm"},
|
||||
|
@ -199,53 +199,14 @@ defmodule PhilomenaMedia.Processors.Webm do
|
|||
mp4
|
||||
end
|
||||
|
||||
defp scale_gif(file, palette, duration, {width, height}) do
|
||||
defp scale_gif(file, duration, dimensions) do
|
||||
gif = Briefly.create!(extname: ".gif")
|
||||
scale_filter = "scale=w=#{width}:h=#{height}:force_original_aspect_ratio=decrease"
|
||||
palette_filter = "paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle"
|
||||
rate_filter = rate_filter(duration)
|
||||
filter_graph = "[0:v]#{scale_filter},#{rate_filter}[x];[x][1:v]#{palette_filter}"
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("ffmpeg", [
|
||||
"-loglevel",
|
||||
"0",
|
||||
"-y",
|
||||
"-i",
|
||||
file,
|
||||
"-i",
|
||||
palette,
|
||||
"-lavfi",
|
||||
filter_graph,
|
||||
"-r",
|
||||
"2",
|
||||
gif
|
||||
])
|
||||
GifPreview.preview(file, gif, duration, dimensions)
|
||||
|
||||
gif
|
||||
end
|
||||
|
||||
defp gif_palette(file, duration) do
|
||||
palette = Briefly.create!(extname: ".png")
|
||||
palette_filter = "palettegen=stats_mode=diff"
|
||||
rate_filter = rate_filter(duration)
|
||||
filter_graph = "#{rate_filter},#{palette_filter}"
|
||||
|
||||
{_output, 0} =
|
||||
System.cmd("ffmpeg", [
|
||||
"-loglevel",
|
||||
"0",
|
||||
"-y",
|
||||
"-i",
|
||||
file,
|
||||
"-vf",
|
||||
filter_graph,
|
||||
palette
|
||||
])
|
||||
|
||||
palette
|
||||
end
|
||||
|
||||
# x264 requires image dimensions to be a multiple of 2
|
||||
# -2 = ~1
|
||||
def box_dimensions({width, height}, {target_width, target_height}) do
|
||||
|
@ -255,8 +216,4 @@ defmodule PhilomenaMedia.Processors.Webm do
|
|||
|
||||
{new_width, new_height}
|
||||
end
|
||||
|
||||
# Avoid division by zero
|
||||
def rate_filter(duration) when duration > 0.5, do: "fps=1/#{duration / 10},settb=1/2,setpts=N"
|
||||
def rate_filter(_duration), do: "setpts=N/TB/2"
|
||||
end
|
||||
|
|
31
lib/philomena_media/req.ex
Normal file
31
lib/philomena_media/req.ex
Normal file
|
@ -0,0 +1,31 @@
|
|||
defmodule PhilomenaMedia.Req do
|
||||
@behaviour ExAws.Request.HttpClient
|
||||
|
||||
@moduledoc """
|
||||
Configuration for `m:Req`.
|
||||
|
||||
Options can be set for `m:Req` with the following config:
|
||||
|
||||
config :philomena, :req_opts,
|
||||
receive_timeout: 30_000
|
||||
|
||||
The default config handles setting the above.
|
||||
"""
|
||||
|
||||
@default_opts [receive_timeout: 30_000]
|
||||
|
||||
@impl true
|
||||
def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do
|
||||
[method: method, url: url, body: body, headers: headers, decode_body: false]
|
||||
|> Keyword.merge(Application.get_env(:philomena, :req_opts, @default_opts))
|
||||
|> Keyword.merge(http_opts)
|
||||
|> Req.request()
|
||||
|> case do
|
||||
{:ok, %{status: status, headers: headers, body: body}} ->
|
||||
{:ok, %{status_code: status, headers: headers, body: body}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, %{reason: reason}}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,9 +17,13 @@ defmodule PhilomenaProxy.Http do
|
|||
|
||||
@type url :: String.t()
|
||||
@type header_list :: [{String.t(), String.t()}]
|
||||
@type body :: binary()
|
||||
@type body :: iodata()
|
||||
@type result :: {:ok, Req.Response.t()} | {:error, Exception.t()}
|
||||
|
||||
@type client_options :: keyword()
|
||||
@user_agent "Mozilla/5.0 (X11; Philomena; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0"
|
||||
@max_body 125_000_000
|
||||
|
||||
@max_body_key :resp_body_size
|
||||
|
||||
@doc ~S"""
|
||||
Perform a HTTP GET request.
|
||||
|
@ -27,15 +31,15 @@ defmodule PhilomenaProxy.Http do
|
|||
## Example
|
||||
|
||||
iex> PhilomenaProxy.Http.get("http://example.com", [{"authorization", "Bearer #{token}"}])
|
||||
{:ok, %Tesla.Env{...}}
|
||||
{:ok, %{status: 200, body: ...}}
|
||||
|
||||
iex> PhilomenaProxy.Http.get("http://nonexistent.example.com")
|
||||
{:error, %Mint.TransportError{reason: :nxdomain}}
|
||||
{:error, %Req.TransportError{reason: :nxdomain}}
|
||||
|
||||
"""
|
||||
@spec get(url(), header_list(), client_options()) :: Tesla.Env.result()
|
||||
def get(url, headers \\ [], options \\ []) do
|
||||
Tesla.get(client(headers), url, opts: [adapter: adapter_opts(options)])
|
||||
@spec get(url(), header_list()) :: result()
|
||||
def get(url, headers \\ []) do
|
||||
request(:get, url, [], headers)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
|
@ -44,15 +48,15 @@ defmodule PhilomenaProxy.Http do
|
|||
## Example
|
||||
|
||||
iex> PhilomenaProxy.Http.head("http://example.com", [{"authorization", "Bearer #{token}"}])
|
||||
{:ok, %Tesla.Env{...}}
|
||||
{:ok, %{status: 200, body: ...}}
|
||||
|
||||
iex> PhilomenaProxy.Http.head("http://nonexistent.example.com")
|
||||
{:error, %Mint.TransportError{reason: :nxdomain}}
|
||||
{:error, %Req.TransportError{reason: :nxdomain}}
|
||||
|
||||
"""
|
||||
@spec head(url(), header_list(), client_options()) :: Tesla.Env.result()
|
||||
def head(url, headers \\ [], options \\ []) do
|
||||
Tesla.head(client(headers), url, opts: [adapter: adapter_opts(options)])
|
||||
@spec head(url(), header_list()) :: result()
|
||||
def head(url, headers \\ []) do
|
||||
request(:head, url, [], headers)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
|
@ -61,27 +65,67 @@ defmodule PhilomenaProxy.Http do
|
|||
## Example
|
||||
|
||||
iex> PhilomenaProxy.Http.post("http://example.com", "", [{"authorization", "Bearer #{token}"}])
|
||||
{:ok, %Tesla.Env{...}}
|
||||
{:ok, %{status: 200, body: ...}}
|
||||
|
||||
iex> PhilomenaProxy.Http.post("http://nonexistent.example.com", "")
|
||||
{:error, %Mint.TransportError{reason: :nxdomain}}
|
||||
{:error, %Req.TransportError{reason: :nxdomain}}
|
||||
|
||||
"""
|
||||
@spec post(url(), body(), header_list(), client_options()) :: Tesla.Env.result()
|
||||
def post(url, body, headers \\ [], options \\ []) do
|
||||
Tesla.post(client(headers), url, body, opts: [adapter: adapter_opts(options)])
|
||||
@spec post(url(), body(), header_list()) :: result()
|
||||
def post(url, body, headers \\ []) do
|
||||
request(:post, url, body, headers)
|
||||
end
|
||||
|
||||
defp adapter_opts(opts) do
|
||||
opts = Keyword.merge(opts, max_body: 125_000_000, inet6: true)
|
||||
@spec request(atom(), String.t(), iodata(), header_list()) :: result()
|
||||
defp request(method, url, body, headers) do
|
||||
Req.new(
|
||||
method: method,
|
||||
url: url,
|
||||
body: body,
|
||||
headers: [{:user_agent, @user_agent} | headers],
|
||||
max_redirects: 1,
|
||||
connect_options: connect_options(url),
|
||||
inet6: true,
|
||||
into: &stream_response_callback/2,
|
||||
decode_body: false
|
||||
)
|
||||
|> Req.Request.put_private(@max_body_key, 0)
|
||||
|> Req.request()
|
||||
end
|
||||
|
||||
case Application.get_env(:philomena, :proxy_host) do
|
||||
nil ->
|
||||
opts
|
||||
defp connect_options(url) do
|
||||
transport_opts =
|
||||
case URI.parse(url) do
|
||||
%{scheme: "https"} ->
|
||||
# SSL defaults validate SHA-1 on root certificates but this is unnecessary because many
|
||||
# many roots are still signed with SHA-1 and it isn't relevant for security. Relax to
|
||||
# allow validation of SHA-1, even though this creates a less secure client.
|
||||
# https://github.com/erlang/otp/issues/8601
|
||||
[
|
||||
transport_opts: [
|
||||
customize_hostname_check: [
|
||||
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
|
||||
],
|
||||
signature_algs_cert: :ssl.signature_algs(:default, :"tlsv1.3") ++ [sha: :rsa]
|
||||
]
|
||||
]
|
||||
|
||||
url ->
|
||||
Keyword.merge(opts, proxy: proxy_opts(URI.parse(url)))
|
||||
end
|
||||
_ ->
|
||||
# Do not pass any options for non-HTTPS schemes. Finch will raise badarg if the above
|
||||
# options are passed.
|
||||
[]
|
||||
end
|
||||
|
||||
proxy_opts =
|
||||
case Application.get_env(:philomena, :proxy_host) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
url ->
|
||||
[proxy: proxy_opts(URI.parse(url))]
|
||||
end
|
||||
|
||||
transport_opts ++ proxy_opts
|
||||
end
|
||||
|
||||
defp proxy_opts(%{host: host, port: port, scheme: "https"}),
|
||||
|
@ -90,18 +134,14 @@ defmodule PhilomenaProxy.Http do
|
|||
defp proxy_opts(%{host: host, port: port, scheme: "http"}),
|
||||
do: {:http, host, port, [transport_opts: [inet6: true]]}
|
||||
|
||||
defp client(headers) do
|
||||
Tesla.client(
|
||||
[
|
||||
{Tesla.Middleware.FollowRedirects, max_redirects: 1},
|
||||
{Tesla.Middleware.Headers,
|
||||
[
|
||||
{"User-Agent",
|
||||
"Mozilla/5.0 (X11; Philomena; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0"}
|
||||
| headers
|
||||
]}
|
||||
],
|
||||
Tesla.Adapter.Mint
|
||||
)
|
||||
defp stream_response_callback({:data, data}, {req, resp}) do
|
||||
req = update_in(req.private[@max_body_key], &(&1 + byte_size(data)))
|
||||
resp = update_in(resp.body, &<<&1::binary, data::binary>>)
|
||||
|
||||
if req.private.resp_body_size < @max_body do
|
||||
{:cont, {req, resp}}
|
||||
else
|
||||
{:halt, {req, RuntimeError.exception("body too big")}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,16 +3,16 @@ defmodule PhilomenaProxy.Scrapers do
|
|||
Scrape utilities to facilitate uploading media from other websites.
|
||||
"""
|
||||
|
||||
# The URL to fetch, as a string.
|
||||
@typedoc "The URL to fetch, as a string."
|
||||
@type url :: String.t()
|
||||
|
||||
# An individual image in a list associated with a scrape result.
|
||||
@typedoc "An individual image in a list associated with a scrape result."
|
||||
@type image_result :: %{
|
||||
url: url(),
|
||||
camo_url: url()
|
||||
}
|
||||
|
||||
# Result of a successful scrape.
|
||||
@typedoc "Result of a successful scrape."
|
||||
@type scrape_result :: %{
|
||||
source_url: url(),
|
||||
description: String.t() | nil,
|
||||
|
@ -56,16 +56,17 @@ defmodule PhilomenaProxy.Scrapers do
|
|||
def scrape!(url) do
|
||||
uri = URI.parse(url)
|
||||
|
||||
@scrapers
|
||||
|> Enum.find(& &1.can_handle?(uri, url))
|
||||
|> wrap()
|
||||
|> Enum.map(& &1.scrape(uri, url))
|
||||
|> unwrap()
|
||||
cond do
|
||||
is_nil(uri.host) ->
|
||||
# Scraping without a hostname doesn't make sense because the proxy cannot fetch it, and
|
||||
# some scrapers may test properties of the hostname.
|
||||
nil
|
||||
|
||||
true ->
|
||||
# Find the first scraper which can handle the URL and process, or return nil
|
||||
Enum.find_value(@scrapers, nil, fn scraper ->
|
||||
scraper.can_handle?(uri, url) && scraper.scrape(uri, url)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp wrap(nil), do: []
|
||||
defp wrap(res), do: [res]
|
||||
|
||||
defp unwrap([result]), do: result
|
||||
defp unwrap(_result), do: nil
|
||||
end
|
||||
|
|
|
@ -9,7 +9,6 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do
|
|||
@image_regex ~r|data-rh="true" rel="preload" href="([^"]*)" as="image"|
|
||||
@source_regex ~r|rel="canonical" href="([^"]*)"|
|
||||
@artist_regex ~r|https://www.deviantart.com/([^/]*)/art|
|
||||
@serial_regex ~r|https://www.deviantart.com/(?:.*?)-(\d+)\z|
|
||||
@cdnint_regex ~r|(https://images-wixmp-[0-9a-f]+.wixmp.com)(?:/intermediary)?/f/([^/]*)/([^/?]*)|
|
||||
@png_regex ~r|(https://[0-9a-z\-\.]+(?:/intermediary)?/f/[0-9a-f\-]+/[0-9a-z\-]+\.png/v1/fill/[0-9a-z_,]+/[0-9a-z_\-]+)(\.png)(.*)|
|
||||
@jpg_regex ~r|(https://[0-9a-z\-\.]+(?:/intermediary)?/f/[0-9a-f\-]+/[0-9a-z\-]+\.jpg/v1/fill/w_[0-9]+,h_[0-9]+,q_)([0-9]+)(,[a-z]+\/[a-z0-6_\-]+\.jpe?g.*)|
|
||||
|
@ -31,14 +30,13 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do
|
|||
@spec scrape(URI.t(), Scrapers.url()) :: Scrapers.scrape_result()
|
||||
def scrape(_uri, url) do
|
||||
url
|
||||
|> follow_redirect(2)
|
||||
|> PhilomenaProxy.Http.get()
|
||||
|> extract_data!()
|
||||
|> try_intermediary_hires!()
|
||||
|> try_new_hires!()
|
||||
|> try_old_hires!()
|
||||
end
|
||||
|
||||
defp extract_data!({:ok, %Tesla.Env{body: body, status: 200}}) do
|
||||
defp extract_data!({:ok, %{body: body, status: 200}}) do
|
||||
[image] = Regex.run(@image_regex, body, capture: :all_but_first)
|
||||
[source] = Regex.run(@source_regex, body, capture: :all_but_first)
|
||||
[artist] = Regex.run(@artist_regex, source, capture: :all_but_first)
|
||||
|
@ -60,7 +58,7 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do
|
|||
with [domain, object_uuid, object_name] <-
|
||||
Regex.run(@cdnint_regex, image.url, capture: :all_but_first),
|
||||
built_url <- "#{domain}/intermediary/f/#{object_uuid}/#{object_name}",
|
||||
{:ok, %Tesla.Env{status: 200}} <- PhilomenaProxy.Http.head(built_url) do
|
||||
{:ok, %{status: 200}} <- PhilomenaProxy.Http.head(built_url) do
|
||||
# This is the high resolution URL.
|
||||
%{
|
||||
data
|
||||
|
@ -107,54 +105,4 @@ defmodule PhilomenaProxy.Scrapers.Deviantart do
|
|||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp try_old_hires!(%{source_url: source, images: [image]} = data) do
|
||||
[serial] = Regex.run(@serial_regex, source, capture: :all_but_first)
|
||||
|
||||
base36 =
|
||||
serial
|
||||
|> String.to_integer()
|
||||
|> Integer.to_string(36)
|
||||
|> String.downcase()
|
||||
|
||||
built_url = "http://orig01.deviantart.net/x_by_x-d#{base36}.png"
|
||||
|
||||
case PhilomenaProxy.Http.get(built_url) do
|
||||
{:ok, %Tesla.Env{status: 301, headers: headers}} ->
|
||||
# Location header provides URL of high res image.
|
||||
{_location, link} = Enum.find(headers, fn {header, _val} -> header == "location" end)
|
||||
|
||||
%{
|
||||
data
|
||||
| images: [
|
||||
%{
|
||||
url: link,
|
||||
camo_url: image.camo_url
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
_ ->
|
||||
# Nothing to be found here, move along...
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
# Workaround for benoitc/hackney#273
|
||||
defp follow_redirect(_url, 0), do: nil
|
||||
|
||||
defp follow_redirect(url, max_times) do
|
||||
case PhilomenaProxy.Http.get(url) do
|
||||
{:ok, %Tesla.Env{headers: headers, status: code}} when code in [301, 302] ->
|
||||
location = Enum.find_value(headers, &location_header/1)
|
||||
follow_redirect(location, max_times - 1)
|
||||
|
||||
response ->
|
||||
response
|
||||
end
|
||||
end
|
||||
|
||||
defp location_header({"Location", location}), do: location
|
||||
defp location_header({"location", location}), do: location
|
||||
defp location_header(_), do: nil
|
||||
end
|
||||
|
|
|
@ -24,7 +24,7 @@ defmodule PhilomenaProxy.Scrapers.Pillowfort do
|
|||
|> process_response!(url)
|
||||
end
|
||||
|
||||
defp json!({:ok, %Tesla.Env{body: body, status: 200}}),
|
||||
defp json!({:ok, %{body: body, status: 200}}),
|
||||
do: Jason.decode!(body)
|
||||
|
||||
defp process_response!(post_json, url) do
|
||||
|
|
|
@ -10,14 +10,10 @@ defmodule PhilomenaProxy.Scrapers.Raw do
|
|||
|
||||
@spec can_handle?(URI.t(), String.t()) :: boolean()
|
||||
def can_handle?(_uri, url) do
|
||||
PhilomenaProxy.Http.head(url)
|
||||
|> case do
|
||||
{:ok, %Tesla.Env{status: 200, headers: headers}} ->
|
||||
headers
|
||||
|> Enum.any?(fn {k, v} ->
|
||||
String.downcase(k) == "content-type" and String.downcase(v) in @mime_types
|
||||
end)
|
||||
|
||||
with {:ok, %{status: 200, headers: headers}} <- PhilomenaProxy.Http.head(url),
|
||||
[type | _] <- headers["content-type"] do
|
||||
String.downcase(type) in @mime_types
|
||||
else
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
|
|
|
@ -37,7 +37,7 @@ defmodule PhilomenaProxy.Scrapers.Tumblr do
|
|||
|> process_response!()
|
||||
end
|
||||
|
||||
defp json!({:ok, %Tesla.Env{body: body, status: 200}}),
|
||||
defp json!({:ok, %{body: body, status: 200}}),
|
||||
do: Jason.decode!(body)
|
||||
|
||||
defp process_response!(%{"response" => %{"posts" => [post | _rest]}}),
|
||||
|
@ -76,7 +76,7 @@ defmodule PhilomenaProxy.Scrapers.Tumblr do
|
|||
end
|
||||
|
||||
defp url_ok?(url) do
|
||||
match?({:ok, %Tesla.Env{status: 200}}, PhilomenaProxy.Http.head(url))
|
||||
match?({:ok, %{status: 200}}, PhilomenaProxy.Http.head(url))
|
||||
end
|
||||
|
||||
defp add_meta(post, images) do
|
||||
|
|
|
@ -18,7 +18,7 @@ defmodule PhilomenaProxy.Scrapers.Twitter do
|
|||
[user, status_id] = Regex.run(@url_regex, url, capture: :all_but_first)
|
||||
|
||||
api_url = "https://api.fxtwitter.com/#{user}/status/#{status_id}"
|
||||
{:ok, %Tesla.Env{status: 200, body: body}} = PhilomenaProxy.Http.get(api_url)
|
||||
{:ok, %{status: 200, body: body}} = PhilomenaProxy.Http.get(api_url)
|
||||
|
||||
json = Jason.decode!(body)
|
||||
tweet = json["tweet"]
|
||||
|
|
|
@ -13,13 +13,31 @@ defmodule PhilomenaQuery.Batch do
|
|||
alias Philomena.Repo
|
||||
import Ecto.Query
|
||||
|
||||
@typedoc """
|
||||
Represents an object which may be operated on via `m:Ecto.Query`.
|
||||
|
||||
This could be a schema object (e.g. `m:Philomena.Images.Image`) or a fully formed query
|
||||
`from i in Image, where: i.hidden_from_users == false`.
|
||||
"""
|
||||
@type queryable :: any()
|
||||
|
||||
@type batch_size :: {:batch_size, integer()}
|
||||
@type id_field :: {:id_field, atom()}
|
||||
@type batch_options :: [batch_size() | id_field()]
|
||||
|
||||
@typedoc """
|
||||
The callback for `record_batches/3`.
|
||||
|
||||
Takes a list of schema structs which were returned in the batch. Return value is ignored.
|
||||
"""
|
||||
@type record_batch_callback :: ([struct()] -> any())
|
||||
|
||||
@typedoc """
|
||||
The callback for `query_batches/3`.
|
||||
|
||||
Takes an `m:Ecto.Query` that can be processed with `m:Philomena.Repo` query commands, such
|
||||
as `Philomena.Repo.update_all/3` or `Philomena.Repo.delete_all/2`. Return value is ignored.
|
||||
"""
|
||||
@type query_batch_callback :: ([Ecto.Query.t()] -> any())
|
||||
|
||||
@doc """
|
||||
|
|
109
lib/philomena_query/ip_mask.ex
Normal file
109
lib/philomena_query/ip_mask.ex
Normal file
|
@ -0,0 +1,109 @@
|
|||
defmodule PhilomenaQuery.IpMask do
|
||||
@moduledoc """
|
||||
Postgres IP masks.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Parse a netmask from a string parameter, producing an `m:Postgrex.INET` type suitable for use in
|
||||
a containment (<<=, <<, >>, >>=) query. Ignores invalid strings and passes the IP through on
|
||||
error. [Postgres documentation](https://www.postgresql.org/docs/current/functions-net.html)
|
||||
has more information on `inet` operations.
|
||||
|
||||
> #### Info {: .info}
|
||||
>
|
||||
> Netmasks lower than /8 are clamped to a minimum of /8. Such low masks are unlikely to be
|
||||
> useful and this avoids producing very expensive masks to evaluate.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "12"})
|
||||
%Postgrex.INET{address: {192, 160, 0, 0}, netmask: 12}
|
||||
|
||||
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "4"})
|
||||
%Postgrex.INET{address: {192, 0, 0, 0}, netmask: 8}
|
||||
|
||||
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "64"})
|
||||
%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}
|
||||
|
||||
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{"mask" => "e"})
|
||||
%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}
|
||||
|
||||
iex> parse_mask(%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}, %{})
|
||||
%Postgrex.INET{address: {192, 168, 1, 1}, netmask: 32}
|
||||
|
||||
iex> parse_mask(%Postgrex.INET{
|
||||
...> address: {0x2001, 0xab0, 0x33a8, 0xd6e2, 0x10e9, 0xac1b, 0x9b0f, 0x67bc},
|
||||
...> netmask: 128
|
||||
...> }, %{"mask" => "64"})
|
||||
%Postgrex.INET{address: {8193, 2736, 13224, 55010, 0, 0, 0, 0}, netmask: 64}
|
||||
|
||||
"""
|
||||
@spec parse_mask(Postgrex.INET.t(), map()) :: Postgrex.INET.t()
|
||||
def parse_mask(ip, params)
|
||||
|
||||
def parse_mask(ip, %{"mask" => mask}) when is_binary(mask) do
|
||||
case Integer.parse(mask) do
|
||||
{mask, _rest} ->
|
||||
mask = clamp_mask(ip.address, mask)
|
||||
address = apply_mask(ip.address, mask)
|
||||
|
||||
%Postgrex.INET{address: address, netmask: mask}
|
||||
|
||||
_ ->
|
||||
ip
|
||||
end
|
||||
end
|
||||
|
||||
def parse_mask(ip, _params), do: ip
|
||||
|
||||
defp clamp(n, min, _max) when n < min, do: min
|
||||
defp clamp(n, _min, max) when n > max, do: max
|
||||
defp clamp(n, _min, _max), do: n
|
||||
|
||||
defp clamp_mask(ip, mask) do
|
||||
# Clamp mask length:
|
||||
# - low end 8 (too taxing to evaluate)
|
||||
# - high end address_bits (limit of address)
|
||||
case tuple_size(ip) do
|
||||
4 ->
|
||||
clamp(mask, 8, 32)
|
||||
|
||||
8 ->
|
||||
clamp(mask, 8, 128)
|
||||
end
|
||||
end
|
||||
|
||||
defp unit_length(ip) when tuple_size(ip) == 4, do: 8
|
||||
defp unit_length(ip) when tuple_size(ip) == 8, do: 16
|
||||
|
||||
defp apply_mask(ip, mask) when is_tuple(ip) do
|
||||
# Determine whether elements are octets or hexadectets
|
||||
length = unit_length(ip)
|
||||
|
||||
# 1. Convert tuple to list of octets/hexadectets
|
||||
# 2. Convert list to bitstring
|
||||
# 3. Perform truncation operation on bitstring
|
||||
# 4. Convert bitstring back to list of octets/hexadectets
|
||||
# 5. Convert list to tuple
|
||||
|
||||
ip
|
||||
|> Tuple.to_list()
|
||||
|> list_to_bits(length)
|
||||
|> apply_mask(mask)
|
||||
|> bits_to_list(length)
|
||||
|> List.to_tuple()
|
||||
end
|
||||
|
||||
defp apply_mask(ip, mask) when is_binary(ip) do
|
||||
# Truncate bit size of ip to mask length and zero-fill the remainder
|
||||
<<ip::bits-size(mask), 0::integer-size(bit_size(ip) - mask)>>
|
||||
end
|
||||
|
||||
defp list_to_bits(list, unit_length) do
|
||||
for u <- list, into: <<>>, do: <<u::integer-size(unit_length)>>
|
||||
end
|
||||
|
||||
defp bits_to_list(bits, unit_length) do
|
||||
for <<u::integer-size(unit_length) <- bits>>, do: u
|
||||
end
|
||||
end
|
|
@ -41,12 +41,34 @@ defmodule PhilomenaQuery.Parse.Parser do
|
|||
TermRangeParser
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
User-supplied context argument.
|
||||
|
||||
Provided to `parse/3` and passed to the transform callback.
|
||||
"""
|
||||
@type context :: any()
|
||||
|
||||
@typedoc "Query in the search engine JSON query language."
|
||||
@type query :: map()
|
||||
|
||||
@typedoc "Whether the default field is `:term` (not analyzed) or `:ngram` (analyzed)."
|
||||
@type default_field_type :: :term | :ngram
|
||||
|
||||
@typedoc """
|
||||
Return value of the transform callback.
|
||||
|
||||
On `{:ok, query}`, the query is incorporated into the parse tree at the current location.
|
||||
On `{:error, error}`, parsing immediately stops and the error is returned from the parser.
|
||||
"""
|
||||
@type transform_result :: {:ok, query()} | {:error, String.t()}
|
||||
|
||||
@typedoc """
|
||||
Type of the transform callback.
|
||||
|
||||
The transform callback receives the context argument passed to `parse/3` and the remainder of
|
||||
the term. For instance `my:example` would match a transform rule with the key `"my"`, and
|
||||
the remainder passed to the callback would be `"example"`.
|
||||
"""
|
||||
@type transform :: (context, String.t() -> transform_result())
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
|
@ -112,11 +134,11 @@ defmodule PhilomenaQuery.Parse.Parser do
|
|||
aliases: %{"hidden" => "hidden_from_users"}
|
||||
]
|
||||
|
||||
Parser.parser(options)
|
||||
Parser.new(options)
|
||||
|
||||
"""
|
||||
@spec parser(keyword()) :: t()
|
||||
def parser(options) do
|
||||
@spec new(keyword()) :: t()
|
||||
def new(options) do
|
||||
parser = struct(Parser, options)
|
||||
|
||||
fields =
|
||||
|
|
|
@ -18,10 +18,36 @@ defmodule PhilomenaQuery.Search do
|
|||
# todo: fetch through compile_env?
|
||||
@policy Philomena.SearchPolicy
|
||||
|
||||
@typedoc """
|
||||
Any schema module which has an associated search index. See the policy module
|
||||
for more information.
|
||||
"""
|
||||
@type schema_module :: @policy.schema_module()
|
||||
|
||||
@typedoc """
|
||||
Represents an object which may be operated on via `m:Ecto.Query`.
|
||||
|
||||
This could be a schema object (e.g. `m:Philomena.Images.Image`) or a fully formed query
|
||||
`from i in Image, where: i.hidden_from_users == false`.
|
||||
"""
|
||||
@type queryable :: any()
|
||||
|
||||
@typedoc """
|
||||
A query body, as deliverable to any index's `_search` endpoint.
|
||||
|
||||
See the query DSL documentation for additional information:
|
||||
https://opensearch.org/docs/latest/query-dsl/
|
||||
"""
|
||||
@type query_body :: map()
|
||||
|
||||
@typedoc """
|
||||
Given a term at the given path, replace the old term with the new term.
|
||||
|
||||
`path` is a list of names to be followed to find the old term. For example,
|
||||
a document containing `{"condiments": "dijon"}` would permit `["condiments"]`
|
||||
as the path, and a document containing `{"namespaced_tags": {"name": ["old"]}}`
|
||||
would permit `["namespaced_tags", "name"]` as the path.
|
||||
"""
|
||||
@type replacement :: %{
|
||||
path: [String.t()],
|
||||
old: term(),
|
||||
|
|
|
@ -1,11 +1,34 @@
|
|||
defmodule PhilomenaQuery.SearchIndex do
|
||||
# Returns the index name for the index.
|
||||
# This is usually a collection name like "images".
|
||||
@moduledoc """
|
||||
Behaviour module for schemas with search indexing.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns the index name for the index.
|
||||
|
||||
This is usually a collection name like "images".
|
||||
|
||||
See https://opensearch.org/docs/latest/api-reference/index-apis/create-index/ for
|
||||
reference on index naming restrictions.
|
||||
"""
|
||||
@callback index_name() :: String.t()
|
||||
|
||||
# Returns the mapping and settings for the index.
|
||||
@doc """
|
||||
Returns the mapping and settings for the index.
|
||||
|
||||
See https://opensearch.org/docs/latest/api-reference/index-apis/put-mapping/ for
|
||||
reference on the mapping syntax, and the following pages for which types may be
|
||||
used in mappings:
|
||||
- https://opensearch.org/docs/latest/field-types/
|
||||
- https://opensearch.org/docs/latest/analyzers/index-analyzers/
|
||||
"""
|
||||
@callback mapping() :: map()
|
||||
|
||||
# Returns the JSON representation of the given struct for indexing in OpenSearch.
|
||||
@doc """
|
||||
Returns the JSON representation of the given struct for indexing in OpenSearch.
|
||||
|
||||
See https://opensearch.org/docs/latest/api-reference/document-apis/index-document/ for
|
||||
reference on how this value is used.
|
||||
"""
|
||||
@callback as_json(struct()) :: map()
|
||||
end
|
||||
|
|
|
@ -7,7 +7,12 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do
|
|||
import Ecto.Query
|
||||
|
||||
plug :verify_authorized
|
||||
plug :load_resource, model: FingerprintBan, only: [:edit, :update, :delete]
|
||||
|
||||
plug :load_resource,
|
||||
model: FingerprintBan,
|
||||
as: :fingerprint_ban,
|
||||
only: [:edit, :update, :delete]
|
||||
|
||||
plug :check_can_delete when action in [:delete]
|
||||
|
||||
def index(conn, %{"q" => q}) when is_binary(q) do
|
||||
|
@ -56,12 +61,12 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do
|
|||
end
|
||||
|
||||
def edit(conn, _params) do
|
||||
changeset = Bans.change_fingerprint(conn.assigns.fingerprint)
|
||||
changeset = Bans.change_fingerprint(conn.assigns.fingerprint_ban)
|
||||
render(conn, "edit.html", title: "Editing Fingerprint Ban", changeset: changeset)
|
||||
end
|
||||
|
||||
def update(conn, %{"fingerprint" => fingerprint_ban_params}) do
|
||||
case Bans.update_fingerprint(conn.assigns.fingerprint, fingerprint_ban_params) do
|
||||
case Bans.update_fingerprint(conn.assigns.fingerprint_ban, fingerprint_ban_params) do
|
||||
{:ok, fingerprint_ban} ->
|
||||
conn
|
||||
|> put_flash(:info, "Fingerprint ban successfully updated.")
|
||||
|
@ -74,7 +79,7 @@ defmodule PhilomenaWeb.Admin.FingerprintBanController do
|
|||
end
|
||||
|
||||
def delete(conn, _params) do
|
||||
{:ok, fingerprint_ban} = Bans.delete_fingerprint(conn.assigns.fingerprint)
|
||||
{:ok, fingerprint_ban} = Bans.delete_fingerprint(conn.assigns.fingerprint_ban)
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Fingerprint ban successfully deleted.")
|
||||
|
|
|
@ -44,7 +44,7 @@ defmodule PhilomenaWeb.Admin.SiteNoticeController do
|
|||
case SiteNotices.update_site_notice(conn.assigns.site_notice, site_notice_params) do
|
||||
{:ok, _site_notice} ->
|
||||
conn
|
||||
|> put_flash(:info, "Succesfully updated site notice.")
|
||||
|> put_flash(:info, "Successfully updated site notice.")
|
||||
|> redirect(to: ~p"/admin/site_notices")
|
||||
|
||||
{:error, changeset} ->
|
||||
|
@ -56,7 +56,7 @@ defmodule PhilomenaWeb.Admin.SiteNoticeController do
|
|||
{:ok, _site_notice} = SiteNotices.delete_site_notice(conn.assigns.site_notice)
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Sucessfully deleted site notice.")
|
||||
|> put_flash(:info, "Successfully deleted site notice.")
|
||||
|> redirect(to: ~p"/admin/site_notices")
|
||||
end
|
||||
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
defmodule PhilomenaWeb.IpProfile.SourceChangeController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias PhilomenaQuery.IpMask
|
||||
alias Philomena.SourceChanges.SourceChange
|
||||
alias Philomena.Repo
|
||||
import Ecto.Query
|
||||
|
||||
plug :verify_authorized
|
||||
|
||||
def index(conn, %{"ip_profile_id" => ip}) do
|
||||
def index(conn, %{"ip_profile_id" => ip} = params) do
|
||||
{:ok, ip} = EctoNetwork.INET.cast(ip)
|
||||
range = IpMask.parse_mask(ip, params)
|
||||
|
||||
source_changes =
|
||||
SourceChange
|
||||
|> where(ip: ^ip)
|
||||
|> where(fragment("? >>= ip", ^range))
|
||||
|> order_by(desc: :id)
|
||||
|> preload([:user, image: [:user, :sources, tags: :aliases]])
|
||||
|> Repo.paginate(conn.assigns.scrivener)
|
||||
|
||||
render(conn, "index.html",
|
||||
title: "Source Changes for IP `#{ip}'",
|
||||
ip: ip,
|
||||
ip: range,
|
||||
source_changes: source_changes
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
defmodule PhilomenaWeb.IpProfile.TagChangeController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias PhilomenaQuery.IpMask
|
||||
alias Philomena.TagChanges.TagChange
|
||||
alias Philomena.Repo
|
||||
import Ecto.Query
|
||||
|
@ -9,10 +10,11 @@ defmodule PhilomenaWeb.IpProfile.TagChangeController do
|
|||
|
||||
def index(conn, %{"ip_profile_id" => ip} = params) do
|
||||
{:ok, ip} = EctoNetwork.INET.cast(ip)
|
||||
range = IpMask.parse_mask(ip, params)
|
||||
|
||||
tag_changes =
|
||||
TagChange
|
||||
|> where(ip: ^ip)
|
||||
|> where(fragment("? >>= ip", ^range))
|
||||
|> added_filter(params)
|
||||
|> preload([:tag, :user, image: [:user, :sources, tags: :aliases]])
|
||||
|> order_by(desc: :id)
|
||||
|
@ -20,7 +22,7 @@ defmodule PhilomenaWeb.IpProfile.TagChangeController do
|
|||
|
||||
render(conn, "index.html",
|
||||
title: "Tag Changes for IP `#{ip}'",
|
||||
ip: ip,
|
||||
ip: range,
|
||||
tag_changes: tag_changes
|
||||
)
|
||||
end
|
||||
|
|
|
@ -212,7 +212,7 @@ defmodule PhilomenaWeb.ProfileController do
|
|||
end
|
||||
|
||||
defp individual_stat(mapping, stat_name) do
|
||||
Enum.map(89..0, &(map_fetch(mapping[&1], stat_name) || 0))
|
||||
Enum.map(89..0//-1, &(map_fetch(mapping[&1], stat_name) || 0))
|
||||
end
|
||||
|
||||
defp map_fetch(nil, _field_name), do: nil
|
||||
|
|
85
lib/philomena_web/fingerprint.ex
Normal file
85
lib/philomena_web/fingerprint.ex
Normal file
|
@ -0,0 +1,85 @@
|
|||
defmodule PhilomenaWeb.Fingerprint do
|
||||
import Plug.Conn
|
||||
|
||||
@type t :: String.t()
|
||||
@name "_ses"
|
||||
|
||||
@doc """
|
||||
Assign the current fingerprint to the conn.
|
||||
"""
|
||||
@spec fetch_fingerprint(Plug.Conn.t(), any()) :: Plug.Conn.t()
|
||||
def fetch_fingerprint(conn, _opts) do
|
||||
conn =
|
||||
conn
|
||||
|> fetch_session()
|
||||
|> fetch_cookies()
|
||||
|
||||
# Try to get the fingerprint from the session, then from the cookie.
|
||||
fingerprint = upgrade(get_session(conn, @name), conn.cookies[@name])
|
||||
|
||||
# If the fingerprint is valid, persist to session.
|
||||
case valid_format?(fingerprint) do
|
||||
true ->
|
||||
conn
|
||||
|> put_session(@name, fingerprint)
|
||||
|> assign(:fingerprint, fingerprint)
|
||||
|
||||
false ->
|
||||
assign(conn, :fingerprint, nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp upgrade(<<"c", _::binary>> = session_value, <<"d", _::binary>> = cookie_value) do
|
||||
if valid_format?(cookie_value) do
|
||||
# When both fingerprint values are valid and the session value
|
||||
# is an old version, use the cookie value.
|
||||
cookie_value
|
||||
else
|
||||
# Use the session value.
|
||||
session_value
|
||||
end
|
||||
end
|
||||
|
||||
defp upgrade(session_value, cookie_value) do
|
||||
# Prefer the session value, using the cookie value if it is unavailable.
|
||||
session_value || cookie_value
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determine whether the fingerprint corresponds to a valid format.
|
||||
|
||||
Valid formats start with `c` or `d` (for the version). The `c` format is a legacy format
|
||||
corresponding to an integer-valued hash from the frontend. The `d` format is the current
|
||||
format corresponding to a hex-valued hash from the frontend. By design, it is not
|
||||
possible to infer anything else about these values from the server.
|
||||
|
||||
See assets/js/fp.ts for additional information on the generation of the `d` format.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> valid_format?("b2502085657")
|
||||
false
|
||||
|
||||
iex> valid_format?("c637334158")
|
||||
true
|
||||
|
||||
iex> valid_format?("d63c4581f8cf58d")
|
||||
true
|
||||
|
||||
iex> valid_format?("5162549b16e8448")
|
||||
false
|
||||
|
||||
"""
|
||||
@spec valid_format?(any()) :: boolean()
|
||||
def valid_format?(fingerprint)
|
||||
|
||||
def valid_format?(<<"c", rest::binary>>) when byte_size(rest) <= 12 do
|
||||
match?({_result, ""}, Integer.parse(rest))
|
||||
end
|
||||
|
||||
def valid_format?(<<"d", rest::binary>>) when byte_size(rest) == 14 do
|
||||
match?({:ok, _result}, Base.decode16(rest, case: :lower))
|
||||
end
|
||||
|
||||
def valid_format?(_fingerprint), do: false
|
||||
end
|
|
@ -36,7 +36,7 @@ defmodule PhilomenaWeb.CompromisedPasswordCheckPlug do
|
|||
|> Base.encode16()
|
||||
|
||||
case PhilomenaProxy.Http.get(make_api_url(prefix)) do
|
||||
{:ok, %Tesla.Env{body: body, status: 200}} -> String.contains?(body, rest)
|
||||
{:ok, %{body: body, status: 200}} -> String.contains?(body, rest)
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,9 +16,7 @@ defmodule PhilomenaWeb.CurrentBanPlug do
|
|||
@doc false
|
||||
@spec call(Conn.t(), any()) :: Conn.t()
|
||||
def call(conn, _opts) do
|
||||
conn = Conn.fetch_cookies(conn)
|
||||
|
||||
fingerprint = conn.cookies["_ses"]
|
||||
fingerprint = conn.assigns.fingerprint
|
||||
user = conn.assigns.current_user
|
||||
ip = conn.remote_ip
|
||||
|
||||
|
|
|
@ -37,9 +37,7 @@ defmodule PhilomenaWeb.FilterBannedUsersPlug do
|
|||
defp maybe_halt_no_fingerprint(%{method: "GET"} = conn), do: conn
|
||||
|
||||
defp maybe_halt_no_fingerprint(conn) do
|
||||
conn = Conn.fetch_cookies(conn)
|
||||
|
||||
case conn.cookies["_ses"] do
|
||||
case conn.assigns.fingerprint do
|
||||
nil ->
|
||||
PhilomenaWeb.NotAuthorizedPlug.call(conn)
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
defmodule PhilomenaWeb.ScraperPlug do
|
||||
@filename_regex ~r/filename="([^"]+)"/
|
||||
|
||||
@spec init(keyword()) :: keyword()
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
|
||||
@spec call(Plug.Conn.t(), keyword()) :: Plug.Conn.t()
|
||||
def call(conn, opts) do
|
||||
params_name = Keyword.get(opts, :params_name, "image")
|
||||
params_key = Keyword.get(opts, :params_key, "image")
|
||||
|
@ -25,18 +27,13 @@ defmodule PhilomenaWeb.ScraperPlug do
|
|||
|
||||
# Writing the tempfile doesn't allow traversal
|
||||
# sobelow_skip ["Traversal.FileModule"]
|
||||
defp maybe_fixup_params(
|
||||
{:ok, %Tesla.Env{body: body, status: 200, headers: headers}},
|
||||
url,
|
||||
opts,
|
||||
conn
|
||||
) do
|
||||
defp maybe_fixup_params({:ok, %{status: 200} = resp}, url, opts, conn) do
|
||||
params_name = Keyword.get(opts, :params_name, "image")
|
||||
params_key = Keyword.get(opts, :params_key, "image")
|
||||
name = extract_filename(url, headers)
|
||||
name = extract_filename(url, resp.headers)
|
||||
file = Plug.Upload.random_file!(UUID.uuid1())
|
||||
|
||||
File.write!(file, body)
|
||||
File.write!(file, resp.body)
|
||||
|
||||
fake_upload = %Plug.Upload{
|
||||
path: file,
|
||||
|
@ -44,22 +41,20 @@ defmodule PhilomenaWeb.ScraperPlug do
|
|||
filename: name
|
||||
}
|
||||
|
||||
updated_form = Map.put(conn.params[params_name], params_key, fake_upload)
|
||||
|
||||
updated_params = Map.put(conn.params, params_name, updated_form)
|
||||
|
||||
%Plug.Conn{conn | params: updated_params}
|
||||
put_in(conn.params[params_name][params_key], fake_upload)
|
||||
end
|
||||
|
||||
defp maybe_fixup_params(_response, _url, _opts, conn), do: conn
|
||||
|
||||
defp extract_filename(url, resp_headers) do
|
||||
{_, header} =
|
||||
Enum.find(resp_headers, {nil, "filename=\"#{Path.basename(url)}\""}, fn {key, value} ->
|
||||
key == "content-disposition" and Regex.match?(@filename_regex, value)
|
||||
end)
|
||||
|
||||
[name] = Regex.run(@filename_regex, header, capture: :all_but_first)
|
||||
defp extract_filename(url, headers) do
|
||||
name =
|
||||
with [value | _] <- headers["content-disposition"],
|
||||
[name] <- Regex.run(@filename_regex, value, capture: :all_but_first) do
|
||||
name
|
||||
else
|
||||
_ ->
|
||||
Path.basename(url)
|
||||
end
|
||||
|
||||
String.slice(name, 0, 127)
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule PhilomenaWeb.Router do
|
|||
use PhilomenaWeb, :router
|
||||
|
||||
import PhilomenaWeb.UserAuth
|
||||
import PhilomenaWeb.Fingerprint
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html"]
|
||||
|
@ -9,6 +10,7 @@ defmodule PhilomenaWeb.Router do
|
|||
plug :fetch_flash
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
plug :fetch_fingerprint
|
||||
plug :fetch_current_user
|
||||
plug PhilomenaWeb.ContentSecurityPolicyPlug
|
||||
plug PhilomenaWeb.CurrentFilterPlug
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
h1 Editing ban
|
||||
|
||||
= render PhilomenaWeb.Admin.FingerprintBanView, "_form.html", changeset: @changeset, action: ~p"/admin/fingerprint_bans/#{@fingerprint}", conn: @conn
|
||||
= render PhilomenaWeb.Admin.FingerprintBanView, "_form.html", changeset: @changeset, action: ~p"/admin/fingerprint_bans/#{@fingerprint_ban}", conn: @conn
|
||||
|
||||
br
|
||||
= link "Back", to: ~p"/admin/fingerprint_bans"
|
||||
|
|
|
@ -59,5 +59,5 @@ h1 = @conversation.title
|
|||
p You've managed to send over 1,000 messages in this conversation!
|
||||
p We'd like to ask you to make a new conversation. Don't worry, this one won't go anywhere if you need to refer back to it.
|
||||
p
|
||||
=> link "Click here", to: ~p"/conversations/new?#{[receipient: other.name]}"
|
||||
=> link "Click here", to: ~p"/conversations/new?#{[recipient: other.name]}"
|
||||
' to make a new conversation with this user.
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
|
||||
.fieldlabel
|
||||
strong You probably do not want to check this unless you know what you are doing - it cannot be changed later
|
||||
| . Pulic filters can be shared with other users and used by them; if you make changes to a filter, it will update all users of that filter.
|
||||
| . Public filters can be shared with other users and used by them; if you make changes to a filter, it will update all users of that filter.
|
||||
|
||||
- input_value(f, :public) == true ->
|
||||
.fieldlabel
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
p
|
||||
'The page(s) you found this image on. Images may have a maximum of
|
||||
span.js-max-source-count> 10
|
||||
span.js-max-source-count> 15
|
||||
' source URLs. Leave any sources you don't want to use blank.
|
||||
|
||||
= inputs_for f, :sources, [as: "image[old_sources]", skip_hidden: true], fn fs ->
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
h4 About this image
|
||||
p
|
||||
'The page(s) you found this image on. Images may have a maximum of
|
||||
span.js-max-source-count> 10
|
||||
span.js-max-source-count> 15
|
||||
' source URLs. Leave any sources you don't want to use blank.
|
||||
|
||||
= inputs_for f, :sources, fn fs ->
|
||||
|
|
|
@ -11,8 +11,17 @@ ul
|
|||
|
||||
h2 Administration Options
|
||||
ul
|
||||
li = link "View tag changes", to: ~p"/ip_profiles/#{to_string(@ip)}/tag_changes"
|
||||
li = link "View source URL history", to: ~p"/ip_profiles/#{to_string(@ip)}/source_changes"
|
||||
li
|
||||
=> link "View tag changes", to: ~p"/ip_profiles/#{to_string(@ip)}/tag_changes"
|
||||
= if ipv6?(@ip) do
|
||||
' …
|
||||
= link "(/64)", to: ~p"/ip_profiles/#{to_string(@ip)}/tag_changes?mask=64"
|
||||
li
|
||||
=> link "View source URL history", to: ~p"/ip_profiles/#{to_string(@ip)}/source_changes"
|
||||
= if ipv6?(@ip) do
|
||||
' …
|
||||
= link "(/64)", to: ~p"/ip_profiles/#{to_string(@ip)}/source_changes?mask=64"
|
||||
|
||||
li = link "View reports this IP has made", to: ~p"/admin/reports?#{[rq: "ip:#{@ip}"]}"
|
||||
li = link "View IP ban history", to: ~p"/admin/subnet_bans?#{[ip: to_string(@ip)]}"
|
||||
li = link "Ban this sucker", to: ~p"/admin/subnet_bans/new?#{[specification: to_string(@ip)]}"
|
||||
|
|
|
@ -11,6 +11,7 @@ html lang="en"
|
|||
=< site_name()
|
||||
- else
|
||||
=<> site_name()
|
||||
link rel="preconnect" href="https://#{cdn_host()}"
|
||||
link rel="stylesheet" href=~p"/css/application.css"
|
||||
link rel="stylesheet" href=stylesheet_path(@conn, @current_user)
|
||||
= if is_nil(@current_user) do
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- form = assigns[:f]
|
||||
- action_text = assigns[:action_text] || 'Edit'
|
||||
- action_icon = assigns[:action_icon] || 'edit'
|
||||
- action_text = assigns[:action_text] || "Edit"
|
||||
- action_icon = assigns[:action_icon] || "edit"
|
||||
- field_name = assigns[:name] || :body
|
||||
- field_placeholder = assigns[:placeholder] || "Your message"
|
||||
- is_required = assigns[:required]
|
||||
|
@ -11,7 +11,7 @@
|
|||
= action_text
|
||||
|
||||
a.button href="#" data-click-tab="preview"
|
||||
i.fa.fa-cog.fa-spin.js-preview-loading.hidden> title=raw('Loading preview…')
|
||||
i.fa.fa-cog.fa-spin.js-preview-loading.hidden> title=raw("Loading preview…")
|
||||
i.fa.fa-eye.js-preview-idle>
|
||||
| Preview
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
.field
|
||||
=> label f, :categories, "Art Categories:"
|
||||
br
|
||||
= collection_checkboxes f, :categories, categories(), selected: f.data.categories, input_opts: [ class: 'checkbox spacing-right' ], wrapper: &Phoenix.HTML.Tag.content_tag(:span, &1, class: "commission__category")
|
||||
= collection_checkboxes f, :categories, categories(), selected: f.data.categories, input_opts: [ class: "checkbox spacing-right" ], wrapper: &Phoenix.HTML.Tag.content_tag(:span, &1, class: "commission__category")
|
||||
= error_tag f, :categories
|
||||
.field
|
||||
=> label f, :sheet_image_id, "Image ID of your commissions sheet (optional but recommended):"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
span.tag.dropdown data-tag-category="#{@tag.category}" data-tag-id="#{@tag.id}" data-tag-name="#{@tag.name}" data-tag-slug="#{@tag.slug}"
|
||||
/ The order of tag states and dropdown links is important for tags.js
|
||||
/ The order of tag states and dropdown links is important for tags.ts
|
||||
span
|
||||
span.tag__state.hidden title="Unwatched"
|
||||
| +
|
||||
|
|
|
@ -29,10 +29,10 @@ p.fieldlabel
|
|||
= select @f, :vote_method, ["-": "", "Single option": :single, "Multiple options": :multiple], class: "input"
|
||||
= error_tag @f, :vote_method
|
||||
|
||||
= inputs_for @f, :options, fn fo ->
|
||||
= inputs_for @f, :options, fn opt ->
|
||||
.field.js-poll-option.field--inline.flex--no-wrap.flex--centered
|
||||
= text_input fo, :label, class: "input flex__grow js-option-label", placeholder: "Option"
|
||||
= error_tag fo, :label
|
||||
= text_input opt, :label, class: "input flex__grow js-option-label", placeholder: "Option"
|
||||
= error_tag opt, :label
|
||||
|
||||
label.input--separate-left.flex__fixed.flex--centered
|
||||
a.js-option-remove href="#"
|
||||
|
|
|
@ -211,9 +211,8 @@ defmodule PhilomenaWeb.UserAuth do
|
|||
|
||||
defp update_usages(conn, user) do
|
||||
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
conn = fetch_cookies(conn)
|
||||
|
||||
UserIpUpdater.cast(user.id, conn.remote_ip, now)
|
||||
UserFingerprintUpdater.cast(user.id, conn.cookies["_ses"], now)
|
||||
UserFingerprintUpdater.cast(user.id, conn.assigns.fingerprint, now)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@ defmodule PhilomenaWeb.UserFingerprintUpdater do
|
|||
alias Philomena.Repo
|
||||
import Ecto.Query
|
||||
|
||||
alias PhilomenaWeb.Fingerprint
|
||||
|
||||
def child_spec([]) do
|
||||
%{
|
||||
id: PhilomenaWeb.UserFingerprintUpdater,
|
||||
|
@ -14,14 +16,13 @@ defmodule PhilomenaWeb.UserFingerprintUpdater do
|
|||
{:ok, spawn_link(&init/0)}
|
||||
end
|
||||
|
||||
def cast(user_id, <<"c", _rest::binary>> = fingerprint, updated_at)
|
||||
when byte_size(fingerprint) <= 12 do
|
||||
pid = Process.whereis(:fingerprint_updater)
|
||||
if pid, do: send(pid, {user_id, fingerprint, updated_at})
|
||||
def cast(user_id, fingerprint, updated_at) do
|
||||
if Fingerprint.valid_format?(fingerprint) do
|
||||
pid = Process.whereis(:fingerprint_updater)
|
||||
if pid, do: send(pid, {user_id, fingerprint, updated_at})
|
||||
end
|
||||
end
|
||||
|
||||
def cast(_user_id, _fingerprint, _updated_at), do: nil
|
||||
|
||||
defp init do
|
||||
Process.register(self(), :fingerprint_updater)
|
||||
run()
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
defmodule PhilomenaWeb.IpProfileView do
|
||||
use PhilomenaWeb, :view
|
||||
|
||||
@spec ipv6?(Postgrex.INET.t()) :: boolean()
|
||||
def ipv6?(ip) do
|
||||
tuple_size(ip.address) == 8
|
||||
end
|
||||
end
|
||||
|
|
22
mix.exs
22
mix.exs
|
@ -11,7 +11,8 @@ defmodule Philomena.MixProject do
|
|||
start_permanent: Mix.env() == :prod,
|
||||
aliases: aliases(),
|
||||
deps: deps(),
|
||||
dialyzer: [plt_add_apps: [:mix]]
|
||||
dialyzer: [plt_add_apps: [:mix]],
|
||||
docs: [formatters: ["html"]]
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -34,7 +35,7 @@ defmodule Philomena.MixProject do
|
|||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.6"},
|
||||
{:phoenix, "~> 1.7"},
|
||||
{:phoenix_pubsub, "~> 2.1"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:ecto_sql, "~> 3.9"},
|
||||
|
@ -44,10 +45,8 @@ defmodule Philomena.MixProject do
|
|||
{:phoenix_live_reload, "~> 1.4", only: :dev},
|
||||
{:gettext, "~> 0.22"},
|
||||
{:jason, "~> 1.4"},
|
||||
{:ranch, "~> 2.1", override: true},
|
||||
{:plug_cowboy, "~> 2.6"},
|
||||
{:slime, "~> 1.3.0",
|
||||
github: "liamwhite/slime", ref: "4c8ad4e9e9dcc792f4db769a9ef2ad7d6eba8f31", override: true},
|
||||
{:bandit, "~> 1.2"},
|
||||
{:slime, "~> 1.3.1"},
|
||||
{:phoenix_slime, "~> 0.13",
|
||||
github: "slime-lang/phoenix_slime", ref: "8944de91654d6fcf6bdcc0aed6b8647fe3398241"},
|
||||
{:phoenix_pubsub_redis, "~> 3.0"},
|
||||
|
@ -64,9 +63,7 @@ defmodule Philomena.MixProject do
|
|||
{:redix, "~> 1.2"},
|
||||
{:remote_ip, "~> 1.1"},
|
||||
{:briefly, "~> 0.4"},
|
||||
{:tesla, "~> 1.5"},
|
||||
{:castore, "~> 1.0", override: true},
|
||||
{:mint, "~> 1.4"},
|
||||
{:req, "~> 0.5"},
|
||||
{:exq, "~> 0.17"},
|
||||
{:ex_aws, "~> 2.0",
|
||||
github: "liamwhite/ex_aws", ref: "a340859dd8ac4d63bd7a3948f0994e493e49bda4", override: true},
|
||||
|
@ -83,9 +80,10 @@ defmodule Philomena.MixProject do
|
|||
{:rustler, "~> 0.27"},
|
||||
|
||||
# Linting
|
||||
{:credo, "~> 1.6", only: [:dev, :test], override: true},
|
||||
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
|
||||
{:credo_envvar, "~> 0.1", only: [:dev, :test], runtime: false},
|
||||
{:credo_naming, "~> 2.0", only: [:dev, :test], runtime: false},
|
||||
{:ex_doc, "~> 0.30", only: [:dev], runtime: false},
|
||||
|
||||
# Security checks
|
||||
{:sobelow, "~> 0.11", only: [:dev, :test], runtime: true},
|
||||
|
@ -94,10 +92,6 @@ defmodule Philomena.MixProject do
|
|||
# Static analysis
|
||||
{:dialyxir, "~> 1.2", only: :dev, runtime: false},
|
||||
|
||||
# Fixes for OTP/25
|
||||
{:neotoma, "~> 1.7.3", manager: :rebar3, override: true},
|
||||
{:hut, "~> 1.4.0", manager: :rebar3, override: true},
|
||||
|
||||
# Fixes for Elixir v1.15+
|
||||
{:canary, "~> 1.1",
|
||||
github: "marcinkoziej/canary", ref: "704debde7a2c0600f78c687807884bf37c45bd79"}
|
||||
|
|
66
mix.lock
66
mix.lock
|
@ -1,90 +1,92 @@
|
|||
%{
|
||||
"bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
||||
"briefly": {:hex, :briefly, "0.5.1", "ee10d48da7f79ed2aebdc3e536d5f9a0c3e36ff76c0ad0d4254653a152b13a8a", [:mix], [], "hexpm", "bd684aa92ad8b7b4e0d92c31200993c4bc1469fc68cd6d5f15144041bd15cb57"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"canada": {:hex, :canada, "1.0.2", "040e4c47609b0a67d5773ac1fbe5e99f840cef173d69b739beda7c98453e0770", [:mix], [], "hexpm", "4269f74153fe89583fe50bd4d5de57bfe01f31258a6b676d296f3681f1483c68"},
|
||||
"canary": {:git, "https://github.com/marcinkoziej/canary.git", "704debde7a2c0600f78c687807884bf37c45bd79", [ref: "704debde7a2c0600f78c687807884bf37c45bd79"]},
|
||||
"castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
|
||||
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
|
||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
|
||||
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
||||
"credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
|
||||
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
||||
"credo_envvar": {:hex, :credo_envvar, "0.1.4", "40817c10334e400f031012c0510bfa0d8725c19d867e4ae39cf14f2cbebc3b20", [:mix], [{:credo, "~> 1.0", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "5055cdb4bcbaf7d423bc2bb3ac62b4e2d825e2b1e816884c468dee59d0363009"},
|
||||
"credo_naming": {:hex, :credo_naming, "2.1.0", "d44ad58890d4db552e141ce64756a74ac1573665af766d1ac64931aa90d47744", [:make, :mix], [{:credo, "~> 1.6", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "830e23b3fba972e2fccec49c0c089fe78c1e64bc16782a2682d78082351a2909"},
|
||||
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
|
||||
"ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
|
||||
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
|
||||
"ecto_network": {:hex, :ecto_network, "1.5.0", "a930c910975e7a91237b858ebf0f4ad7b2aae32fa846275aa203cb858459ec73", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "4d614434ae3e6d373a2f693d56aafaa3f3349714668ffd6d24e760caf578aa2f"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.11.2", "c7cc7f812af571e50b80294dc2e535821b3b795ce8008d07aa5f336591a185a8", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "73c07f995ac17dbf89d3cfaaf688fcefabcd18b7b004ac63b0dc4ef39499ed6b"},
|
||||
"elastix": {:hex, :elastix, "0.10.0", "7567da885677ba9deffc20063db5f3ca8cd10f23cff1ab3ed9c52b7063b7e340", [:mix], [{:httpoison, "~> 1.4", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}, {:retry, "~> 0.8", [hex: :retry, repo: "hexpm", optional: false]}], "hexpm", "5fb342ce068b20f7845f5dd198c2dc80d967deafaa940a6e51b846db82696d1d"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
|
||||
"elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"},
|
||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||
"ex_aws": {:git, "https://github.com/liamwhite/ex_aws.git", "a340859dd8ac4d63bd7a3948f0994e493e49bda4", [ref: "a340859dd8ac4d63bd7a3948f0994e493e49bda4"]},
|
||||
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"},
|
||||
"expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
|
||||
"exq": {:hex, :exq, "0.19.0", "06eb92944dad39f0954dc8f63190d3e24d11734eef88cf5800883e57ebf74f3c", [:mix], [{:elixir_uuid, ">= 1.2.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0 and < 6.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:redix, ">= 0.9.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "24fc0ebdd87cc7406e1034fb46c2419f9c8a362f0ec634d23b6b819514d36390"},
|
||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
|
||||
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
|
||||
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
|
||||
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"},
|
||||
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
||||
"hut": {:hex, :hut, "1.4.0", "7a1238ec00f95c9ec75412587ee11ac652eca308a7f4b8cc9629746d579d6cf0", [:"erlang.mk", :rebar3], [], "hexpm", "7af8704b9bae98a336f70d9560fc3c97f15665265fa603dbd05352e63d6ebb03"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
|
||||
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.2", "6cd5c5e2edbc9298629c85347b39fb3210656e541153826efd0b2a63767f3395", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "68d2f06f96b9c445a23434c9d5f09682866a5b4e90f631829db1c64f140e795b"},
|
||||
"mua": {:hex, :mua, "0.2.1", "7f1c20dbe7266d514a07bf5b7a3946413d70150be41cb5475b5a95bb517a378f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "0bc556803a1d09dfb69bfebecb838cf33a2d123de84f700c41b6b8134027c11f"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
|
||||
"mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"},
|
||||
"mua": {:hex, :mua, "0.2.2", "d2997abc1eee43d91e4a355665658743ad2609b8d5992425940ce17b7ff87933", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "cda7e38c65d3105b3017b25ac402b4c9457892abeb2e11c331b25a92d16b04c0"},
|
||||
"neotoma": {:hex, :neotoma, "1.7.3", "d8bd5404b73273989946e4f4f6d529e5c2088f5fa1ca790b4dbe81f4be408e61", [:rebar], [], "hexpm", "2da322b9b1567ffa0706a7f30f6bbbde70835ae44a1050615f4b4a3d436e0f28"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"pbkdf2": {:git, "https://github.com/basho/erlang-pbkdf2.git", "7e9bd5fcd3cc3062159e4c9214bb628aa6feb5ca", [ref: "7e9bd5fcd3cc3062159e4c9214bb628aa6feb5ca"]},
|
||||
"phoenix": {:hex, :phoenix, "1.7.11", "1d88fc6b05ab0c735b250932c4e6e33bfa1c186f76dcf623d8dd52f07d6379c7", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "b1ec57f2e40316b306708fe59b92a16b9f6f4bf50ccfa41aa8c7feb79e0ec02a"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.5.0", "1a1f841ccda19b15f1d82968840a5b895c5f687b6734e430e4b2dbe035ca1837", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "13990570fde09e16959ef214501fe2813e1192d62ca753ec8798980580436f94"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.1", "6ab463cf43938ee9906067b33c8d66782343de4280a70084cd5617accc6345a8", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "e8467d308b61f294f68afe12c81bf585584c7ceed40ec8adde88ec176d480a78"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_pubsub_redis": {:hex, :phoenix_pubsub_redis, "3.0.1", "d4d856b1e57a21358e448543e1d091e07e83403dde4383b8be04ed9d2c201cbc", [:mix], [{:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1 or ~> 1.6", [hex: :poolboy, repo: "hexpm", optional: false]}, {:redix, "~> 0.10.0 or ~> 1.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "0b36a17ff6e9a56159f8df8933d62b5c1f0695eae995a02e0c86c035ace6a309"},
|
||||
"phoenix_slime": {:git, "https://github.com/slime-lang/phoenix_slime.git", "8944de91654d6fcf6bdcc0aed6b8647fe3398241", [ref: "8944de91654d6fcf6bdcc0aed6b8647fe3398241"]},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"},
|
||||
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
|
||||
"plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
|
||||
"postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"},
|
||||
"pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
|
||||
"qrcode": {:hex, :qrcode, "0.1.5", "551271830515c150f34568345b060c625deb0e6691db2a01b0a6de3aafc93886", [:mix], [], "hexpm", "a266b7fb7be0d3b713912055dde3575927eca920e5d604ded45cd534f6b7a447"},
|
||||
"ranch": {:hex, :ranch, "2.1.0", "2261f9ed9574dcfcc444106b9f6da155e6e540b2f82ba3d42b339b93673b72a3", [:make, :rebar3], [], "hexpm", "244ee3fa2a6175270d8e1fc59024fd9dbc76294a321057de8f803b1479e76916"},
|
||||
"redix": {:hex, :redix, "1.3.0", "f4121163ff9d73bf72157539ff23b13e38422284520bb58c05e014b19d6f0577", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "60d483d320c77329c8cbd3df73007e51b23f3fae75b7693bc31120d83ab26131"},
|
||||
"remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
|
||||
"redix": {:hex, :redix, "1.5.1", "a2386971e69bf23630fb3a215a831b5478d2ee7dc9ea7ac811ed89186ab5d7b7", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "85224eb2b683c516b80d472eb89b76067d5866913bf0be59d646f550de71f5c4"},
|
||||
"remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"},
|
||||
"req": {:hex, :req, "0.5.0", "6d8a77c25cfc03e06a439fb12ffb51beade53e3fe0e2c5e362899a18b50298b3", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dda04878c1396eebbfdec6db6f3d4ca609e5c8846b7ee88cc56eb9891406f7a3"},
|
||||
"retry": {:hex, :retry, "0.18.0", "dc58ebe22c95aa00bc2459f9e0c5400e6005541cf8539925af0aa027dc860543", [:mix], [], "hexpm", "9483959cc7bf69c9e576d9dfb2b678b71c045d3e6f39ab7c9aa1489df4492d73"},
|
||||
"rustler": {:hex, :rustler, "0.31.0", "7e5eefe61e6e6f8901e5aa3de60073d360c6320d9ec363027b0197297b80c46a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "99e378459bfb9c3bda6d3548b2b3bc6f9ad97f728f76bdbae7bf5c770a4f8abd"},
|
||||
"rustler": {:hex, :rustler, "0.33.0", "4a5b0a7a7b0b51549bea49947beff6fae9bc5d5326104dcd4531261e876b5619", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "7c4752728fee59a815ffd20c3429c55b644041f25129b29cdeb5c470b80ec5fd"},
|
||||
"scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
|
||||
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
|
||||
"secure_compare": {:hex, :secure_compare, "0.1.0", "01b3c93c8edb696e8a5b38397ed48e10958c8a5ec740606656445bcbec0aadb8", [:mix], [], "hexpm", "6391a49eb4a6182f0d7425842fc774bbed715e78b2bfb0c83b99c94e02c78b5c"},
|
||||
"slime": {:git, "https://github.com/liamwhite/slime.git", "4c8ad4e9e9dcc792f4db769a9ef2ad7d6eba8f31", [ref: "4c8ad4e9e9dcc792f4db769a9ef2ad7d6eba8f31"]},
|
||||
"slime": {:hex, :slime, "1.3.1", "d6781854092a638e451427c33e67be348352651a7917a128155b8a41ac88d0a2", [:mix], [{:neotoma, "~> 1.7", [hex: :neotoma, repo: "hexpm", optional: false]}], "hexpm", "099b09280297e0c6c8d1f56b0033b885fc4eb541ad3c4a75f88a589354e2501b"},
|
||||
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
|
||||
"swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
|
||||
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
|
||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ defmodule PhilomenaWeb.UserAuthTest do
|
|||
conn =
|
||||
conn
|
||||
|> Map.replace!(:secret_key_base, PhilomenaWeb.Endpoint.config(:secret_key_base))
|
||||
|> assign(:fingerprint, "d015c342859dde3")
|
||||
|> init_test_session(%{})
|
||||
|
||||
%{user: user_fixture(), conn: conn}
|
||||
|
|
|
@ -41,9 +41,11 @@ defmodule PhilomenaWeb.ConnCase do
|
|||
|> Philomena.Filters.change_filter()
|
||||
|> Philomena.Repo.insert!()
|
||||
|
||||
fingerprint = to_string(:io_lib.format(~c"d~14.16.0b", [:rand.uniform(2 ** 53)]))
|
||||
|
||||
conn =
|
||||
Phoenix.ConnTest.build_conn()
|
||||
|> Phoenix.ConnTest.put_req_cookie("_ses", Integer.to_string(System.unique_integer()))
|
||||
|> Phoenix.ConnTest.put_req_cookie("_ses", fingerprint)
|
||||
|
||||
{:ok, conn: conn}
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue