diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4f3922bb3..dd9b214ec 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,8 @@ ### Before you begin - I understand my contributions may be rejected for any reason -- I understand my contributions are for the benefit of Derpibooru and/or the Philomena software +- I understand my contributions are for the benefit of the Philomena software +- I understand my contributions may be used by any and all site operators using the Philomena software - I understand my contributions are licensed under the GNU AGPLv3 * [ ] I understand all of the above diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..56cc23faa --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# Prettier doesn't know about the ||syntax|| and produces garbage output. +priv/repo/seeds/data/pages/markdown.md diff --git a/assets/css/options/colored-logo.css b/assets/css/options/colored-logo.css new file mode 100644 index 000000000..a5bd4dc30 --- /dev/null +++ b/assets/css/options/colored-logo.css @@ -0,0 +1,11 @@ +i.favicon-home { + position: relative; + top: 2px; + background-image: unset !important; + background-color: var(--logo-color); + -webkit-mask-image: url("/favicon.svg"); + mask-image: url("/favicon.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; +} diff --git a/assets/css/themes/dark-blue.css b/assets/css/themes/dark-blue.css index 89781e6c3..a30bd9d3a 100644 --- a/assets/css/themes/dark-blue.css +++ b/assets/css/themes/dark-blue.css @@ -77,5 +77,5 @@ $autocomplete-history-color: #79afe4; */ :root { - --keep: #111111; + --logo-color: #73d6ed; } diff --git a/assets/css/themes/dark-gray.css b/assets/css/themes/dark-gray.css index 0e6077058..70b1cad59 100644 --- a/assets/css/themes/dark-gray.css +++ b/assets/css/themes/dark-gray.css @@ -76,5 +76,5 @@ $autocomplete-history-color: #9baab5; */ :root { - --keep: #888888; + --logo-color: #9194a5; } diff --git a/assets/css/themes/dark-green.css b/assets/css/themes/dark-green.css index a5bc8e21e..191c22f85 100644 --- a/assets/css/themes/dark-green.css +++ b/assets/css/themes/dark-green.css @@ -76,5 +76,5 @@ $autocomplete-history-color: #11cf69; */ :root { - --keep: #333333; + --logo-color: #0fc84d; } diff --git a/assets/css/themes/dark-orange.css b/assets/css/themes/dark-orange.css index df56d04d9..62f99d964 100644 --- a/assets/css/themes/dark-orange.css +++ b/assets/css/themes/dark-orange.css @@ -77,4 +77,5 @@ $autocomplete-history-color: #eb6d2e; :root { --comment-color: #b099dd; + --logo-color: #fb6115; } diff --git a/assets/css/themes/dark-pink.css b/assets/css/themes/dark-pink.css index 41c72dd4a..e59c411f2 100644 --- a/assets/css/themes/dark-pink.css +++ b/assets/css/themes/dark-pink.css @@ -76,5 +76,5 @@ $autocomplete-history-color: #f0509b; */ :root { - --keep: #555555; + --logo-color: #e357c5; } diff --git a/assets/css/themes/dark-purple.css b/assets/css/themes/dark-purple.css index f667c0688..9e3ef21ad 100644 --- a/assets/css/themes/dark-purple.css +++ b/assets/css/themes/dark-purple.css @@ -77,5 +77,5 @@ $autocomplete-history-color: #b577eb; */ :root { - --keep: #666666; + --logo-color: #9a91d9; } diff --git a/assets/css/themes/dark-red.css b/assets/css/themes/dark-red.css index 02ab9b9ff..8f6a347f1 100644 --- a/assets/css/themes/dark-red.css +++ b/assets/css/themes/dark-red.css @@ -78,5 +78,5 @@ $autocomplete-history-color: #d2d207; */ :root { - --keep: #777777; + --logo-color: #ff8080; } diff --git a/assets/css/themes/dark-teal.css b/assets/css/themes/dark-teal.css index e9745460c..60fc362b7 100644 --- a/assets/css/themes/dark-teal.css +++ b/assets/css/themes/dark-teal.css @@ -76,5 +76,5 @@ $autocomplete-history-color: #0ebcbf; */ :root { - --keep: #222222; + --logo-color: #49d1ca; } diff --git a/assets/css/themes/dark-yellow.css b/assets/css/themes/dark-yellow.css index 5df329161..7144f6194 100644 --- a/assets/css/themes/dark-yellow.css +++ b/assets/css/themes/dark-yellow.css @@ -76,5 +76,5 @@ $autocomplete-history-color: #e9c600; */ :root { - --keep: #999999; + --logo-color: #fbc115; } diff --git a/assets/css/themes/light-blue.css b/assets/css/themes/light-blue.css index 17eeb4826..0785a33e6 100644 --- a/assets/css/themes/light-blue.css +++ b/assets/css/themes/light-blue.css @@ -60,5 +60,5 @@ $autocomplete-history-color: #419cd9; */ :root { - --keep: #000000; + --logo-color: #73d6ed; } diff --git a/assets/css/themes/light-gray.css b/assets/css/themes/light-gray.css index 1590ea798..2fed4f40d 100644 --- a/assets/css/themes/light-gray.css +++ b/assets/css/themes/light-gray.css @@ -69,4 +69,5 @@ $autocomplete-history-color: #3e7da1; --media-box-color: hsl(from $base-color h calc(s - 26) calc(l + 20)); --media-box-hover-color: hsl(from $base-color h calc(s - 20) calc(l + 14)); --background-odd-color: #eaeaea; + --logo-color: #d4d7e9; } diff --git a/assets/css/themes/light-green.css b/assets/css/themes/light-green.css index d2890ac0c..ca2162bcb 100644 --- a/assets/css/themes/light-green.css +++ b/assets/css/themes/light-green.css @@ -60,4 +60,5 @@ $autocomplete-history-color: #2ca052; :root { --comment-color: #9273d0; --background-odd-color: #e6f3ea; + --logo-color: #0ef059; } diff --git a/assets/css/themes/light-orange.css b/assets/css/themes/light-orange.css index 50a241c51..a67afb542 100644 --- a/assets/css/themes/light-orange.css +++ b/assets/css/themes/light-orange.css @@ -58,4 +58,5 @@ $autocomplete-history-color: #d97441; :root { --comment-color: #9273d0; + --logo-color: #ffad98; } diff --git a/assets/css/themes/light-pink.css b/assets/css/themes/light-pink.css index fff46994d..91c60bfd0 100644 --- a/assets/css/themes/light-pink.css +++ b/assets/css/themes/light-pink.css @@ -57,5 +57,5 @@ $autocomplete-history-color: #d941c0; */ :root { - --keep: #dddddd; + --logo-color: #f6b0c7; } diff --git a/assets/css/themes/light-purple.css b/assets/css/themes/light-purple.css index 52d86ef0f..b02d14658 100644 --- a/assets/css/themes/light-purple.css +++ b/assets/css/themes/light-purple.css @@ -57,5 +57,5 @@ $autocomplete-history-color: #9241d9; */ :root { - --keep: #eeeeee; + --logo-color: #b6a0f3; } diff --git a/assets/css/themes/light-red.css b/assets/css/themes/light-red.css index d0e31e46c..b4906cda0 100644 --- a/assets/css/themes/light-red.css +++ b/assets/css/themes/light-red.css @@ -59,4 +59,5 @@ $autocomplete-history-color: #d94141; :root { --header-admin-color: hsl(from $foreground-color h s calc(l + 60)); --header-admin-hover-color: hsl(from $foreground-color h s calc(l + 50)); + --logo-color: #ff8080; } diff --git a/assets/css/themes/light-teal.css b/assets/css/themes/light-teal.css index b93df9c74..9fac3140c 100644 --- a/assets/css/themes/light-teal.css +++ b/assets/css/themes/light-teal.css @@ -58,4 +58,5 @@ $autocomplete-history-color: #1098a2; :root { --background-odd-color: #e2f1f1; + --logo-color: #a0f3ef; } diff --git a/assets/css/themes/light-yellow.css b/assets/css/themes/light-yellow.css index 38c904fb7..2c6448a72 100644 --- a/assets/css/themes/light-yellow.css +++ b/assets/css/themes/light-yellow.css @@ -59,4 +59,5 @@ $autocomplete-history-color: #8e7e06; :root { --comment-color: #9273d0; --background-odd-color: #f5efde; + --logo-color: #fbc115; } diff --git a/assets/js/__tests__/interactions.spec.ts b/assets/js/__tests__/interactions.spec.ts index 93691d47c..e2d5d5770 100644 --- a/assets/js/__tests__/interactions.spec.ts +++ b/assets/js/__tests__/interactions.spec.ts @@ -243,7 +243,7 @@ describe('interactions', () => { setupInteractions(); expect(downvote.classList.contains('disabled')).toBe(true); - expect(downvote.getAttribute('title')).toMatch('Neigh!'); + expect(downvote.getAttribute('title')).toMatch('Downvote'); }); it('displayInteractionSet handles voted with no up/down value (no class changes)', () => { diff --git a/assets/js/booru.ts b/assets/js/booru.ts index ed0e9828b..4e6998c26 100644 --- a/assets/js/booru.ts +++ b/assets/js/booru.ts @@ -93,6 +93,10 @@ export interface BooruObject { * Fancy tag setting for editing images. */ fancyTagEdit: boolean; + /** + * Minimum number of tags required on upload + */ + minimumTags: number; } declare global { diff --git a/assets/js/interactions.ts b/assets/js/interactions.ts index edfe17a28..aa2629dcd 100644 --- a/assets/js/interactions.ts +++ b/assets/js/interactions.ts @@ -34,7 +34,7 @@ interface CacheRecord { value: InteractionValue; } -const spoilerDownvoteMsg = 'Neigh! - Remove spoilered tags from your filters to downvote from thumbnails'; +const spoilerDownvoteMsg = 'Downvote - Remove spoilered tags from your filters to downvote from thumbnails'; /** * Quick helper function to less verbosely iterate a QSA diff --git a/assets/js/query/__tests__/lex.spec.ts b/assets/js/query/__tests__/lex.spec.ts index 19516a4ac..497085624 100644 --- a/assets/js/query/__tests__/lex.spec.ts +++ b/assets/js/query/__tests__/lex.spec.ts @@ -73,14 +73,14 @@ describe('Lexical analysis', () => { }); it('should prioritize AND over OR', () => { - const array = generateLexArray('safe OR solo AND fluttershy', parseTerm); - expect(terms).toEqual(['safe', 'solo', 'fluttershy']); + const array = generateLexArray('safe OR solo AND alice', parseTerm); + expect(terms).toEqual(['safe', 'solo', 'alice']); expect(array).toEqual([noMatch, noMatch, noMatch, 'and_op', 'or_op']); }); it('should override ordering when using parenthetical expressions', () => { - const array = generateLexArray('(safe OR solo) AND fluttershy', parseTerm); - expect(terms).toEqual(['safe', 'solo', 'fluttershy']); + const array = generateLexArray('(safe OR solo) AND alice', parseTerm); + expect(terms).toEqual(['safe', 'solo', 'alice']); expect(fuzzes).toEqual([0, 0, 0]); expect(boosts).toEqual([1, 1, 1]); expect(array).toEqual([noMatch, noMatch, 'or_op', noMatch, 'and_op']); @@ -153,25 +153,25 @@ describe('Lexical analysis', () => { }); it('should allow extra spaces in terms', () => { - const array = generateLexArray('twilight sparkle', parseTerm); - expect(terms).toEqual(['twilight sparkle']); + const array = generateLexArray('robin hood', parseTerm); + expect(terms).toEqual(['robin hood']); expect(array).toEqual([noMatch]); }); it('should collapse consecutive AND expressions', () => { - const array = generateLexArray('safe AND solo AND fluttershy AND applejack', parseTerm); - expect(terms).toEqual(['safe', 'solo', 'fluttershy', 'applejack']); + const array = generateLexArray('safe AND solo AND alice AND sofia', parseTerm); + expect(terms).toEqual(['safe', 'solo', 'alice', 'sofia']); expect(array).toEqual([noMatch, noMatch, 'and_op', noMatch, 'and_op', noMatch, 'and_op']); }); it('should collapse consecutive OR expressions', () => { - const array = generateLexArray('safe OR solo OR fluttershy OR applejack', parseTerm); - expect(terms).toEqual(['safe', 'solo', 'fluttershy', 'applejack']); + const array = generateLexArray('safe OR solo OR alice OR sofia', parseTerm); + expect(terms).toEqual(['safe', 'solo', 'alice', 'sofia']); expect(array).toEqual([noMatch, noMatch, 'or_op', noMatch, 'or_op', noMatch, 'or_op']); }); it('should mark error on mismatched parentheses', () => { - expect(() => generateLexArray('(safe OR solo AND fluttershy', parseTerm)).toThrow('Mismatched parentheses.'); + expect(() => generateLexArray('(safe OR solo AND alice', parseTerm)).toThrow('Mismatched parentheses.'); // expect(() => generateLexArray(')bad', parseTerm).error).toThrow('Mismatched parentheses.'); }); }); diff --git a/assets/js/query/__tests__/literal.spec.ts b/assets/js/query/__tests__/literal.spec.ts index e5ea804bc..1c04e66d7 100644 --- a/assets/js/query/__tests__/literal.spec.ts +++ b/assets/js/query/__tests__/literal.spec.ts @@ -15,22 +15,22 @@ describe('Literal field parsing', () => { }); it('should handle fuzzy matching based on normalized edit distance', () => { - const matcher = makeLiteralMatcher('fluttersho', 0.8, false); - expect(matcher('fluttershy', 'tags', 0)).toBe(true); - expect(matcher('rarity', 'tags', 0)).toBe(false); + const matcher = makeLiteralMatcher('materialization', 0.8, false); + expect(matcher('materialisation', 'tags', 0)).toBe(true); + expect(matcher('quantum entanglement', 'tags', 0)).toBe(false); }); it('should handle fuzzy matching based on raw edit distance', () => { - const matcher = makeLiteralMatcher('fluttersho', 1, false); - expect(matcher('fluttershy', 'tags', 0)).toBe(true); - expect(matcher('rarity', 'tags', 0)).toBe(false); + const matcher = makeLiteralMatcher('materialization', 1, false); + expect(matcher('materialisation', 'tags', 0)).toBe(true); + expect(matcher('quantum entanglement', 'tags', 0)).toBe(false); }); it('should handle wildcard matching', () => { - const matcher = makeLiteralMatcher('fl?tter*', 0, true); - expect(matcher('fluttershy', 'tags', 0)).toBe(true); - expect(matcher('flitter', 'tags', 0)).toBe(true); - expect(matcher('rainbow dash', 'tags', 0)).toBe(false); - expect(matcher('gentle flutter', 'tags', 0)).toBe(false); + const matcher = makeLiteralMatcher('ma?e*', 0, true); + expect(matcher('mane', 'tags', 0)).toBe(true); + expect(matcher('materialization', 'tags', 0)).toBe(true); + expect(matcher('alice', 'tags', 0)).toBe(false); + expect(matcher('nightmare', 'tags', 0)).toBe(false); }); }); diff --git a/assets/js/query/__tests__/parse.spec.ts b/assets/js/query/__tests__/parse.spec.ts index 5eecfd7ec..62449f204 100644 --- a/assets/js/query/__tests__/parse.spec.ts +++ b/assets/js/query/__tests__/parse.spec.ts @@ -14,21 +14,21 @@ describe('Semantic analysis', () => { beforeAll(() => { const e0 = document.createElement('div'); e0.setAttribute(termSpaceToImageField.id, '0'); - e0.setAttribute(termSpaceToImageField.tags, 'safe, solo, fluttershy'); + e0.setAttribute(termSpaceToImageField.tags, 'safe, solo, alice'); const e1 = document.createElement('div'); e1.setAttribute(termSpaceToImageField.id, '1'); - e1.setAttribute(termSpaceToImageField.tags, 'suggestive, solo, fluttershy'); + e1.setAttribute(termSpaceToImageField.tags, 'suggestive, solo, alice'); const e2 = document.createElement('div'); e2.setAttribute(termSpaceToImageField.id, '2'); - e2.setAttribute(termSpaceToImageField.tags, 'suggestive, fluttershy, twilight sparkle'); + e2.setAttribute(termSpaceToImageField.tags, 'suggestive, alice, robin hood'); documents = [e0, e1, e2]; }); it('should match single term expressions', () => { - const tokens = generateLexArray('fluttershy', parseWithDefaultMatcher); + const tokens = generateLexArray('alice', parseWithDefaultMatcher); const matcher = parseTokens(tokens); expect(matcher(documents[0])).toBe(true); @@ -37,7 +37,7 @@ describe('Semantic analysis', () => { }); it('should match AND expressions', () => { - const tokens = generateLexArray('fluttershy,solo', parseWithDefaultMatcher); + const tokens = generateLexArray('alice,solo', parseWithDefaultMatcher); const matcher = parseTokens(tokens); expect(matcher(documents[0])).toBe(true); @@ -46,7 +46,7 @@ describe('Semantic analysis', () => { }); it('should match OR expressions', () => { - const tokens = generateLexArray('suggestive || twilight sparkle', parseWithDefaultMatcher); + const tokens = generateLexArray('suggestive || robin hood', parseWithDefaultMatcher); const matcher = parseTokens(tokens); expect(matcher(documents[0])).toBe(false); @@ -55,7 +55,7 @@ describe('Semantic analysis', () => { }); it('should match NOT expressions', () => { - const tokens = generateLexArray('NOT twilight sparkle', parseWithDefaultMatcher); + const tokens = generateLexArray('NOT robin hood', parseWithDefaultMatcher); const matcher = parseTokens(tokens); expect(matcher(documents[0])).toBe(true); diff --git a/assets/js/upload.ts b/assets/js/upload.ts index 143a092c4..ecf2a39fd 100644 --- a/assets/js/upload.ts +++ b/assets/js/upload.ts @@ -265,8 +265,8 @@ export function setupImageUpload() { errors.push('Tag input may not contain any other rating if safe'); } - if (tagsArr.length < 3) { - errors.push('Tag input must contain at least 3 tags'); + if (tagsArr.length < window.booru.minimumTags) { + errors.push(`Tag input must contain at least ${window.booru.minimumTags} tags`); } errors.forEach(msg => createTagError(msg)); diff --git a/assets/static/opensearch.xml b/assets/static/opensearch.xml deleted file mode 100644 index 48909d2ff..000000000 --- a/assets/static/opensearch.xml +++ /dev/null @@ -1,10 +0,0 @@ - - Derpibooru - Derpibooru image search - UTF-8 - https://derpibooru.org/favicon.ico - https://derpibooru.org/favicon.svg - - - - diff --git a/config/avatar.json b/config/avatar.json deleted file mode 100644 index baa37b40a..000000000 --- a/config/avatar.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "header": "", - "background": "", - "species": ["unicorn", "pegasus", "earthpony"], - "body_shapes": [ - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - } - ], - "tail_shapes": [ - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - } - ], - "hair_shapes": [ - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - } - ], - "extra_shapes": [ - { - "shape": "", - "species": ["unicorn"] - }, - { - "shape": "", - "species": ["pegasus"] - }, - { - "shape": "", - "species": ["batpony"] - }, - { - "shape": "", - "species": ["batpony"] - }, - { - "shape": "", - "species": ["unicorn", "pegasus", "earthpony", "batpony"] - } - ], - "footer": "" -} diff --git a/config/config.exs b/config/config.exs index a91901cc0..b959d3761 100644 --- a/config/config.exs +++ b/config/config.exs @@ -57,6 +57,7 @@ config :logger, :console, # Use Elixir's built-in module for JSON parsing in Phoenix config :phoenix, :json_library, JSON +config :postgrex, :json_library, JSON # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/config/quick_tag_table.json b/config/quick_tag_table.json deleted file mode 100644 index 140e8915f..000000000 --- a/config/quick_tag_table.json +++ /dev/null @@ -1,470 +0,0 @@ -{ - "tabs": [ - "Main", - "Species", - "Shorthands A", - "B", - "Ships ts", - "rd", - "ry", - "aj", - "fs", - "pp", - "misc", - "Season 9", - "8", - "7", - "6", - "5", - "4", - "3", - "2", - "1" - ], - "tab_modes": { - "Main": "default", - "Species": "default", - "Shorthands A": "shorthand", - "B": "shorthand", - "Ships ts": "shipping", - "rd": "shipping", - "ry": "shipping", - "aj": "shipping", - "fs": "shipping", - "pp": "shipping", - "misc": "shipping", - "Season 9": "season", - "8": "season", - "7": "season", - "6": "season", - "5": "season", - "4": "season", - "3": "season", - "2": "season", - "1": "season" - }, - "Main": { - "Ratings": ["safe", "suggestive", "questionable", "explicit", "semi-grimdark", "grimdark", "grotesque"], - "General Spoilers": ["spoiler:comic", "spoiler:g5", "spoiler:pony life", "spoilers for another series"], - "Species": [ - "anthro", - "equestria girls", - "human", - "humanized", - "pony", - "earth pony", - "pegasus", - "unicorn", - "alicorn" - ], - "Often Forgotten": [ - "solo", - "screencap", - "image macro", - "monochrome", - "oc", - "oc only", - "photo", - "Tag original characters oc:name" - ] - }, - "Species": { - "Major": [ - "bat pony", - "changeling", - "changedling", - "deer", - "dragon", - "griffon", - "classical hippogriff", - "yak", - "zebra" - ], - "Minor": [ - "abyssinian", - "breezie", - "centaur", - "diamond dog", - "draconequus", - "kirin", - "seapony (g4)", - "siren", - "sphinx" - ], - "Animal": [ - "chimera", - "cockatrice", - "hydra", - "manticore", - "parasprite", - "phoenix", - "tatzlwurm", - "timber wolf", - "windigo" - ], - "Misc": ["original species", "hybrid"] - }, - "Shorthands A": [ - [ - "Mane Cast", - [ - ["m6", "mane six"], - ["pt", "princess twilight"], - ["ts", "twilight sparkle"], - ["rd", "rainbow dash"], - ["ry", "rarity"], - ["aj", "applejack"], - ["fs", "fluttershy"], - ["pp", "pinkie pie"], - ["sp", "spike"] - ] - ], - [ - "Secondary Cast", - [ - ["cmc", "cutie mark crusaders"], - ["ab", "apple bloom"], - ["sl", "scootaloo"], - ["sb", "sweetie belle"], - ["tia", "princess celestia"], - ["luna", "princess luna"], - ["pcd", "princess cadance"], - ["sa", "shining armor"], - ["sg", "starlight glimmer"] - ] - ], - [ - "More Ponies", - [ - ["tx", "trixie"], - ["sus", "sunset shimmer"], - ["sombra", "king sombra"], - ["qc", "queen chrysalis"], - ["dc", "discord"], - ["nmm", "nightmare moon"], - ["sf", "spitfire"], - ["sn", "soarin'"], - ["ld", "lightning dust"] - ] - ], - [ - "More Apples", - [ - ["bm", "big macintosh"], - ["gs", "granny smith"], - ["bb", "braeburn"], - ["bs", "babs seed"] - ] - ], - [ - "Fillies and Colts", - [ - ["ss", "silver spoon"], - ["dt", "diamond tiara"], - ["pfh", "princess flurry heart"] - ] - ] - ], - "B": [ - [ - "Background Ponies", - [ - ["dh", "derpy hooves"], - ["dw", "doctor whooves"], - ["cgt", "colgate"], - ["bon", "bon bon"], - ["oct", "octavia melody"], - ["dj", "vinyl scratch"], - ["bp", "berry punch"], - ["pbb", "prince blueblood"] - ] - ], - [ - "Student Six", - [ - ["s6", "student six"], - ["ga", "gallus"], - ["ols", "ocellus"], - ["snb", "sandbar"], - ["svs", "silverstream"], - ["sm", "smolder"], - ["yn", "yona"] - ] - ], - [ - "Uncategorized", - [ - ["maud", "maud pie"], - ["coco", "coco pommel"], - ["suri", "suri polomare"], - ["rg", "royal guard"], - ["za", "zecora"], - ["mm", "mayor mare"], - ["pdp", "pinkamena diane pie"], - ["owol", "owlowiscious"], - ["opal", "opalescence"] - ] - ], - [ - "Other Tags", - [ - ["cm", "cutie mark"], - ["eoh", "elements of harmony"], - ["nmn", "nightmare night"] - ] - ] - ], - "Ships ts": { - "implying": ["shipping", "twilight sparkle"], - "not_implying": [] - }, - "rd": { - "implying": ["shipping", "rainbow dash"], - "not_implying": [] - }, - "ry": { - "implying": ["shipping", "rarity"], - "not_implying": [] - }, - "aj": { - "implying": ["shipping", "applejack"], - "not_implying": [] - }, - "fs": { - "implying": ["shipping", "fluttershy"], - "not_implying": [] - }, - "pp": { - "implying": ["shipping", "pinkie pie"], - "not_implying": [] - }, - "misc": { - "implying": ["shipping"], - "not_implying": ["twilight sparkle", "rainbow dash", "rarity", "applejack", "fluttershy", "pinkie pie"] - }, - "Season 9": [ - ["1-2", "the beginning of the end"], - ["3", "uprooted"], - ["4", "twilight's seven"], - ["5", "the point of no return"], - ["6", "common ground"], - ["7", "she's all yak"], - ["8", "frenemies (episode)"], - ["9", "sweet and smoky"], - ["10", "going to seed"], - ["11", "student counsel"], - ["12", "the last crusade (episode)"], - ["13", "between dark and dawn"], - ["14", "that's a laugh"], - ["15", "2 4 6 greaaat"], - ["16", "a trivial pursuit"], - ["17", "the summer sun setback"], - ["18", "she talks to angel"], - ["19", "dragon dropped"], - ["20", "a horse shoe-in"], - ["21", "daring doubt"], - ["22", "growing up is hard to do"], - ["23", "the big mac question"], - ["24-25", "the ending of the end"], - ["26", "the last problem"] - ], - "8": [ - ["1-2", "school daze"], - ["3", "the maud couple"], - ["4", "fake it 'til you make it"], - ["5", "grannies gone wild"], - ["6", "surf and/or turf"], - ["7", "horse play"], - ["8", "the parent map"], - ["9", "non-compete clause"], - ["10", "the break up breakdown"], - ["11", "molt down"], - ["12", "marks for effort"], - ["13", "the mean 6"], - ["14", "a matter of principals"], - ["15", "the hearth's warming club"], - ["16", "friendship university"], - ["17", "the end in friend"], - ["18", "yakity-sax"], - ["19", "on the road to friendship"], - ["20", "the washouts (episode)"], - ["21", "a rockhoof and a hard place"], - ["22", "what lies beneath"], - ["23", "sounds of silence"], - ["24", "father knows beast"], - ["25-26", "school raze"], - ["Special", "best gift ever"] - ], - "7": [ - ["1", "celestial advice"], - ["2", "all bottled up"], - ["3", "a flurry of emotions"], - ["4", "rock solid friendship"], - ["5", "fluttershy leans in"], - ["6", "forever filly"], - ["7", "parental glideance"], - ["8", "hard to say anything"], - ["9", "honest apple"], - ["10", "a royal problem"], - ["11", "not asking for trouble"], - ["12", "discordant harmony"], - ["13", "the perfect pear"], - ["14", "fame and misfortune"], - ["15", "triple threat"], - ["16", "campfire tales"], - ["17", "to change a changeling"], - ["18", "daring done?"], - ["19", "it isn't the mane thing about you"], - ["20", "a health of information"], - ["21", "marks and recreation"], - ["22", "once upon a zeppelin"], - ["23", "secrets and pies"], - ["24", "uncommon bond"], - ["25-26", "shadow play"] - ], - "6": [ - ["1-2", "the crystalling"], - ["3", "the gift of the maud pie"], - ["4", "on your marks"], - ["5", "gauntlet of fire"], - ["6", "no second prances"], - ["7", "newbie dash"], - ["8", "a hearth's warming tail"], - ["9", "the saddle row review"], - ["10", "applejack's \"day\" off"], - ["11", "flutter brutter"], - ["12", "spice up your life"], - ["13", "stranger than fan fiction"], - ["14", "the cart before the ponies"], - ["15", "28 pranks later"], - ["16", "the times they are a changeling"], - ["17", "dungeons and discords"], - ["18", "buckball season"], - ["19", "the fault in our cutie marks"], - ["20", "viva las pegasus"], - ["21", "every little thing she does"], - ["22", "ppov"], - ["23", "where the apple lies"], - ["24", "top bolt"], - ["25-26", "to where and back again"] - ], - "5": [ - ["1-2", "the cutie map"], - ["3", "castle sweet castle"], - ["4", "bloom and gloom"], - ["5", "tanks for the memories"], - ["6", "appleoosa's most wanted"], - ["7", "make new friends but keep discord"], - ["8", "the lost treasure of griffonstone"], - ["9", "slice of life (episode)"], - ["10", "princess spike"], - ["11", "party pooped"], - ["12", "amending fences"], - ["13", "do princesses dream of magic sheep"], - ["14", "canterlot boutique"], - ["15", "rarity investigates"], - ["16", "made in manehattan"], - ["17", "brotherhooves social"], - ["18", "crusaders of the lost mark"], - ["19", "the one where pinkie pie knows"], - ["20", "hearthbreakers"], - ["21", "scare master"], - ["22", "what about discord?"], - ["23", "the hooffields and mccolts"], - ["24", "the mane attraction"], - ["25-26", "the cutie remark"] - ], - "4": [ - ["1-2", "princess twilight sparkle (episode)"], - ["3", "castle mane-ia"], - ["4", "daring don't"], - ["5", "flight to the finish"], - ["6", "power ponies"], - ["7", "bats!"], - ["8", "rarity takes manehattan"], - ["9", "pinkie apple pie"], - ["10", "rainbow falls"], - ["11", "three's a crowd"], - ["12", "pinkie pride"], - ["13", "simple ways"], - ["14", "filli vanilli"], - ["15", "twilight time"], - ["16", "it ain't easy being breezies"], - ["17", "somepony to watch over me"], - ["18", "maud pie (episode)"], - ["19", "for whom the sweetie belle toils"], - ["20", "leap of faith"], - ["21", "testing testing 1-2-3"], - ["22", "trade ya"], - ["23", "inspiration manifestation"], - ["24", "equestria games (episode)"], - ["25-26", "twilight's kingdom"] - ], - "3": [ - ["1-2", "the crystal empire"], - ["3", "too many pinkie pies"], - ["4", "one bad apple"], - ["5", "magic duel"], - ["6", "sleepless in ponyville"], - ["7", "wonderbolts academy"], - ["8", "just for sidekicks"], - ["9", "apple family reunion"], - ["10", "spike at your service"], - ["11", "keep calm and flutter on"], - ["12", "games ponies play"], - ["13", "magical mystery cure"] - ], - "2": [ - ["1-2", "the return of harmony"], - ["3", "lesson zero"], - ["4", "luna eclipsed"], - ["5", "sisterhooves social"], - ["6", "the cutie pox"], - ["7", "may the best pet win"], - ["8", "the mysterious mare do well"], - ["9", "sweet and elite"], - ["10", "secret of my excess"], - ["11", "family appreciation day"], - ["12", "baby cakes"], - ["13", "hearth's warming eve (episode)"], - ["14", "the last roundup"], - ["15", "the super speedy cider squeezy 6000"], - ["16", "read it and weep"], - ["17", "hearts and hooves day (episode)"], - ["18", "a friend in deed"], - ["19", "putting your hoof down"], - ["20", "it's about time"], - ["21", "dragon quest"], - ["22", "hurricane fluttershy"], - ["23", "ponyville confidential"], - ["24", "mmmystery on the friendship express"], - ["25-26", "a canterlot wedding"] - ], - "1": [ - ["1-2", "friendship is magic"], - ["3", "the ticket master"], - ["4", "applebuck season"], - ["5", "griffon the brush off"], - ["6", "boast busters"], - ["7", "dragonshy"], - ["8", "look before you sleep"], - ["9", "bridle gossip"], - ["10", "swarm of the century"], - ["11", "winter wrap up"], - ["12", "call of the cutie"], - ["13", "fall weather friends"], - ["14", "suited for success"], - ["15", "feeling pinkie keen"], - ["16", "sonic rainboom (episode)"], - ["17", "stare master"], - ["18", "the show stoppers"], - ["19", "a dog and pony show"], - ["20", "green isn't your color"], - ["21", "over a barrel"], - ["22", "a bird in the hoof"], - ["23", "the cutie mark chronicles"], - ["24", "owl's well that ends well"], - ["25", "party of one"], - ["26", "the best night ever"] - ] -} diff --git a/config/runtime.exs b/config/runtime.exs index 0e976d4ea..d9a1bb907 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -17,11 +17,13 @@ config :philomena, opensearch_url: System.get_env("OPENSEARCH_URL", "https://admin:admin@localhost:9200"), advert_file_root: System.fetch_env!("ADVERT_FILE_ROOT"), avatar_file_root: System.fetch_env!("AVATAR_FILE_ROOT"), + system_file_root: System.fetch_env!("SYSTEM_FILE_ROOT"), badge_file_root: System.fetch_env!("BADGE_FILE_ROOT"), password_pepper: System.fetch_env!("PASSWORD_PEPPER"), avatar_url_root: System.fetch_env!("AVATAR_URL_ROOT"), advert_url_root: System.fetch_env!("ADVERT_URL_ROOT"), image_file_root: System.fetch_env!("IMAGE_FILE_ROOT"), + system_url_root: System.fetch_env!("SYSTEM_URL_ROOT"), tumblr_api_key: System.fetch_env!("TUMBLR_API_KEY"), otp_secret_key: System.fetch_env!("OTP_SECRET_KEY"), image_url_root: System.fetch_env!("IMAGE_URL_ROOT"), @@ -29,7 +31,6 @@ config :philomena, mailer_address: System.fetch_env!("MAILER_ADDRESS"), mediaproc_addr: System.fetch_env!("MEDIAPROC_ADDR"), tag_file_root: System.fetch_env!("TAG_FILE_ROOT"), - hide_version: System.get_env("HIDE_VERSION", "false"), site_domains: System.fetch_env!("SITE_DOMAINS"), tag_url_root: System.fetch_env!("TAG_URL_ROOT"), redis_host: System.get_env("REDIS_HOST", "localhost"), @@ -40,20 +41,10 @@ config :philomena, app_dir = System.get_env("APP_DIR", File.cwd!()) -json_config = - %{ - aggregation: "aggregation.json", - avatar: "avatar.json", - footer: "footer.json", - quick_tag_table: "quick_tag_table.json", - tag: "tag.json" - } - |> Map.new(fn {name, file} -> - {name, JSON.decode!(File.read!("#{app_dir}/config/#{file}"))} - end) - config :philomena, - config: json_config + config: %{ + aggregation: JSON.decode!(File.read!("#{app_dir}/config/aggregation.json")) + } config :exq, host: System.get_env("REDIS_HOST", "localhost"), diff --git a/docker-compose.yml b/docker-compose.yml index aa260825c..bca6a0b32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,11 +31,13 @@ services: - OTP_SECRET_KEY=Wn7O/8DD+qxL0X4X7bvT90wOkVGcA90bIHww4twR03Ci//zq7PnMw8ypqyyT/b/C - ADVERT_FILE_ROOT=adverts - AVATAR_FILE_ROOT=avatars + - SYSTEM_FILE_ROOT=system - BADGE_FILE_ROOT=badges - IMAGE_FILE_ROOT=images - TAG_FILE_ROOT=tags - AVATAR_URL_ROOT=/avatars - ADVERT_URL_ROOT=/spns + - SYSTEM_URL_ROOT=/system - IMAGE_URL_ROOT=/img - BADGE_URL_ROOT=/badge-img - TAG_URL_ROOT=/tag-img diff --git a/docker/production/.env-example b/docker/production/.env-example index f356e7d17..544bc4f4f 100644 --- a/docker/production/.env-example +++ b/docker/production/.env-example @@ -12,6 +12,7 @@ ADVERT_URL_ROOT=https://cdn.example.com/spns BADGE_URL_ROOT=https://cdn.example.com/badges TAG_URL_ROOT=https://cdn.example.com/tags CHANNEL_URL_ROOT=https://cdn.example.com/media +SYSTEM_URL_ROOT=https://cdn.example.com/system HCAPTCHA_SITE_KEY=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee HCAPTCHA_SECRET_KEY=0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa SITE_DOMAINS=example.com,www.example.com @@ -24,6 +25,7 @@ AVATAR_FILE_ROOT=avatars BADGE_FILE_ROOT=badges TAG_FILE_ROOT=tags IMAGE_FILE_ROOT=images +SYSTEM_FILE_ROOT=system CAMO_HOST=ext.example.com CAMO_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/docker/web/config/Caddyfile b/docker/web/config/Caddyfile index 3cd19e332..75f226219 100644 --- a/docker/web/config/Caddyfile +++ b/docker/web/config/Caddyfile @@ -26,6 +26,8 @@ @avatars path_regexp ^/avatars/(.+)$ @badges path_regexp ^/badge-img/(.+)$ @tags path_regexp ^/tag-img/(.+)$ + @system path_regexp ^/system/(.+)$ + @favicon path_regexp ^/favicon\.(ico|svg)$ import s3path @imgdownload /images/{re.imgdownload.1}/{re.imgdownload.2}/full.{re.imgdownload.3} import s3path @imgview /images/{re.imgview.1}/{re.imgview.2}/full.{re.imgview.3} @@ -34,6 +36,8 @@ import s3path @spns /adverts/{re.spns.1} import s3path @badges /badges/{re.badges.1} import s3path @tags /tags/{re.tags.1} + import s3path @system /system/{re.system.1} + import s3path @favicon /system/favicon.{re.favicon.1} # This block and the next block after it (the header one) are required # because it is possible for there to be files in the bucket which have diff --git a/lib/philomena/artist_links/artist_link.ex b/lib/philomena/artist_links/artist_link.ex index a3bcce8d4..02edcf281 100644 --- a/lib/philomena/artist_links/artist_link.ex +++ b/lib/philomena/artist_links/artist_link.ex @@ -92,7 +92,15 @@ defmodule Philomena.ArtistLinks.ArtistLink do defp put_verification_code(changeset) do code = :crypto.strong_rand_bytes(5) |> Base.encode16() - change(changeset, verification_code: "DERPI-LINKVALIDATION-#{code}") + + change(changeset, + verification_code: + String.replace( + Philomena.Configs.get("linkvalidation_format"), + "{code}", + code + ) + ) end defp put_next_check_at(changeset) do diff --git a/lib/philomena/avatars.ex b/lib/philomena/avatars.ex new file mode 100644 index 000000000..ac3de6c6a --- /dev/null +++ b/lib/philomena/avatars.ex @@ -0,0 +1,24 @@ +defmodule Philomena.Avatars do + import Ecto.Query, warn: false + alias Philomena.Repo + alias Philomena.Avatars.Kind + alias Philomena.Avatars.Part + alias Philomena.Avatars.Shape + alias Philomena.Avatars.ShapeKind + + def get_kinds() do + Repo.all(Kind) + end + + def get_parts() do + Repo.all(Part) + end + + def get_shapes() do + Repo.all(Shape) + end + + def get_shape_kinds() do + Repo.all(ShapeKind) + end +end diff --git a/lib/philomena/avatars/kind.ex b/lib/philomena/avatars/kind.ex new file mode 100644 index 000000000..8a1699147 --- /dev/null +++ b/lib/philomena/avatars/kind.ex @@ -0,0 +1,14 @@ +defmodule Philomena.Avatars.Kind do + use Ecto.Schema + import Ecto.Changeset + + schema "avatar_kinds" do + field :name, :string + end + + @doc false + def changeset(kind, attrs) do + kind + |> cast(attrs, [:name]) + end +end diff --git a/lib/philomena/avatars/part.ex b/lib/philomena/avatars/part.ex new file mode 100644 index 000000000..deebb7fe7 --- /dev/null +++ b/lib/philomena/avatars/part.ex @@ -0,0 +1,15 @@ +defmodule Philomena.Avatars.Part do + use Ecto.Schema + import Ecto.Changeset + + schema "avatar_parts" do + field :name, :string + field :priority, :integer, default: 1 + end + + @doc false + def changeset(part, attrs) do + part + |> cast(attrs, [:name, :priority]) + end +end diff --git a/lib/philomena/avatars/shape.ex b/lib/philomena/avatars/shape.ex new file mode 100644 index 000000000..a799391bc --- /dev/null +++ b/lib/philomena/avatars/shape.ex @@ -0,0 +1,19 @@ +defmodule Philomena.Avatars.Shape do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.Avatars.Part + + schema "avatar_shapes" do + belongs_to :avatar_part, Part + + field :shape, :string + field :any_kind, :boolean, default: false + end + + @doc false + def changeset(shape, attrs) do + shape + |> cast(attrs, [:name, :avatar_part_id]) + end +end diff --git a/lib/philomena/avatars/shape_kind.ex b/lib/philomena/avatars/shape_kind.ex new file mode 100644 index 000000000..56c655c49 --- /dev/null +++ b/lib/philomena/avatars/shape_kind.ex @@ -0,0 +1,18 @@ +defmodule Philomena.Avatars.ShapeKind do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.Avatars.Kind + alias Philomena.Avatars.Shape + + schema "avatar_shape_kinds" do + belongs_to :avatar_kind, Kind + belongs_to :avatar_shape, Shape + end + + @doc false + def changeset(shape_kind, attrs) do + shape_kind + |> cast(attrs, [:avatar_shape_id, :avatar_kind_id]) + end +end diff --git a/lib/philomena/commissions/commission.ex b/lib/philomena/commissions/commission.ex index c4108cee6..12f2125c2 100644 --- a/lib/philomena/commissions/commission.ex +++ b/lib/philomena/commissions/commission.ex @@ -12,12 +12,12 @@ defmodule Philomena.Commissions.Commission do has_many :items, Item field :open, :boolean - field :categories, {:array, :string}, default: [] field :information, :string, default: "" field :contact, :string, default: "" field :will_create, :string, default: "" field :will_not_create, :string, default: "" field :commission_items_count, :integer, default: 0 + field :accepting_requests, :boolean, default: false timestamps(inserted_at: :created_at, type: :utc_datetime) end @@ -31,61 +31,34 @@ defmodule Philomena.Commissions.Commission do :will_create, :will_not_create, :open, - :sheet_image_id, - :categories + :sheet_image_id ]) - |> drop_blank_categories() |> validate_required([:user_id, :information, :contact, :open]) |> validate_length(:information, max: 1000, count: :bytes) |> validate_length(:contact, max: 1000, count: :bytes) |> validate_length(:will_create, max: 1000, count: :bytes) |> validate_length(:will_not_create, max: 1000, count: :bytes) - |> validate_subset(:categories, Keyword.values(categories())) end - defp drop_blank_categories(changeset) do - categories = - changeset - |> get_field(:categories) - |> Enum.filter(&(&1 not in [nil, ""])) - - change(changeset, categories: categories) - end - - def categories do - [ - Anthro: "Anthro", - "Canon Characters": "Canon Characters", - Comics: "Comics", - "Fetish Art": "Fetish Art", - "Human and EqG": "Human and EqG", - NSFW: "NSFW", - "Original Characters": "Original Characters", - "Original Species": "Original Species", - Pony: "Pony", - Requests: "Requests", - Safe: "Safe", - Shipping: "Shipping", - "Violence and Gore": "Violence and Gore" - ] - end - - def types do + def suggested_tags do [ - "Sketch", - "Colored Sketch", - "Inked", - "Flat Color", - "Vector", - "Cel Shaded", - "Fully Shaded", - "Traditional", - "Pixel Art", - "Animation", - "Crafted Item", - "Sculpture", - "Plushie", - "Other" + "safe", + "suggestive", + "questionable", + "explicit", + "anthro", + "feral", + "human", + "humanoid", + "oc", + "original species", + "shipping", + "comic", + "gore", + "violence", + "fetish", + "my little pony", + "fanart" ] end end diff --git a/lib/philomena/commissions/item.ex b/lib/philomena/commissions/item.ex index 34f1a1530..5989bbcfb 100644 --- a/lib/philomena/commissions/item.ex +++ b/lib/philomena/commissions/item.ex @@ -25,7 +25,26 @@ defmodule Philomena.Commissions.Item do |> validate_length(:description, max: 300, count: :bytes) |> validate_length(:add_ons, max: 500, count: :bytes) |> validate_number(:base_price, greater_than_or_equal_to: 0, less_than_or_equal_to: 99_999) - |> validate_inclusion(:item_type, Commission.types()) + |> validate_inclusion(:item_type, types()) |> foreign_key_constraint(:example_image_id, name: :fk_rails_56d368749a) end + + def types do + [ + "Sketch", + "Colored Sketch", + "Inked", + "Flat Color", + "Vector", + "Cel Shaded", + "Fully Shaded", + "Traditional", + "Pixel Art", + "Animation", + "Crafted Item", + "Sculpture", + "Plushie", + "Other" + ] + end end diff --git a/lib/philomena/commissions/tagging.ex b/lib/philomena/commissions/tagging.ex new file mode 100644 index 000000000..3bbc2dfbb --- /dev/null +++ b/lib/philomena/commissions/tagging.ex @@ -0,0 +1,21 @@ +defmodule Philomena.Commissions.Tagging do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.Commissions.Commission + alias Philomena.Tags.Tag + + @primary_key false + + schema "commission_taggings" do + belongs_to :commission, Commission, primary_key: true + belongs_to :tag, Tag, primary_key: true + end + + @doc false + def changeset(tagging, attrs) do + tagging + |> cast(attrs, []) + |> validate_required([]) + end +end diff --git a/lib/philomena/config.ex b/lib/philomena/config.ex deleted file mode 100644 index a6a35bd38..000000000 --- a/lib/philomena/config.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Philomena.Config do - def get(key) do - Application.get_env(:philomena, :config)[key] - end -end diff --git a/lib/philomena/configs.ex b/lib/philomena/configs.ex new file mode 100644 index 000000000..52e1f7b2e --- /dev/null +++ b/lib/philomena/configs.ex @@ -0,0 +1,122 @@ +defmodule Philomena.Configs do + import Ecto.Query, warn: false + alias Philomena.Repo + + alias Philomena.Configs.Config + + def get(key) do + key + |> fetch() + |> cast_value(Map.get(config_types(), key, :string)) + end + + def get_all() do + defaults() + |> Map.keys() + |> Enum.map(fn key -> {key, get(key)} end) + |> Map.new() + end + + def set(key, value) do + %Config{} + |> Config.changeset(%{key: key, value: value}) + |> Repo.insert( + on_conflict: :replace_all, + conflict_target: :key + ) + end + + # Fetch config by its key. + # This will try to fetch it from the environment variable first, + # then from the database, and finally from the defaults. + # If the environment variable is set, it will override the database value. + # The environment variable must be set to the uppercase version of the key. + defp fetch(key) do + fetch(key, System.get_env(String.upcase(key), "")) + end + + defp fetch(key, "") do + case Repo.get_by(Config, key: key) do + nil -> Map.get(defaults(), key) + config -> config.value + end + end + + defp fetch(_, val) do + val + end + + defp cast_value(value, :integer), do: String.to_integer(value) + defp cast_value(value, :boolean), do: value in ["true", "1", "yes"] + defp cast_value(value, :float), do: String.to_float(value) + defp cast_value(value, :list), do: String.split(value, ",") |> Enum.map(&String.trim/1) + defp cast_value(value, _), do: value + + defp defaults do + %{ + "site_name" => "Philomena", + "site_slug" => "philomena", + "site_description" => "The next-generation imageboard", + "site_url" => "http://philomena.local", + "default_theme" => "dark-blue", + "default_light_theme" => "light-blue", + "ad_text" => "Interested in advertising on {site_name}? Click here to learn more.", + "donation_text" => + "This site is free to use, but costs money to run. If you enjoy using {site_name}, please consider supporting us financially.", + "linkvalidation_format" => "PHILOMENA-LINKVALIDATION-{code}", + "anonymous_name" => "Anonymous", + "borderless_tags" => "false", + "rounded_tags" => "false", + "compact_hidden_communications" => "false", + # todo + "read_only_mode" => "false", + "hide_version" => "false", + "commissions_enabled" => "true", + "livestreams_enabled" => "true", + "dnp_enabled" => "true", + "getting_started_enabled" => "false", + "colored_logo" => "true", + "allow_system_uploads" => "true", + "default_filter_id" => "1", + "everything_filter_id" => "2", + "nsfw_filter_id" => "2", + "minimum_tags" => "3", + "hidden_trending_tags" => "" + } + end + + defp config_types do + %{ + "site_name" => :string, + "site_slug" => :string, + "site_description" => :string, + "site_url" => :string, + "default_theme" => :string, + "default_light_theme" => :string, + "ad_text" => :string, + "donation_text" => :string, + "linkvalidation_format" => :string, + "anonymous_name" => :string, + "favicon_image" => :string, + "tagblocked_image" => :string, + "noavatar_image" => :string, + "favicon_ico" => :string, + "borderless_tags" => :boolean, + "rounded_tags" => :boolean, + "compact_hidden_communications" => :boolean, + "read_only_mode" => :boolean, + "hide_version" => :boolean, + "commissions_enabled" => :boolean, + "livestreams_enabled" => :boolean, + "dnp_enabled" => :boolean, + "getting_started_enabled" => :boolean, + "colored_logo" => :boolean, + "allow_system_uploads" => :boolean, + "default_filter_id" => :integer, + "everything_filter_id" => :integer, + "nsfw_filter_id" => :integer, + "minimum_tags" => :integer, + "hidden_trending_tags" => :list + } + end +end diff --git a/lib/philomena/configs/config.ex b/lib/philomena/configs/config.ex new file mode 100644 index 000000000..1d5c3cec0 --- /dev/null +++ b/lib/philomena/configs/config.ex @@ -0,0 +1,15 @@ +defmodule Philomena.Configs.Config do + use Ecto.Schema + import Ecto.Changeset + + schema "configs" do + field :key, :string + field :value, :string + end + + @doc false + def changeset(config, attrs) do + config + |> cast(attrs, [:key, :value]) + end +end diff --git a/lib/philomena/data_exports/aggregator.ex b/lib/philomena/data_exports/aggregator.ex index 7101881b3..649d3f776 100644 --- a/lib/philomena/data_exports/aggregator.ex +++ b/lib/philomena/data_exports/aggregator.ex @@ -90,7 +90,7 @@ defmodule Philomena.DataExports.Aggregator do :body ]}, {Commission, - [:open, :sheet_image_id, :categories, :information, :contact, :will_create, :will_not_create]}, + [:open, :sheet_image_id, :information, :contact, :will_create, :will_not_create]}, {DnpEntry, [:tag_id, :aasm_state, :dnp_type, :hide_reason, :feedback, :reason, :instructions], :requesting_user_id}, {DuplicateReport, [:reason, :image_id, :duplicate_of_image_id]}, diff --git a/lib/philomena/dnp_entries/dnp_entry.ex b/lib/philomena/dnp_entries/dnp_entry.ex index e59ab21ef..76e125a78 100644 --- a/lib/philomena/dnp_entries/dnp_entry.ex +++ b/lib/philomena/dnp_entries/dnp_entry.ex @@ -63,6 +63,8 @@ defmodule Philomena.DnpEntries.DnpEntry do end def reasons do + site_name = Philomena.Configs.get("site_name") + [ {"No Edits", "I would like to prevent edited versions of my artwork from being uploaded in the future"}, @@ -71,11 +73,11 @@ defmodule Philomena.DnpEntries.DnpEntry do {"Uploader Credit Change", "I would like the uploader credit for already existing uploads of my art to be assigned to me"}, {"Certain Type/Location Only", - "I only want to allow art of a certain type or from a certain location to be uploaded to Derpibooru"}, + "I only want to allow art of a certain type or from a certain location to be uploaded to #{site_name}"}, {"With Permission Only", - "I only want people with my permission to be allowed to upload my art to Derpibooru"}, + "I only want people with my permission to be allowed to upload my art to #{site_name}"}, {"Artist Upload Only", - "I want to be the only person allowed to upload my art to Derpibooru"}, + "I want to be the only person allowed to upload my art to #{site_name}"}, {"Other", "I would like a DNP entry under other conditions"} ] end diff --git a/lib/philomena/footer_links.ex b/lib/philomena/footer_links.ex new file mode 100644 index 000000000..98c9ce761 --- /dev/null +++ b/lib/philomena/footer_links.ex @@ -0,0 +1,39 @@ +defmodule Philomena.FooterLinks do + import Ecto.Query, warn: false + alias Philomena.Repo + alias Philomena.FooterLinks.Category + alias Philomena.FooterLinks.Link + + def get_categories() do + Repo.all(Category) + end + + def get_links() do + Link + |> preload(:category) + |> Repo.all() + end + + def get_footer_data() do + categories = + Repo.all( + from c in Category, + order_by: c.position + ) + + links = + Repo.all( + from l in Link, + order_by: l.position + ) + + links_by_category = Enum.group_by(links, & &1.category_id) + + Enum.map(categories, fn category -> + %{ + title: category.title, + links: Map.get(links_by_category, category.id, []) + } + end) + end +end diff --git a/lib/philomena/footer_links/category.ex b/lib/philomena/footer_links/category.ex new file mode 100644 index 000000000..23b52f693 --- /dev/null +++ b/lib/philomena/footer_links/category.ex @@ -0,0 +1,15 @@ +defmodule Philomena.FooterLinks.Category do + use Ecto.Schema + import Ecto.Changeset + + schema "footer_categories" do + field :title, :string + field :position, :integer + end + + @doc false + def changeset(category, attrs) do + category + |> cast(attrs, [:title, :position]) + end +end diff --git a/lib/philomena/footer_links/link.ex b/lib/philomena/footer_links/link.ex new file mode 100644 index 000000000..73d61bb51 --- /dev/null +++ b/lib/philomena/footer_links/link.ex @@ -0,0 +1,22 @@ +defmodule Philomena.FooterLinks.Link do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.FooterLinks.Category + + schema "footer_links" do + belongs_to :footer_category, Category + + field :title, :string + field :url, :string + field :position, :integer + field :bold, :boolean, default: false + field :new_tab, :boolean, default: false + end + + @doc false + def changeset(link, attrs) do + link + |> cast(attrs, [:title, :url, :footer_category_id, :position, :bold, :new_tab]) + end +end diff --git a/lib/philomena/images/tag_validator.ex b/lib/philomena/images/tag_validator.ex index 1c654722a..c6d7b81ea 100644 --- a/lib/philomena/images/tag_validator.ex +++ b/lib/philomena/images/tag_validator.ex @@ -1,5 +1,4 @@ defmodule Philomena.Images.TagValidator do - alias Philomena.Config import Ecto.Changeset def validate_tags(changeset) do @@ -26,8 +25,8 @@ defmodule Philomena.Images.TagValidator do rating_set = ratings(tag_set) changeset - |> validate_number_of_tags(tag_set, 3) - |> validate_bad_words(tag_set) + |> validate_number_of_tags(tag_set, Philomena.Configs.get("minimum_tags")) + |> validate_tag_validity(tags) |> validate_has_rating(rating_set) |> validate_safe(rating_set) |> validate_sexual_exclusion(rating_set) @@ -56,15 +55,17 @@ defmodule Philomena.Images.TagValidator do end end - def validate_bad_words(changeset, tag_set) do - bad_words = MapSet.new(Config.get(:tag)["blacklist"]) - intersection = MapSet.intersection(tag_set, bad_words) + def validate_tag_validity(changeset, tags) do + invalid_tags = + tags + |> Enum.filter(fn tag -> tag.invalid == true end) + |> Enum.map(& &1.name) - if MapSet.size(intersection) > 0 do - Enum.reduce( - intersection, + if Enum.any?(invalid_tags) do + add_error( changeset, - &add_error(&2, :tag_input, "contains forbidden tag `#{&1}'") + :tag_input, + "must not contain invalid tags (#{Enum.join(invalid_tags, ", ")})" ) else changeset diff --git a/lib/philomena/quick_tags.ex b/lib/philomena/quick_tags.ex new file mode 100644 index 000000000..1d000151b --- /dev/null +++ b/lib/philomena/quick_tags.ex @@ -0,0 +1,98 @@ +defmodule Philomena.QuickTags do + import Ecto.Query, warn: false + alias Philomena.Repo + alias Philomena.QuickTags.Default + alias Philomena.QuickTags.Season + alias Philomena.QuickTags.Shipping + alias Philomena.QuickTags.ShorthandCategory + alias Philomena.QuickTags.Shorthand + alias Philomena.QuickTags.Tab + + def get_tabs() do + Repo.all(Tab) + end + + def get_shorthand_categories() do + ShorthandCategory + |> preload(:quick_tag_tab) + |> Repo.all() + end + + def get_shorthand_tags() do + Shorthand + |> preload(:shorthand_category) + |> Repo.all() + end + + def get_default_tags() do + Default + |> preload(:quick_tag_tab) + |> Repo.all() + end + + def get_season_tags() do + Season + |> preload(:quick_tag_tab) + |> Repo.all() + end + + def get_shipping_tags() do + Shipping + |> preload(:quick_tag_tab) + |> Repo.all() + end + + @doc """ + Builds a structured representation of quick tags for display. + Returns tabs with their associated data in the natural database structure. + """ + def build_quick_tag_structure() do + tabs = + get_tabs() + |> Enum.sort_by(& &1.position) + + defaults = get_default_tags() + seasons = get_season_tags() + categories = get_shorthand_categories() + shorthands = get_shorthand_tags() + shippings = get_shipping_tags() + + # Group data by tab + tabs_with_data = + tabs + |> Enum.map(fn tab -> + %{ + tab: tab, + mode: determine_tab_mode(tab, defaults, seasons, categories, shippings), + defaults: Enum.filter(defaults, &(&1.quick_tag_tab_id == tab.id)), + seasons: Enum.filter(seasons, &(&1.quick_tag_tab_id == tab.id)), + categories: Enum.filter(categories, &(&1.quick_tag_tab_id == tab.id)), + shippings: Enum.filter(shippings, &(&1.quick_tag_tab_id == tab.id)) + } + end) + + # Get all tag names for lookup + tag_names = extract_all_tag_names(defaults, seasons, shorthands) + + {tabs_with_data, tag_names, shorthands} + end + + defp determine_tab_mode(tab, defaults, seasons, categories, shippings) do + cond do + Enum.any?(defaults, &(&1.quick_tag_tab_id == tab.id)) -> :default + Enum.any?(seasons, &(&1.quick_tag_tab_id == tab.id)) -> :season + Enum.any?(categories, &(&1.quick_tag_tab_id == tab.id)) -> :shorthand + Enum.any?(shippings, &(&1.quick_tag_tab_id == tab.id)) -> :shipping + true -> :default + end + end + + defp extract_all_tag_names(defaults, seasons, shorthands) do + default_tags = Enum.flat_map(defaults, & &1.tags) + season_tags = Enum.map(seasons, & &1.tag) + shorthand_tags = Enum.map(shorthands, & &1.tag) + + (default_tags ++ season_tags ++ shorthand_tags) + |> Enum.uniq() + end +end diff --git a/lib/philomena/quick_tags/default.ex b/lib/philomena/quick_tags/default.ex new file mode 100644 index 000000000..942da5485 --- /dev/null +++ b/lib/philomena/quick_tags/default.ex @@ -0,0 +1,19 @@ +defmodule Philomena.QuickTags.Default do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.QuickTags.Tab + + schema "default_quick_tags" do + belongs_to :quick_tag_tab, Tab + + field :category, :string + field :tags, {:array, :string} + end + + @doc false + def changeset(tags, attrs) do + tags + |> cast(attrs, [:category, :tags]) + end +end diff --git a/lib/philomena/quick_tags/season.ex b/lib/philomena/quick_tags/season.ex new file mode 100644 index 000000000..1f89f7401 --- /dev/null +++ b/lib/philomena/quick_tags/season.ex @@ -0,0 +1,19 @@ +defmodule Philomena.QuickTags.Season do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.QuickTags.Tab + + schema "season_quick_tags" do + belongs_to :quick_tag_tab, Tab + + field :episode, :integer + field :tag, :string + end + + @doc false + def changeset(tags, attrs) do + tags + |> cast(attrs, [:episode, :tag]) + end +end diff --git a/lib/philomena/quick_tags/shipping.ex b/lib/philomena/quick_tags/shipping.ex new file mode 100644 index 000000000..ba3bcdd88 --- /dev/null +++ b/lib/philomena/quick_tags/shipping.ex @@ -0,0 +1,20 @@ +defmodule Philomena.QuickTags.Shipping do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.QuickTags.Tab + + schema "shipping_quick_tags" do + belongs_to :quick_tag_tab, Tab + + field :category, :string + field :implying, {:array, :string}, default: [] + field :not_implying, {:array, :string}, default: [] + end + + @doc false + def changeset(tags, attrs) do + tags + |> cast(attrs, [:category, :implying, :not_implying]) + end +end diff --git a/lib/philomena/quick_tags/shorthand.ex b/lib/philomena/quick_tags/shorthand.ex new file mode 100644 index 000000000..b4dff854d --- /dev/null +++ b/lib/philomena/quick_tags/shorthand.ex @@ -0,0 +1,19 @@ +defmodule Philomena.QuickTags.Shorthand do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.QuickTags.ShorthandCategory + + schema "shorthand_quick_tags" do + belongs_to :shorthand_quick_tag_category, ShorthandCategory + + field :shorthand, :string + field :tag, :string + end + + @doc false + def changeset(shorthand, attrs) do + shorthand + |> cast(attrs, [:shorthand, :tag]) + end +end diff --git a/lib/philomena/quick_tags/shorthand_category.ex b/lib/philomena/quick_tags/shorthand_category.ex new file mode 100644 index 000000000..08cf5f7c4 --- /dev/null +++ b/lib/philomena/quick_tags/shorthand_category.ex @@ -0,0 +1,18 @@ +defmodule Philomena.QuickTags.ShorthandCategory do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.QuickTags.Tab + + schema "shorthand_quick_tag_categories" do + belongs_to :quick_tag_tab, Tab + + field :category, :string + end + + @doc false + def changeset(category, attrs) do + category + |> cast(attrs, [:category]) + end +end diff --git a/lib/philomena/quick_tags/tab.ex b/lib/philomena/quick_tags/tab.ex new file mode 100644 index 000000000..1c31950da --- /dev/null +++ b/lib/philomena/quick_tags/tab.ex @@ -0,0 +1,15 @@ +defmodule Philomena.QuickTags.Tab do + use Ecto.Schema + import Ecto.Changeset + + schema "quick_tag_tabs" do + field :title, :string + field :position, :integer + end + + @doc false + def changeset(tab, attrs) do + tab + |> cast(attrs, [:title, :position]) + end +end diff --git a/lib/philomena/system_images.ex b/lib/philomena/system_images.ex new file mode 100644 index 000000000..ddd78e106 --- /dev/null +++ b/lib/philomena/system_images.ex @@ -0,0 +1,133 @@ +defmodule Philomena.SystemImages do + @moduledoc """ + The System Images context. + """ + + import Ecto.Query, warn: false + alias Philomena.Repo + + alias Philomena.SystemImages.SystemImage + alias Philomena.SystemImages.Uploader + + @protected_keys ["favicon.svg", "favicon.ico", "tagblocked.svg", "no_avatar.svg"] + + @doc """ + Returns the list of system images. + + ## Examples + + iex> list_system_images() + [%SystemImage{}, ...] + + """ + def list_system_images do + Repo.all(SystemImage) + end + + @doc """ + Gets a single system_image. + + Raises `Ecto.NoResultsError` if the System Image does not exist. + + ## Examples + + iex> get_system_image!(123) + %SystemImage{} + + iex> get_system_image!(456) + ** (Ecto.NoResultsError) + + """ + def get_system_image!(id), do: Repo.get!(SystemImage, id) + + @doc """ + Gets a single system image by its key. + + Returns nil if the System Image does not exist. + + ## Examples + + iex> get_system_image_by_key("favicon.svg") + %SystemImage{} + + iex> get_system_image_by_key("nonexistent.png") + nil + + """ + def get_system_image_by_key(key), do: Repo.get_by(SystemImage, key: key) + + @doc """ + Creates a system image. + + ## Examples + + iex> create_system_image(%{key: "favicon.svg"}) + {:ok, %SystemImage{}} + + iex> create_system_image(%{key: "virus.exe"}) + {:error, %Ecto.Changeset{}} + + """ + def create_system_image(%{image: image} = attrs) do + %SystemImage{} + |> SystemImage.changeset(attrs) + |> Repo.insert() + |> case do + {:ok, system_image} -> + Uploader.upload_system_image(image, system_image.key) + + {:ok, system_image} + + error -> + error + end + end + + @doc """ + Updates a system image without updating its image. + + ## Examples + + iex> update_system_image(system_image, %{key: "new_key.png"}) + {:ok, %SystemImage{}} + + iex> update_system_image(system_image, %{key: "virus.exe"}) + {:error, %Ecto.Changeset{}} + + """ + def update_system_image(%SystemImage{key: key} = system_image, _) when key in @protected_keys, + do: {:error, system_image} + + def update_system_image(%SystemImage{} = system_image, attrs) do + system_image + |> SystemImage.changeset(attrs) + |> Repo.update() + end + + @doc """ + Updates the uploaded file for a system image. + + """ + def update_system_image_file(%SystemImage{key: key}, %{image: image}) do + Uploader.upload_system_image(image, key) + end + + @doc """ + Deletes a SystemImage. + + ## Examples + + iex> delete_system_image(system_image) + {:ok, %SystemImage{}} + + iex> delete_system_image(system_image) + {:error, %Ecto.Changeset{}} + + """ + def delete_system_image(%SystemImage{key: key} = system_image) when key in @protected_keys, + do: {:error, system_image} + + def delete_system_image(%SystemImage{} = system_image) do + Repo.delete(system_image) + end +end diff --git a/lib/philomena/system_images/system_image.ex b/lib/philomena/system_images/system_image.ex new file mode 100644 index 000000000..e037747f8 --- /dev/null +++ b/lib/philomena/system_images/system_image.ex @@ -0,0 +1,14 @@ +defmodule Philomena.SystemImages.SystemImage do + use Ecto.Schema + import Ecto.Changeset + + schema "system_images" do + field :key, :string + end + + @doc false + def changeset(system_image, attrs) do + system_image + |> cast(attrs, [:key]) + end +end diff --git a/lib/philomena/system_images/uploader.ex b/lib/philomena/system_images/uploader.ex new file mode 100644 index 000000000..9db361b4b --- /dev/null +++ b/lib/philomena/system_images/uploader.ex @@ -0,0 +1,41 @@ +defmodule Philomena.SystemImages.Uploader do + @moduledoc """ + Upload and processing callback logic for system images. + """ + + alias PhilomenaMedia.Uploader + alias PhilomenaMedia.Remote + + def upload_system_image(file, "favicon.svg") do + path = Path.join(system_file_root(), "favicon.svg") + + Uploader.persist_file(path, file) + generate_favicon_ico(file) + end + + def upload_system_image(file, image_name) do + path = Path.join(system_file_root(), image_name) + + Uploader.persist_file(path, file) + end + + defp generate_favicon_ico(file) do + outfile = Briefly.create!(extname: ".ico") + + Remote.cmd("magick", [ + "-density", + "256x256", + "-background", + "transparent", + "-define", + "icon:auto-resize=\"16,32,48,64,128\"", + file, + outfile + ]) + + upload_system_image(outfile, "favicon.ico") + end + + defp system_file_root, + do: Application.fetch_env!(:philomena, :system_file_root) +end diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index 43ca67ef4..b257ce4d3 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -76,8 +76,8 @@ defmodule Philomena.Tags do ## Examples - iex> get_or_create_tags("safe, cute, pony") - [%Tag{name: "safe"}, %Tag{name: "cute"}, %Tag{name: "pony"}] + iex> get_or_create_tags("safe, cute, alice") + [%Tag{name: "safe"}, %Tag{name: "cute"}, %Tag{name: "alice"}] """ @spec get_or_create_tags(String.t()) :: list() diff --git a/lib/philomena/tags/local_autocomplete.ex b/lib/philomena/tags/local_autocomplete.ex index 6f1785edf..bf0eab74a 100644 --- a/lib/philomena/tags/local_autocomplete.ex +++ b/lib/philomena/tags/local_autocomplete.ex @@ -96,6 +96,6 @@ defmodule Philomena.Tags.LocalAutocomplete do select: t.id, limit: ^amount - Repo.all(assoc_query, timeout: 120_000) + Repo.all(assoc_query, timeout: 300_000) end end diff --git a/lib/philomena/tags/tag.ex b/lib/philomena/tags/tag.ex index 41eef2542..ffc6b9c07 100644 --- a/lib/philomena/tags/tag.ex +++ b/lib/philomena/tags/tag.ex @@ -20,10 +20,12 @@ defmodule Philomena.Tags.Tag do "commissioner", "editor", "fanfic", + "generator", "oc", "parent", "parents", "photographer", + "prompter", "series", "species", "spoiler", @@ -37,8 +39,10 @@ defmodule Philomena.Tags.Tag do "comic" => "content-fanmade", "editor" => "origin", "fanfic" => "content-fanmade", + "generator" => "origin", "oc" => "oc", "photographer" => "origin", + "prompter" => "origin", "series" => "content-fanmade", "spoiler" => "spoiler", "video" => "content-fanmade" @@ -50,7 +54,8 @@ defmodule Philomena.Tags.Tag do "commissioner:", "editor:", "oc:", - "photographer:" + "photographer:", + "prompter:" ] @derive {Phoenix.Param, key: :slug} @@ -87,6 +92,7 @@ defmodule Philomena.Tags.Tag do field :image_format, :string field :image_mime_type, :string field :mod_notes, :string + field :invalid, :boolean, default: false field :uploaded_image, :string, virtual: true field :removed_image, :string, virtual: true diff --git a/lib/philomena/user_downvote_wipe.ex b/lib/philomena/user_downvote_wipe.ex index b958ab9ba..d50400301 100644 --- a/lib/philomena/user_downvote_wipe.ex +++ b/lib/philomena/user_downvote_wipe.ex @@ -17,11 +17,13 @@ defmodule Philomena.UserDownvoteWipe do |> where(user_id: ^user.id, up: false) |> Batch.query_batches(id_field: :image_id) |> Enum.each(fn queryable -> - {_, image_ids} = Repo.delete_all(select(queryable, [i_v], i_v.image_id)) + {_, image_ids} = Repo.delete_all(select(queryable, [i_v], i_v.image_id), timeout: 120_000) {count, nil} = - Repo.update_all(where(Image, [i], i.id in ^image_ids), - inc: [downvotes_count: -1, score: 1] + Repo.update_all( + where(Image, [i], i.id in ^image_ids), + [inc: [downvotes_count: -1, score: 1]], + timeout: 120_000 ) Repo.update_all(where(User, id: ^user.id), inc: [votes_cast_count: -count]) @@ -34,14 +36,19 @@ defmodule Philomena.UserDownvoteWipe do |> where(user_id: ^user.id, up: true) |> Batch.query_batches(id_field: :image_id) |> Enum.each(fn queryable -> - {_, image_ids} = Repo.delete_all(select(queryable, [i_v], i_v.image_id)) + {_, image_ids} = Repo.delete_all(select(queryable, [i_v], i_v.image_id), timeout: 120_000) {count, nil} = - Repo.update_all(where(Image, [i], i.id in ^image_ids), - inc: [upvotes_count: -1, score: -1] + Repo.update_all( + where(Image, [i], i.id in ^image_ids), + [inc: [upvotes_count: -1, score: -1]], + timeout: 120_000 ) - Repo.update_all(where(User, id: ^user.id), inc: [votes_cast_count: -count]) + Repo.update_all(where(User, id: ^user.id), + inc: [votes_cast_count: -count], + timeout: 120_000 + ) reindex(image_ids) end) @@ -50,12 +57,16 @@ defmodule Philomena.UserDownvoteWipe do |> where(user_id: ^user.id) |> Batch.query_batches(id_field: :image_id) |> Enum.each(fn queryable -> - {_, image_ids} = Repo.delete_all(select(queryable, [i_f], i_f.image_id)) + {_, image_ids} = Repo.delete_all(select(queryable, [i_f], i_f.image_id), timeout: 120_000) {count, nil} = - Repo.update_all(where(Image, [i], i.id in ^image_ids), inc: [faves_count: -1]) + Repo.update_all(where(Image, [i], i.id in ^image_ids), [inc: [faves_count: -1]], + timeout: 120_000 + ) - Repo.update_all(where(User, id: ^user.id), inc: [images_favourited_count: -count]) + Repo.update_all(where(User, id: ^user.id), [inc: [images_favourited_count: -count]], + timeout: 120_000 + ) reindex(image_ids) end) diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index ca3d9ad3e..610a84d31 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -517,16 +517,17 @@ defmodule Philomena.Users.User do end def totp_qrcode(user) do + site_name = Philomena.Configs.get("site_name") secret = totp_secret(user) provisioning_uri = %URI{ scheme: "otpauth", host: "totp", - path: "/Derpibooru:" <> user.email, + path: "/#{site_name}:" <> user.email, query: URI.encode_query(%{ secret: secret, - issuer: "Derpibooru" + issuer: site_name }) } @@ -615,7 +616,7 @@ defmodule Philomena.Users.User do do: user.otp_backup_codes |> Enum.reject(&Password.verify_pass(token, &1)) def theme_colors do - ~W(red orange yellow blue green purple teal pink gray) + ~W(red orange yellow green blue purple teal pink gray) end def theme_names do diff --git a/lib/philomena_query/batch.ex b/lib/philomena_query/batch.ex index bb9c2c74d..664fba036 100644 --- a/lib/philomena_query/batch.ex +++ b/lib/philomena_query/batch.ex @@ -132,6 +132,6 @@ defmodule PhilomenaQuery.Batch do |> where([m], field(m, ^id_field) > ^max_id) |> select([m], field(m, ^id_field)) |> limit(^batch_size) - |> Repo.all() + |> Repo.all(timeout: 120_000) end end diff --git a/lib/philomena_web/controllers/activity_controller.ex b/lib/philomena_web/controllers/activity_controller.ex index faf5b58eb..74c3e751a 100644 --- a/lib/philomena_web/controllers/activity_controller.ex +++ b/lib/philomena_web/controllers/activity_controller.ex @@ -3,6 +3,7 @@ defmodule PhilomenaWeb.ActivityController do alias PhilomenaWeb.ImageLoader alias PhilomenaQuery.Search + alias Philomena.Configs alias Philomena.{ Images.Image, @@ -20,6 +21,10 @@ defmodule PhilomenaWeb.ActivityController do def index(conn, _params) do user = conn.assigns.current_user + hidden_trending_tags = + Configs.get("hidden_trending_tags") + |> Enum.map(&String.to_integer/1) + {images, _tags} = ImageLoader.default_query(conn, pagination: %{conn.assigns.image_pagination | page_number: 1} @@ -28,7 +33,16 @@ defmodule PhilomenaWeb.ActivityController do {top_scoring, _tags} = ImageLoader.query( conn, - %{range: %{first_seen_at: %{gt: "now-3d"}}}, + %{ + bool: %{ + must: %{ + range: %{first_seen_at: %{gt: "now-3d"}} + }, + must_not: [ + %{terms: %{tag_ids: hidden_trending_tags}} + ] + } + }, sorts: &%{query: &1, sorts: [%{wilson_score: :desc}, %{first_seen_at: :desc}]}, pagination: %{page_number: :rand.uniform(6), page_size: 4} ) diff --git a/lib/philomena_web/controllers/opensearch_controller.ex b/lib/philomena_web/controllers/opensearch_controller.ex new file mode 100644 index 000000000..766fdbbad --- /dev/null +++ b/lib/philomena_web/controllers/opensearch_controller.ex @@ -0,0 +1,10 @@ +defmodule PhilomenaWeb.OpensearchController do + use PhilomenaWeb, :controller + + def index(conn, _params) do + conn + |> put_resp_content_type("text/xml") + |> put_format(:xml) + |> render("index.xml") + end +end diff --git a/lib/philomena_web/controllers/setting_controller.ex b/lib/philomena_web/controllers/setting_controller.ex index b628cdaf3..328e60373 100644 --- a/lib/philomena_web/controllers/setting_controller.ex +++ b/lib/philomena_web/controllers/setting_controller.ex @@ -1,6 +1,7 @@ defmodule PhilomenaWeb.SettingController do use PhilomenaWeb, :controller + alias Philomena.Configs alias Philomena.Users alias Philomena.Users.User alias Philomena.Schema.TagList @@ -81,13 +82,13 @@ defmodule PhilomenaWeb.SettingController do |> Map.put(:theme_color, theme_color) end - defp assign_theme(_), do: assign_theme(%{theme: "dark-blue"}) + defp assign_theme(_), do: assign_theme(%{theme: Configs.get("default_theme")}) defp determine_theme(%{"theme_name" => name, "theme_color" => color} = attrs) when name != nil and color != nil, do: Map.put(attrs, "theme", "#{name}-#{color}") - defp determine_theme(attrs), do: Map.put(attrs, "theme", "dark-blue") + defp determine_theme(attrs), do: Map.put(attrs, "theme", Configs.get("default_theme")) defp maybe_update_user(conn, nil, _user_params), do: {:ok, conn} diff --git a/lib/philomena_web/controllers/staff_controller.ex b/lib/philomena_web/controllers/staff_controller.ex index 2d363f84e..8857e4676 100644 --- a/lib/philomena_web/controllers/staff_controller.ex +++ b/lib/philomena_web/controllers/staff_controller.ex @@ -17,7 +17,8 @@ defmodule PhilomenaWeb.StaffController do "Technical Team": Enum.filter( users, - &(&1.role != "admin" and &1.secondary_role in ["Site Developer", "Devops"]) + &(&1.role != "admin" and + &1.secondary_role in ["Site Developer", "Devops", "System Administrator"]) ), "Public Relations": Enum.filter(users, &(&1.role != "admin" and &1.secondary_role == "Public Relations")), @@ -37,7 +38,14 @@ defmodule PhilomenaWeb.StaffController do Enum.filter( users, &(&1.role != "user" and - &1.secondary_role not in [nil, "", "Site Developer", "Devops", "Public Relations"] and + &1.secondary_role not in [ + nil, + "", + "Site Developer", + "System Administrator", + "Devops", + "Public Relations" + ] and &1.hide_default_role == true) ) ] diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 49a2799dd..23ffcd78d 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -554,6 +554,7 @@ defmodule PhilomenaWeb.Router do get "/:forum_id/:id", TopicController, :show get "/:forum_id/:id/:page", TopicController, :show get "/:forum_id/:id/post/:post_id", TopicController, :show + get "/opensearch.xml", OpensearchController, :index end # Other scopes may use custom stacks. diff --git a/lib/philomena_web/stats_updater.ex b/lib/philomena_web/stats_updater.ex index b91094d8c..307250eb1 100644 --- a/lib/philomena_web/stats_updater.ex +++ b/lib/philomena_web/stats_updater.ex @@ -1,5 +1,4 @@ defmodule PhilomenaWeb.StatsUpdater do - alias Philomena.Config alias PhilomenaQuery.Search alias Philomena.Images.Image alias Philomena.Comments.Comment @@ -65,7 +64,7 @@ defmodule PhilomenaWeb.StatsUpdater do end defp aggregations do - data = Config.get(:aggregation) + data = Application.get_env(:philomena, :config)[:aggregation] { Search.search(Image, data["images"]), diff --git a/lib/philomena_web/templates/activity/_getting_starter.html.slime b/lib/philomena_web/templates/activity/_getting_starter.html.slime new file mode 100644 index 000000000..7e0b17228 --- /dev/null +++ b/lib/philomena_web/templates/activity/_getting_starter.html.slime @@ -0,0 +1,24 @@ +- site_name = Philomena.Configs.get("site_name") + +.block.block--fixed.block--success + h3 Welcome to #{site_name}! + p + ' Check out our + strong + a href="/pages/start" + ' Getting Started Guide + ' to learn how to use the site, as it may explain some peculiarities of #{site_name} that you may not be used to. Also be sure to check out the + strong + a href="/pages/rules" + ' Site Rules + ' and + strong + a href="/pages/tags" + | Tagging Guidelines + | . + p + ' If you are of legal age and would like to view adult content, you can visit the + strong + a href="/filters" + ' Filters Page + ' to personalize your content settings. Have fun! diff --git a/lib/philomena_web/templates/activity/index.html.slime b/lib/philomena_web/templates/activity/index.html.slime index 0add728c0..755e24069 100644 --- a/lib/philomena_web/templates/activity/index.html.slime +++ b/lib/philomena_web/templates/activity/index.html.slime @@ -40,6 +40,8 @@ ' Most Commented-on Images .column-layout__main + = if is_nil(@conn.assigns.current_user) and Philomena.Configs.get("getting_started_enabled") do + = render PhilomenaWeb.ActivityView, "_getting_started.html", conn: @conn = render PhilomenaWeb.ImageView, "index.html", conn: @conn, images: @images, size: :thumb = if @show_sidebar and not is_nil(@watched) and Enum.any?(@watched) do .block diff --git a/lib/philomena_web/templates/admin/user/_form.html.slime b/lib/philomena_web/templates/admin/user/_form.html.slime index 1d522625e..62e4761fb 100644 --- a/lib/philomena_web/templates/admin/user/_form.html.slime +++ b/lib/philomena_web/templates/admin/user/_form.html.slime @@ -21,7 +21,7 @@ .table-list__label__input = select f, :role, ["user", "assistant", "moderator", "admin"], class: "input" label.table-list__label .table-list__label__text Secondary banner: - .table-list__label__input = select f, :secondary_role, [[key: "-", value: ""], "Site Developer", "Devops", "Philomena Contributor", "Public Relations"], class: "input" + .table-list__label__input = select f, :secondary_role, [[key: "-", value: ""], "Site Developer", "Devops", "System Administrator", "Philomena Contributor", "Public Relations"], class: "input" label.table-list__label .table-list__label__text Hide staff banner: .table-list__label__input = checkbox f, :hide_default_role, class: "checkbox" diff --git a/lib/philomena_web/templates/advert/_box.html.slime b/lib/philomena_web/templates/advert/_box.html.slime index 0193d67c8..b6cbae057 100644 --- a/lib/philomena_web/templates/advert/_box.html.slime +++ b/lib/philomena_web/templates/advert/_box.html.slime @@ -1,14 +1,12 @@ .block#imagespns - .spnstxt - ' Interested in advertising on Derpibooru? - => link "Click here", to: "/pages/advertising" - ' for information! + - site_name = Philomena.Configs.get("site_name") + - ad_text = String.replace(Philomena.Configs.get("ad_text"), "{site_name}", site_name) + - donation_text = String.replace(Philomena.Configs.get("donation_text"), "{site_name}", site_name) + + .spnstxt = link ad_text, to: "/pages/advertising" a#imagespns__link href=~p"/adverts/#{@advert}" rel="nofollow" title=@advert.title img src=advert_image_url(@advert) alt=@advert.title p - strong - ' Derpibooru costs over $25 a day to operate - - = link "help support us financially", to: "/pages/donations" - ' ! + strong = link donation_text, to: "/pages/donations" diff --git a/lib/philomena_web/templates/api/rss/watched/index.html.eex b/lib/philomena_web/templates/api/rss/watched/index.html.eex index 094512500..9ad8d0415 100644 --- a/lib/philomena_web/templates/api/rss/watched/index.html.eex +++ b/lib/philomena_web/templates/api/rss/watched/index.html.eex @@ -1,8 +1,8 @@ - Derpibooru Watchlist - Your watched tags feed from Derpibooru + <%= Philomena.Configs.get("site_name") %> Watchlist + Your watched tags feed from <%= Philomena.Configs.get("site_name") %> <%= url(~p"/api/v1/rss/watched") %> <%= last_build_date() %> diff --git a/lib/philomena_web/templates/commission/_directory_results.html.slime b/lib/philomena_web/templates/commission/_directory_results.html.slime index a54d4c47b..a1bfbbd17 100644 --- a/lib/philomena_web/templates/commission/_directory_results.html.slime +++ b/lib/philomena_web/templates/commission/_directory_results.html.slime @@ -40,9 +40,10 @@ elixir: => Decimal.round(max.base_price, 2) |> Decimal.to_string() ' USD - p - strong> Categories: - = Enum.join(c.categories, ", ") + / TODO: debranding (commission_taggings) + / p + / strong> Categories: + / = Enum.join(c.categories, ", ") p strong> Offers: diff --git a/lib/philomena_web/templates/commission/_directory_sidebar.html.slime b/lib/philomena_web/templates/commission/_directory_sidebar.html.slime index 58c86be1c..93e259cae 100644 --- a/lib/philomena_web/templates/commission/_directory_sidebar.html.slime +++ b/lib/philomena_web/templates/commission/_directory_sidebar.html.slime @@ -3,13 +3,14 @@ span.block__header__title Search .block__content = form_for @changeset, ~p"/commissions", [as: :commission, method: "get", class: "hform"], fn f -> - .field = label f, :categories, "Art Categories:" - - = for {name, _value} <- categories() do - - checked = @conn.params["commission"]["category"] && Atom.to_string(name) in @conn.params["commission"]["category"] - .field - => checkbox f, name, checked_value: name, checked: checked, name: "commission[category][]", class: "checkbox spacing-right", hidden_input: false - => label f, name, name + / TODO: debranding (commission_taggings) + / .field = label f, :categories, "Art Categories:" + + / = for {name, _value} <- categories() do + / - checked = @conn.params["commission"]["category"] && Atom.to_string(name) in @conn.params["commission"]["category"] + / .field + / => checkbox f, name, checked_value: name, checked: checked, name: "commission[category][]", class: "checkbox spacing-right", hidden_input: false + / => label f, name, name br diff --git a/lib/philomena_web/templates/filter/_filter.html.slime b/lib/philomena_web/templates/filter/_filter.html.slime index ab4e92604..6df7f36eb 100644 --- a/lib/philomena_web/templates/filter/_filter.html.slime +++ b/lib/philomena_web/templates/filter/_filter.html.slime @@ -21,8 +21,10 @@ li = link "View this filter", to: ~p"/filters/#{@filter}", class: "button" - li - = link "Copy and Customize", to: ~p"/filters/new?#{[based_on: @filter]}", class: "button" + + = if not is_nil(@conn.assigns.current_user) do + li + = link "Copy and Customize", to: ~p"/filters/new?#{[based_on: @filter]}", class: "button" = if can?(@conn, :edit, @filter) do li diff --git a/lib/philomena_web/templates/image/_image_box.html.slime b/lib/philomena_web/templates/image/_image_box.html.slime index 06d8e4355..d06fe2e93 100644 --- a/lib/philomena_web/templates/image/_image_box.html.slime +++ b/lib/philomena_web/templates/image/_image_box.html.slime @@ -21,14 +21,14 @@ elixir: .media-box data-image-id=@image.id .media-box__header.media-box__header--link-row class=header_class data-image-id=@image.id a.interaction--fave href="#" rel="nofollow" data-image-id=@image.id - span.fave-span title="Fave!" + span.fave-span title="Fave" i.fa.fa-star span.favorites title="Favorites" data-image-id=@image.id = @image.faves_count a.interaction--upvote href="#" rel="nofollow" data-image-id=@image.id - i.fa.fa-arrow-up title="Yay!" + i.fa.fa-arrow-up title="Upvote" span.score title="Score" data-image-id=@image.id = @image.score a.interaction--downvote href="#" rel="nofollow" data-image-id=@image.id - i.fa.fa-arrow-down title="Neigh!" + i.fa.fa-arrow-down title="Downvote" a.interaction--comments href="/#{@image.id}#comments" title="Comments" i.fa.fa-comments span.comments_count data-image-id=@image.id = @image.comments_count diff --git a/lib/philomena_web/templates/image/_image_meta.html.slime b/lib/philomena_web/templates/image/_image_meta.html.slime index 32a2eb5ca..fba2d858a 100644 --- a/lib/philomena_web/templates/image/_image_meta.html.slime +++ b/lib/philomena_web/templates/image/_image_meta.html.slime @@ -12,16 +12,16 @@ .stretched-mobile-links a.interaction--fave href="#" rel="nofollow" data-image-id=@image.id span.favorites> title="Favorites" data-image-id=@image.id = @image.faves_count - span.fave-span title="Fave!" + span.fave-span title="Fave" i.fa.fa-star a.interaction--upvote href="#" rel="nofollow" data-image-id=@image.id = if show_vote_counts?(@conn.assigns.current_user) do span.upvotes> title="Upvotes" data-image-id=@image.id = @image.upvotes_count - span.upvote-span title="Yay!" + span.upvote-span title="Upvote" i.fa.fa-arrow-up span.score.block__header__title data-image-id=@image.id = @image.score a.interaction--downvote href="#" rel="nofollow" data-image-id=@image.id - span.downvote-span title="Neigh!" + span.downvote-span title="Downvote" i.fa.fa-arrow-down = if show_vote_counts?(@conn.assigns.current_user) do span.downvotes< title="Downvotes" data-image-id=@image.id = @image.downvotes_count diff --git a/lib/philomena_web/templates/image/_options.html.slime b/lib/philomena_web/templates/image/_options.html.slime index 36e7adcef..193abf275 100644 --- a/lib/philomena_web/templates/image/_options.html.slime +++ b/lib/philomena_web/templates/image/_options.html.slime @@ -1,4 +1,5 @@ - display_mod_tools? = can?(@conn, :hide, @image) +- site_name = Philomena.Configs.get("site_name") #image_options_area .block__header.block__header--js-tabbed @@ -30,7 +31,7 @@ #embed_options - source_link = if image_has_sources(@image), do: " - [url=#{image_first_source(@image)}]Original source[/url]", else: " (Original source unknown at time of posting)" - medium_url = thumb_url(@image, false, select_version(@image, :medium)) - h5 Derpibooru + h5 = site_name p strong> Small thumbnail input.input#embed_small_thumbnail_tag type="text" value=">>#{@image.id}s" cols="10" readonly="readonly" @@ -57,7 +58,7 @@ | Copy br textarea.input.input--wide.input--separate-top#bbcode_embed_full_tag rows="2" cols="100" readonly="readonly" - = "[img]#{medium_url}[/img]\n[url=#{url(~p"/images/#{@image}")}]View on Derpibooru[/url]#{source_link}" + = "[img]#{medium_url}[/img]\n[url=#{url(~p"/images/#{@image}")}]View on #{site_name}[/url]#{source_link}" p strong> Thumbnailed BBcode a href="#" data-click-copy="#bbcode_embed_thumbnail_tag" @@ -65,7 +66,7 @@ | Copy br textarea.input.input--wide.input--separate-top#bbcode_embed_thumbnail_tag rows="2" cols="100" readonly="readonly" - = "[img]#{medium_url}[/img]\n[url=#{url(~p"/images/#{@image}")}]View on Derpibooru[/url]#{source_link}" + = "[img]#{medium_url}[/img]\n[url=#{url(~p"/images/#{@image}")}]View on #{site_name}[/url]#{source_link}" = if display_mod_tools? do .block__tab.hidden data-tab="replace" diff --git a/lib/philomena_web/templates/image/_tags.html.slime b/lib/philomena_web/templates/image/_tags.html.slime index fe73ed1da..dcf38bccc 100644 --- a/lib/philomena_web/templates/image/_tags.html.slime +++ b/lib/philomena_web/templates/image/_tags.html.slime @@ -21,12 +21,15 @@ = hidden_input f, :old_tag_input, value: tag_input .field - = label f, :tag_input do - ' Separate tags with commas. Use 'artist:name' tags to identify artists. Got questions? Check the - a> href="/pages/tags" tag guidelines - ' or the - a href="/pages/spoilers" spoiler guidelines - ' . + .block.block--fixed.block--warning + = label f, :tag_input do + ' Separate tags with commas. Use 'artist:name' tags to identify artists. Got questions? Check the + strong + a> href="/pages/tags" tag guidelines + ' or the + strong + a href="/pages/spoilers" spoiler guidelines + ' . = render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :tag_input, type: :edit, extra: [value: tag_input] = error_tag f, :tag_input diff --git a/lib/philomena_web/templates/image/new.html.slime b/lib/philomena_web/templates/image/new.html.slime index 2a080eb11..e9dca44ae 100644 --- a/lib/philomena_web/templates/image/new.html.slime +++ b/lib/philomena_web/templates/image/new.html.slime @@ -59,10 +59,26 @@ ' Add source .field - label for="image[tag_input]" - ' Describe with - strong> 3+ - ' tags, including ratings and applicable artist tags + .block.block--fixed.block--warning + h3 Important info about our tagging + p + label for="image[tag_input]" + ' Describe with + strong + => Philomena.Configs.get("minimum_tags") + ' or more + ' tags, including ratings and applicable creator tags + p + label for="image[tag_input]" + ' Tags are separated by commas, and may have spaces in them (for example: use "solo female", not "solo_female"). + p + label for="image[tag_input]" + strong> Please make sure you understand how to tag sexual ratings correctly! + ' Check out + strong + a href="/pages/tags" + ' our tagging guidelines + ' before posting, and pick an appropriate rating tag. = render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :tag_input, type: :upload = error_tag f, :tag_input diff --git a/lib/philomena_web/templates/image/reporting/show.html.slime b/lib/philomena_web/templates/image/reporting/show.html.slime index 32216dbec..80870498d 100644 --- a/lib/philomena_web/templates/image/reporting/show.html.slime +++ b/lib/philomena_web/templates/image/reporting/show.html.slime @@ -1,3 +1,8 @@ +p + ' If this image breaks any of the + a href="/pages/rules" + | Site Rules + ' , or you have any special requests, please use this button to report it. a href=~p"/images/#{@image}/reports/new" button.button.button--link i.fa.fa-exclamation-triangle> @@ -6,6 +11,7 @@ a href=~p"/images/#{@image}/reports/new" .report-duplicate - checked = Enum.any?(@dupe_reports, & &1.state == "open") + p Is this image already on the site? Did you upload an updated version of this image? Report it here to have this image merged and redirected to the newer/better image! input.toggle-box id="image-dedupe" type="checkbox" checked=checked label for="image-dedupe" Updating/merging .toggle-box-container diff --git a/lib/philomena_web/templates/layout/_footer.html.slime b/lib/philomena_web/templates/layout/_footer.html.slime index 927562380..f4ea21469 100644 --- a/lib/philomena_web/templates/layout/_footer.html.slime +++ b/lib/philomena_web/templates/layout/_footer.html.slime @@ -1,18 +1,16 @@ footer#footer #footer_content - - footer_data = footer_data() - - = for column <- footer_data["cols"] do + = for column <- footer_data() do .footercol - h5.hide-mobile = column - h2.hide-desktop = column + h5.hide-mobile = column.title + h2.hide-desktop = column.title - = for row <- footer_data[column] do - = if row["bold"] do + = for row <- column.links do + = if row.bold do strong - a href=row["url"] target=row["target"]||"_self" = row["title"] + a href=row.url target=row.target||"_self" = row.title - else - a href=row["url"] target=row["target"]||"_self" = row["title"] + a href=row.url target=row.target||"_self" = row.title br #serving_info diff --git a/lib/philomena_web/templates/layout/_header.html.slime b/lib/philomena_web/templates/layout/_header.html.slime index e3d6d356f..69717a436 100644 --- a/lib/philomena_web/templates/layout/_header.html.slime +++ b/lib/philomena_web/templates/layout/_header.html.slime @@ -7,7 +7,7 @@ header.header a.header__link href="/" i.fa.fw.favicon-home span.fa__text.hide-limited-desktop.hide-mobile - ' Derpibooru + = Philomena.Configs.get("site_name") a.header__link.hide-mobile href="/images/new" title="Upload" i.fa.fa-upload @@ -21,9 +21,10 @@ header.header name="q" title=title value=@conn.params["q"] - placeholder="Search" + placeholder="Search (ex: "safe, folded wings")" autocapitalize="none" autocomplete=if(@conn.cookies["enable_search_ac"], do: "on", else: "off") + autocorrect="off" inputmode="search" data-autocomplete="multi-tags" data-autocomplete-condition="enable_search_ac" diff --git a/lib/philomena_web/templates/layout/_opengraph.html.slime b/lib/philomena_web/templates/layout/_opengraph.html.slime index 1c97cfa9d..bd1603587 100644 --- a/lib/philomena_web/templates/layout/_opengraph.html.slime +++ b/lib/philomena_web/templates/layout/_opengraph.html.slime @@ -1,3 +1,6 @@ +- site_name = Philomena.Configs.get("site_name") +- site_description = Philomena.Configs.get("site_description") + meta name="generator" content=generator_name() meta name="theme-color" content="#618fc3" meta name="format-detection" content="telephone=no" @@ -5,7 +8,7 @@ meta name="format-detection" content="telephone=no" = if opengraph?(@conn) do - image = @conn.assigns.image - filtered = not image.thumbnails_generated - - title = "##{image.id} - #{Philomena.Images.tag_list(image)} - Derpibooru" + - title = "##{image.id} - #{Philomena.Images.tag_list(image)} - #{site_name}" - description = "⭐ #{image.faves_count} ↕️ #{image.score} 💬 #{image.comments_count} • #{image.image_width}x#{image.image_height} #{String.upcase(to_string(image.image_format))}" - {thumb_large, {large_width, large_height}} = ImageView.thumb_url_size(image, false, :large) - thumb_rendered = ImageView.thumb_url(image, false, :rendered) @@ -13,7 +16,7 @@ meta name="format-detection" content="telephone=no" meta name="keywords" content=tag_list(image) meta name="description" content=title - meta property="og:site_name" content="Derpibooru" + meta property="og:site_name" content=site_name meta property="og:title" content=title meta property="og:description" content=description meta property="og:url" content=url(~p"/images/#{image}") @@ -64,4 +67,4 @@ meta name="format-detection" content="telephone=no" meta property="og:type" content="website" - else - meta name="description" content="Derpibooru is a linear imagebooru which lets you share, find and discover new art and media surrounding the show My Little Pony: Friendship is Magic" + meta name="description" content=site_description diff --git a/lib/philomena_web/templates/layout/_options.html.slime b/lib/philomena_web/templates/layout/_options.html.slime index 74124e582..48ed4629e 100644 --- a/lib/philomena_web/templates/layout/_options.html.slime +++ b/lib/philomena_web/templates/layout/_options.html.slime @@ -4,3 +4,6 @@ = if @current_user.rounded_tags do link rel="stylesheet" href=~p"/css/options/tag-rounded-border.css" + += if Philomena.Configs.get("colored_logo") do + link rel="stylesheet" href=~p"/css/options/colored-logo.css" diff --git a/lib/philomena_web/templates/layout/app.html.slime b/lib/philomena_web/templates/layout/app.html.slime index 4f9a86239..39ef827e9 100644 --- a/lib/philomena_web/templates/layout/app.html.slime +++ b/lib/philomena_web/templates/layout/app.html.slime @@ -1,3 +1,4 @@ +- site_name = Philomena.Configs.get("site_name") doctype html html lang="en" head @@ -7,9 +8,10 @@ html lang="en" title = if assigns[:title] do => assigns[:title] - ' - Derpibooru + ' - + => site_name - else - ' Derpibooru + => site_name link rel="preconnect" href="https://#{cdn_host()}" link rel="stylesheet" href=~p"/css/application.css" link#js-theme-stylesheet rel="stylesheet" href=stylesheet_path(@conn, @current_user) @@ -18,7 +20,7 @@ html lang="en" = render PhilomenaWeb.LayoutView, "_options.html", assigns link rel="icon" href="/favicon.ico" type="image/x-icon" link rel="icon" href="/favicon.svg" type="image/svg+xml" - link rel="search" type="application/opensearchdescription+xml" title="Derpibooru" href="/opensearch.xml" + link rel="search" type="application/opensearchdescription+xml" title=site_name href="/opensearch.xml" = csrf_meta_tag() = vite_hmr? do diff --git a/lib/philomena_web/templates/layout/two_factor.html.slime b/lib/philomena_web/templates/layout/two_factor.html.slime index bf491965c..c5486847c 100644 --- a/lib/philomena_web/templates/layout/two_factor.html.slime +++ b/lib/philomena_web/templates/layout/two_factor.html.slime @@ -5,7 +5,7 @@ html lang="en" meta http-equiv="X-UA-Compatible" content="IE=edge" = viewport_meta_tag(@conn) - title Two Factor Authentication - Derpibooru + title Two Factor Authentication - #{Philomena.Configs.get("site_name")} link rel="stylesheet" href=~p"/css/application.css" link rel="stylesheet" href=stylesheet_path(@conn, nil) link rel="stylesheet" href=light_stylesheet_path(@conn) media="(prefers-color-scheme: light)" diff --git a/lib/philomena_web/templates/opensearch/index.xml.eex b/lib/philomena_web/templates/opensearch/index.xml.eex new file mode 100644 index 000000000..37f6fa596 --- /dev/null +++ b/lib/philomena_web/templates/opensearch/index.xml.eex @@ -0,0 +1,10 @@ + + <%= site_name() %> + <%= site_name() %> image search + UTF-8 + <%= "#{site_url()}/favicon.ico" %> + <%= "#{site_url()}/favicon.svg" %> + > + + + diff --git a/lib/philomena_web/templates/profile/artist_link/_form.html.slime b/lib/philomena_web/templates/profile/artist_link/_form.html.slime index b97987e49..ecd401d3c 100644 --- a/lib/philomena_web/templates/profile/artist_link/_form.html.slime +++ b/lib/philomena_web/templates/profile/artist_link/_form.html.slime @@ -15,7 +15,7 @@ .field label for="uri" - ' URL of your art webpage (may be your Derpibooru profile page if you have no other sources) + ' URL of your art webpage (may be your #{Philomena.Configs.get("site_name")} profile page if you have no other sources) = url_input f, :uri, class: "input input--wide", placeholder: "https://www.deviantart.com/your-name", required: true = error_tag f, :uri diff --git a/lib/philomena_web/templates/profile/artist_link/index.html.slime b/lib/philomena_web/templates/profile/artist_link/index.html.slime index 7cb3be111..1a2a53a59 100644 --- a/lib/philomena_web/templates/profile/artist_link/index.html.slime +++ b/lib/philomena_web/templates/profile/artist_link/index.html.slime @@ -3,7 +3,7 @@ p a.button href=~p"/profiles/#{@user}/artist_links/new" ' Request a link p - ' Artist links associate your account on Derpibooru with tags about content you create and with accounts on sites elsewhere. This allows users to easily identify artists and staff to act more rapidly on takedown requests. + ' Artist links associate your account on #{Philomena.Configs.get("site_name")} with tags about content you create and with accounts on sites elsewhere. This allows users to easily identify artists and staff to act more rapidly on takedown requests. table.table thead diff --git a/lib/philomena_web/templates/profile/commission/_form.html.slime b/lib/philomena_web/templates/profile/commission/_form.html.slime index 1936edf1f..e98a56d68 100644 --- a/lib/philomena_web/templates/profile/commission/_form.html.slime +++ b/lib/philomena_web/templates/profile/commission/_form.html.slime @@ -20,11 +20,12 @@ => label f, :will_not_create, "Content you will not draw or create (optional):" = textarea f, :will_not_create, class: "input input--wide input--text", placeholder: "List specific content you are not willing to accept commissions for." = error_tag f, :will_not_create - .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") - = error_tag f, :categories + / TODO: debranding (commission_taggings) + / .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") + / = error_tag f, :categories .field => label f, :sheet_image_id, "Image ID of your commissions sheet (optional but recommended):" br diff --git a/lib/philomena_web/templates/profile/commission/_listing_sidebar.html.slime b/lib/philomena_web/templates/profile/commission/_listing_sidebar.html.slime index 93db5128d..2b99181a4 100644 --- a/lib/philomena_web/templates/profile/commission/_listing_sidebar.html.slime +++ b/lib/philomena_web/templates/profile/commission/_listing_sidebar.html.slime @@ -33,14 +33,14 @@ .block__content.commission__block_body = @rendered.contact -/ Categories block -.block - .block__header - span.block__header__title Art Categories - .block__content - = for cat <- @commission.categories do - span.commission__category - = cat +/ TODO: debranding (commission_taggings) +/ .block +/ .block__header +/ span.block__header__title Art Categories +/ .block__content +/ = for cat <- @commission.categories do +/ span.commission__category +/ = cat / Will create block = if @commission.will_create not in [nil, ""] do diff --git a/lib/philomena_web/templates/tag/_quick_tag_table.html.slime b/lib/philomena_web/templates/tag/_quick_tag_table.html.slime index 060927389..48db990a6 100644 --- a/lib/philomena_web/templates/tag/_quick_tag_table.html.slime +++ b/lib/philomena_web/templates/tag/_quick_tag_table.html.slime @@ -1,14 +1,10 @@ elixir: - tabs = Enum.with_index(@data["tabs"]) - tab_modes = @data["tab_modes"] + tabs = Enum.with_index(@tabs_with_data) .block__header--sub.block__header--js-tabbed - = for {name, i} <- tabs do - = link name, to: "#", class: tab_class(i), data: [click_tab: name] + = for {tab_data, i} <- tabs do + = link tab_data.tab.title, to: "#", class: tab_class(i), data: [click_tab: tab_data.tab.title] -= for {name, i} <- tabs do - - tab_data = @data[name] - - tab_mode = tab_modes[name] - - .block__tab.quick-tag-table__tab class=tab_body_class(i) data-tab=name - = render PhilomenaWeb.TagView, "_quick_tag_table_#{tab_mode}.html", tab: name, data: tab_data, shipping: @shipping, tags: @tags, conn: @conn += for {tab_data, i} <- tabs do + .block__tab.quick-tag-table__tab class=tab_body_class(i) data-tab=tab_data.tab.title + = render PhilomenaWeb.TagView, "_quick_tag_table_#{tab_data.mode}.html", tab_data: tab_data, tags: @tags, shorthands: @shorthands, conn: @conn diff --git a/lib/philomena_web/templates/tag/_quick_tag_table_default.html.slime b/lib/philomena_web/templates/tag/_quick_tag_table_default.html.slime index 7b4a61e24..1cee25cce 100644 --- a/lib/philomena_web/templates/tag/_quick_tag_table_default.html.slime +++ b/lib/philomena_web/templates/tag/_quick_tag_table_default.html.slime @@ -1,8 +1,8 @@ -= for {heading, tag_names} <- @data do += for default <- @tab_data.defaults do div - strong = heading + strong = default.category br - = for tag_name <- tag_names do + = for tag_name <- default.tags do = tag_link @tags[tag_name], tag_name br diff --git a/lib/philomena_web/templates/tag/_quick_tag_table_season.html.slime b/lib/philomena_web/templates/tag/_quick_tag_table_season.html.slime index 1f451a67d..ff791dd7f 100644 --- a/lib/philomena_web/templates/tag/_quick_tag_table_season.html.slime +++ b/lib/philomena_web/templates/tag/_quick_tag_table_season.html.slime @@ -1,9 +1,9 @@ -= for slice <- Enum.chunk_every(@data, 10) do += for slice <- Enum.chunk_every(@tab_data.seasons, 10) do div - = for map <- slice do - - [header, tag_name] = Enum.to_list(map) + = for season <- slice do + - episode_label = if is_integer(season.episode), do: to_string(season.episode), else: season.episode - = header + = episode_label ' . - = tag_link @tags[tag_name], tag_name + = tag_link @tags[season.tag], season.tag br diff --git a/lib/philomena_web/templates/tag/_quick_tag_table_shipping.html.slime b/lib/philomena_web/templates/tag/_quick_tag_table_shipping.html.slime index 6dfd7c226..f083d5fdc 100644 --- a/lib/philomena_web/templates/tag/_quick_tag_table_shipping.html.slime +++ b/lib/philomena_web/templates/tag/_quick_tag_table_shipping.html.slime @@ -1,5 +1,6 @@ -= for slice <- Enum.chunk_every(@shipping[@tab], 10) do - div - = for tag <- slice do - = tag_link tag, tag.name - br += if Map.has_key?(@tab_data, :shipping_tags) do + = for slice <- Enum.chunk_every(@tab_data.shipping_tags, 10) do + div + = for tag <- slice do + = tag_link tag, tag.name + br diff --git a/lib/philomena_web/templates/tag/_quick_tag_table_shorthand.html.slime b/lib/philomena_web/templates/tag/_quick_tag_table_shorthand.html.slime index ad34a1818..c50f552c5 100644 --- a/lib/philomena_web/templates/tag/_quick_tag_table_shorthand.html.slime +++ b/lib/philomena_web/templates/tag/_quick_tag_table_shorthand.html.slime @@ -1,10 +1,10 @@ -= for [heading, maps] <- @data do += for category <- @tab_data.categories do div - strong = heading + strong = category.category br - = for [name, alias_name] <- maps do - => name + = for shorthand <- Enum.filter(@shorthands, &(&1.shorthand_quick_tag_category_id == category.id)) do + => shorthand.shorthand ' - - = tag_link @tags[alias_name], alias_name + = tag_link @tags[shorthand.tag], shorthand.tag br diff --git a/lib/philomena_web/templates/tag/index.html.slime b/lib/philomena_web/templates/tag/index.html.slime index 9810550ca..f4bbf06a6 100644 --- a/lib/philomena_web/templates/tag/index.html.slime +++ b/lib/philomena_web/templates/tag/index.html.slime @@ -68,7 +68,7 @@ table.table td Literal td Matches the name of the target tag, if this tag is aliased. td - code = link "alias_of:twilight sparkle", to: ~p"/tags?#{[tq: "alias_of:twilight sparkle"]}" + code = link "alias_of:folded wings", to: ~p"/tags?#{[tq: "alias_of:folded wings"]}" tr td code aliased @@ -82,7 +82,7 @@ table.table td Literal td Matches the name of any of this tag's aliases. td - code = link "aliases:ts", to: ~p"/tags?#{[tq: "aliases:ts"]}" + code = link "aliases:closed wings", to: ~p"/tags?#{[tq: "aliases:closed wings"]}" tr td code analyzed_name @@ -110,7 +110,7 @@ table.table td Numeric Range td Matches the numeric surrogate key for this tag. td - code = link "id:40482", to: ~p"/tags?#{[tq: "id:40482"]}" + code = link "id:1", to: ~p"/tags?#{[tq: "id:1"]}" tr td code images @@ -145,7 +145,7 @@ table.table td Literal td Matches the name of this tag with any namespace component removed. td - code = link "name_in_namespace:johnjoseco", to: ~p"/tags?#{[tq: "name_in_namespace:johnjoseco"]}" + code = link "name_in_namespace:nighty", to: ~p"/tags?#{[tq: "name_in_namespace:nighty"]}" tr td code namespace @@ -166,4 +166,4 @@ table.table td Literal td Matches the slug of this tag. td - code = link "slug:-fwslash-mlp-fwslash-", to: ~p"/tags?#{[tq: "slug:-fwslash-mlp-fwslash-"]}" + code = link "slug:eyes+closed", to: ~p"/tags?#{[tq: "slug:eyes+closed"]}" diff --git a/lib/philomena_web/views/api/json/oembed_view.ex b/lib/philomena_web/views/api/json/oembed_view.ex index 60ceaac10..32095147d 100644 --- a/lib/philomena_web/views/api/json/oembed_view.ex +++ b/lib/philomena_web/views/api/json/oembed_view.ex @@ -10,16 +10,18 @@ defmodule PhilomenaWeb.Api.Json.OembedView do end def render("show.json", %{image: image}) do + site_name = Philomena.Configs.get("site_name") + {thumbnail_url, {thumbnail_width, thumbnail_height}} = ImageView.thumb_url_size(image, false, :large) %{ type: "photo", version: "1.0", - title: "##{image.id} - #{tag_list(image)} - Derpibooru", + title: "##{image.id} - #{tag_list(image)} - #{site_name}", author_name: artist_tags(image.tags), author_url: image_first_source(image), - provider_name: "Derpibooru", + provider_name: site_name, provider_url: PhilomenaWeb.Endpoint.url(), # 2 hours cache_age: 7200, @@ -29,10 +31,10 @@ defmodule PhilomenaWeb.Api.Json.OembedView do url: ImageView.pretty_url(image, true, false), width: image.image_width, height: image.image_height, - derpibooru_id: image.id, - derpibooru_score: image.score, - derpibooru_comments: image.comments_count, - derpibooru_tags: Enum.map(image.tags, & &1.name) + philomena_id: image.id, + philomena_score: image.score, + philomena_comments: image.comments_count, + philomena_tags: Enum.map(image.tags, & &1.name) } end diff --git a/lib/philomena_web/views/app_view.ex b/lib/philomena_web/views/app_view.ex index 61aa5ef86..fc75a0c07 100644 --- a/lib/philomena_web/views/app_view.ex +++ b/lib/philomena_web/views/app_view.ex @@ -259,4 +259,17 @@ defmodule PhilomenaWeb.AppView do def get_flash(%{assigns: %{flash: nil}}, _key), do: %{} def get_flash(%{assigns: %{flash: flash}}, key), do: Phoenix.Flash.get(flash, key) def get_flash(_, _key), do: %{} + + @spec env_cache(key :: atom(), generator :: (-> any())) :: any() + def env_cache(key, generator) do + case Application.get_env(:philomena, key) do + nil -> + value = generator.() + Application.put_env(:philomena, key, value) + value + + value -> + value + end + end end diff --git a/lib/philomena_web/views/avatar_generator_view.ex b/lib/philomena_web/views/avatar_generator_view.ex index 1953dce5b..645e4eb15 100644 --- a/lib/philomena_web/views/avatar_generator_view.ex +++ b/lib/philomena_web/views/avatar_generator_view.ex @@ -2,7 +2,9 @@ defmodule PhilomenaWeb.AvatarGeneratorView do use PhilomenaWeb, :view import Bitwise - alias Philomena.Config + alias Philomena.Avatars + + # todo: debranding def generated_avatar(displayed_name) do config = config() @@ -16,8 +18,8 @@ defmodule PhilomenaWeb.AvatarGeneratorView do {value, value} end) - # Set species - {species, rand} = at(species(config), rand) + # Set kind (race, species, etc) + {kind, rand} = at(kinds(config), rand) # Set the ranges for the colors we are going to make color_range = 128 @@ -25,30 +27,32 @@ defmodule PhilomenaWeb.AvatarGeneratorView do {body_r, body_g, body_b, rand} = rgb(0..color_range, color_brightness, rand) {hair_r, hair_g, hair_b, rand} = rgb(0..color_range, color_brightness, rand) - {style_hr, _rand} = at(all_species(hair_shapes(config), species), rand) + {style_hr, _rand} = at(all_kinds(hair_shapes(config), kind), rand) # Creates bounded hex color strings - color_bd = format("~2.16.0B~2.16.0B~2.16.0B", [body_r, body_g, body_b]) - color_hr = format("~2.16.0B~2.16.0B~2.16.0B", [hair_r, hair_g, hair_b]) + color_primary = format("~2.16.0B~2.16.0B~2.16.0B", [body_r, body_g, body_b]) + color_secondary = format("~2.16.0B~2.16.0B~2.16.0B", [hair_r, hair_g, hair_b]) # Make a character - avatar_svg(config, color_bd, color_hr, species, style_hr) + avatar_svg(config, color_primary, color_secondary, kind, style_hr) end # Build the final SVG for the character. # # Inputs to raw/1 are not user-generated. # sobelow_skip ["XSS.Raw"] - defp avatar_svg(config, color_bd, color_hr, species, style_hr) do + defp avatar_svg(config, color_primary, color_secondary, kind, style_hr) do [ - header(config), + "", background(config), - for_species(tail_shapes(config), species)["shape"] |> String.replace("HAIR_FILL", color_hr), - for_species(body_shapes(config), species)["shape"] |> String.replace("BODY_FILL", color_bd), - style_hr["shape"] |> String.replace("HAIR_FILL", color_hr), - all_species(extra_shapes(config), species) - |> Enum.map(&String.replace(&1["shape"], "BODY_FILL", color_bd)), - footer(config) + for_kind(tail_shapes(config), kind)["shape"] + |> String.replace("SECONDARY_COLOR", color_secondary), + for_kind(body_shapes(config), kind)["shape"] + |> String.replace("PRIMARY_COLOR", color_primary), + style_hr["shape"] |> String.replace("SECONDARY_COLOR", color_secondary), + all_kinds(extra_shapes(config), kind) + |> Enum.map(&String.replace(&1["shape"], "PRIMARY_COLOR", color_primary)), + "" ] |> List.flatten() |> Enum.map(&raw/1) @@ -83,14 +87,14 @@ defmodule PhilomenaWeb.AvatarGeneratorView do {Enum.at(list, position), rest} end - defp for_species(styles, species), do: hd(all_species(styles, species)) + defp for_kind(styles, kind), do: hd(all_kinds(styles, kind)) - defp all_species(styles, species), - do: Enum.filter(styles, &Enum.member?(&1["species"], species)) + defp all_kinds(styles, kind), + do: Enum.filter(styles, &Enum.member?(&1["kinds"], kind)) defp format(format_string, args), do: to_string(:io_lib.format(format_string, args)) - defp species(%{"species" => species}), do: species + defp kinds(%{"kinds" => kinds}), do: kinds defp header(%{"header" => header}), do: header defp background(%{"background" => background}), do: background defp tail_shapes(%{"tail_shapes" => tail_shapes}), do: tail_shapes @@ -99,5 +103,14 @@ defmodule PhilomenaWeb.AvatarGeneratorView do defp extra_shapes(%{"extra_shapes" => extra_shapes}), do: extra_shapes defp footer(%{"footer" => footer}), do: footer - defp config, do: Config.get(:avatar) + def config() do + env_cache(:avatar_config, fn -> + %{ + "kinds" => Avatars.get_kinds(), + "parts" => Avatars.get_parts(), + "shapes" => Avatars.get_shapes(), + "shape_kinds" => Avatars.get_shape_kinds() + } + end) + end end diff --git a/lib/philomena_web/views/commission_view.ex b/lib/philomena_web/views/commission_view.ex index 3d4ae339d..ad6ced361 100644 --- a/lib/philomena_web/views/commission_view.ex +++ b/lib/philomena_web/views/commission_view.ex @@ -2,7 +2,8 @@ defmodule PhilomenaWeb.CommissionView do use PhilomenaWeb, :view alias Philomena.Commissions.Commission + alias Philomena.Commissions.Item - def categories, do: [[key: "-", value: ""] | Commission.categories()] - def types, do: Commission.types() + def suggested_tags, do: [[key: "-", value: ""] | Commission.suggested_tags()] + def types, do: Item.types() end diff --git a/lib/philomena_web/views/layout_view.ex b/lib/philomena_web/views/layout_view.ex index 57e8cea8f..b4a6119f2 100644 --- a/lib/philomena_web/views/layout_view.ex +++ b/lib/philomena_web/views/layout_view.ex @@ -3,8 +3,9 @@ defmodule PhilomenaWeb.LayoutView do import PhilomenaWeb.Config alias PhilomenaWeb.ImageView - alias Philomena.Config alias Philomena.Users.User + alias Philomena.Configs + alias Philomena.FooterLinks alias Plug.Conn @themes User.themes() @@ -25,7 +26,7 @@ defmodule PhilomenaWeb.LayoutView do end def hide_version do - Application.get_env(:philomena, :hide_version) == "true" + Configs.get("hide_version") end def cdn_host do @@ -75,7 +76,8 @@ defmodule PhilomenaWeb.LayoutView do fancy_tag_upload: if(user, do: user.fancy_tag_field_on_upload, else: "true") |> to_string(), interactions: JSON.encode!(interactions), ignored_tag_list: JSON.encode!(ignored_tag_list(conn.assigns[:tags])), - hide_staff_tools: conn.cookies["hide_staff_tools"] |> to_string() + hide_staff_tools: conn.cookies["hide_staff_tools"] |> to_string(), + minimum_tags: Configs.get("minimum_tags") ] data = Keyword.merge(data, extra) @@ -84,7 +86,7 @@ defmodule PhilomenaWeb.LayoutView do end def footer_data do - Config.get(:footer) + env_cache(:footer_data, &FooterLinks.get_footer_data/0) end def stylesheet_path(conn, %{theme: theme}) @@ -92,13 +94,13 @@ defmodule PhilomenaWeb.LayoutView do do: static_path(conn, "/css/#{theme}.css") def stylesheet_path(_conn, _user), - do: ~p"/css/dark-blue.css" + do: ~p"/css/#{Configs.get("default_theme") <> ".css"}" def light_stylesheet_path(_conn), - do: ~p"/css/light-blue.css" + do: ~p"/css/#{Configs.get("default_light_theme") <> ".css"}" def theme_name(%{theme: theme}), do: theme - def theme_name(_user), do: "dark-blue" + def theme_name(_user), do: Configs.get("default_theme") def hide_staff_tools_attribute(conn), do: if(conn.cookies["hide_staff_tools"] == "true", do: "true", else: "false") diff --git a/lib/philomena_web/views/opensearch_view.ex b/lib/philomena_web/views/opensearch_view.ex new file mode 100644 index 000000000..ed2259eae --- /dev/null +++ b/lib/philomena_web/views/opensearch_view.ex @@ -0,0 +1,8 @@ +defmodule PhilomenaWeb.OpensearchView do + use PhilomenaWeb, :view + + alias Philomena.Configs + + def site_name(), do: Configs.get("site_name") + def site_url(), do: Configs.get("site_url") +end diff --git a/lib/philomena_web/views/profile/commission/item_view.ex b/lib/philomena_web/views/profile/commission/item_view.ex index 9f8d88af7..76691804d 100644 --- a/lib/philomena_web/views/profile/commission/item_view.ex +++ b/lib/philomena_web/views/profile/commission/item_view.ex @@ -1,7 +1,7 @@ defmodule PhilomenaWeb.Profile.Commission.ItemView do use PhilomenaWeb, :view - alias Philomena.Commissions.Commission + alias Philomena.Commissions.Item - def types, do: Commission.types() + def types, do: Item.types() end diff --git a/lib/philomena_web/views/profile/commission_view.ex b/lib/philomena_web/views/profile/commission_view.ex index c8e4a82f9..17e2bf33f 100644 --- a/lib/philomena_web/views/profile/commission_view.ex +++ b/lib/philomena_web/views/profile/commission_view.ex @@ -3,7 +3,7 @@ defmodule PhilomenaWeb.Profile.CommissionView do alias Philomena.Commissions.Commission - def categories, do: Commission.categories() + def suggested_tags, do: Commission.suggested_tags() def current?(%{id: id}, %{id: id}), do: true def current?(_user1, _user2), do: false diff --git a/lib/philomena_web/views/report_view.ex b/lib/philomena_web/views/report_view.ex index 2d18c1d74..93b5542a5 100644 --- a/lib/philomena_web/views/report_view.ex +++ b/lib/philomena_web/views/report_view.ex @@ -11,14 +11,15 @@ defmodule PhilomenaWeb.ReportView do import Ecto.Changeset + # Todo: debranding: move to config or db def report_categories do [ - "Rule #0: Namecalling, trolling, discrimination": "Rule #0", + "Rule #0: Malicious behavior or discrimination": "Rule #0", "Rule #1: DNP, content theft, pay content, trace/bad edit": "Rule #1", "Rule #2: Bad tagging/sourcing": "Rule #2", - "Rule #3: Image not MLP-related/obligatory pony": "Rule #3", - "Rule #4: Whining about filterable content": "Rule #4", - "Rule #5: Underage+human/anthro-looking porn": "Rule #5", + "Rule #3: Unrelated image": "Rule #3", + "Rule #4: Complaining about filterable content": "Rule #4", + "Rule #5: Illegal or forbidden content": "Rule #5", "Rule #6: Spam, off-topic, or general site abuse": "Rule #6", "Rule #7: Above topic rating (NOT swear words)": "Rule #7", "Rule #8: Privacy violation": "Rule #8", diff --git a/lib/philomena_web/views/tag_view.ex b/lib/philomena_web/views/tag_view.ex index ef860d711..b492f4ad7 100644 --- a/lib/philomena_web/views/tag_view.ex +++ b/lib/philomena_web/views/tag_view.ex @@ -2,9 +2,9 @@ defmodule PhilomenaWeb.TagView do use PhilomenaWeb, :view # this is bad practice, don't copy this. - alias Philomena.Config alias PhilomenaQuery.Search alias Philomena.Tags.Tag + alias Philomena.QuickTags alias Philomena.Repo alias PhilomenaWeb.ImageScope import Ecto.Query @@ -32,20 +32,30 @@ defmodule PhilomenaWeb.TagView do end def quick_tags(conn) do - case Application.get_env(:philomena, :quick_tags) do - nil -> - quick_tags = - Config.get(:quick_tag_table) - |> lookup_quick_tags() - |> render_quick_tags(conn) + env_cache(:quick_tags, fn -> + {tabs_with_data, tag_names, shorthands} = QuickTags.build_quick_tag_structure() - Application.put_env(:philomena, :quick_tags, quick_tags) + # Look up tag objects + tags = tags_indexed_by_name(tag_names) - quick_tags + # Build shipping data with implied tags + tabs_with_shipping = + tabs_with_data + |> Enum.map(fn tab_data -> + if tab_data.mode == :shipping and tab_data.shippings != [] do + shipping_config = List.first(tab_data.shippings) - quick_tags -> - quick_tags - end + implied_tags = + implied_by_multitag(shipping_config.implying, shipping_config.not_implying) + + Map.put(tab_data, :shipping_tags, implied_tags) + else + tab_data + end + end) + + render_quick_tags({tabs_with_shipping, tags, shorthands}, conn) + end) end def tab_class(0), do: "selected" @@ -85,31 +95,13 @@ defmodule PhilomenaWeb.TagView do defp title([]), do: nil defp title(descriptions), do: Enum.join(descriptions, "\n") - defp lookup_quick_tags(%{"tabs" => tabs, "tab_modes" => tab_modes} = data) do - tags = - tabs - |> Enum.flat_map(&names_in_tab(tab_modes[&1], data[&1])) - |> tags_indexed_by_name() - - shipping = - tabs - |> Enum.filter(&(tab_modes[&1] == "shipping")) - |> Map.new(fn tab -> - sd = data[tab] - - {tab, implied_by_multitag(sd["implying"], sd["not_implying"])} - end) - - {tags, shipping, data} - end - # This is a rendered template, so raw/1 has no effect on safety # sobelow_skip ["XSS.Raw"] - defp render_quick_tags({tags, shipping, data}, conn) do + defp render_quick_tags({tabs_with_data, tags, shorthands}, conn) do render(PhilomenaWeb.TagView, "_quick_tag_table.html", + tabs_with_data: tabs_with_data, tags: tags, - shipping: shipping, - data: data, + shorthands: shorthands, conn: conn ) |> Phoenix.HTML.Safe.to_iodata() diff --git a/lib/philomena_web/views/user_attribution_view.ex b/lib/philomena_web/views/user_attribution_view.ex index 3a377be22..846fab97f 100644 --- a/lib/philomena_web/views/user_attribution_view.ex +++ b/lib/philomena_web/views/user_attribution_view.ex @@ -2,6 +2,7 @@ defmodule PhilomenaWeb.UserAttributionView do use PhilomenaWeb, :view alias Philomena.Attribution + alias Philomena.Configs alias PhilomenaWeb.AvatarGeneratorView def anonymous?(object) do @@ -42,7 +43,7 @@ defmodule PhilomenaWeb.UserAttributionView do if not is_nil(object.user) and reveal_anon? do "#{object.user.name} (##{hash}, hidden)" else - "Background Pony ##{hash}" + "#{Configs.get("anonymous_name")} ##{hash}" end end diff --git a/priv/repo/migrations/20251014181016_create_footer_links.exs b/priv/repo/migrations/20251014181016_create_footer_links.exs new file mode 100644 index 000000000..0cb3df07f --- /dev/null +++ b/priv/repo/migrations/20251014181016_create_footer_links.exs @@ -0,0 +1,24 @@ +defmodule Philomena.Repo.Migrations.CreateFooterLinks do + use Ecto.Migration + + def change do + create table(:footer_categories) do + add :title, :string, null: false + add :position, :integer, null: false + end + + create table(:footer_links) do + add :footer_category_id, + references(:footer_categories, on_update: :update_all, on_delete: :delete_all), + null: false + + add :title, :string, null: false + add :url, :string, null: false + add :position, :integer, null: false + add :bold, :boolean, null: false, default: false + add :new_tab, :boolean, null: false, default: false + end + + create index(:footer_categories, [:position]) + end +end diff --git a/priv/repo/migrations/20251014181033_create_quick_tags.exs b/priv/repo/migrations/20251014181033_create_quick_tags.exs new file mode 100644 index 000000000..56483bc71 --- /dev/null +++ b/priv/repo/migrations/20251014181033_create_quick_tags.exs @@ -0,0 +1,58 @@ +defmodule Philomena.Repo.Migrations.CreateQuickTags do + use Ecto.Migration + + def change do + create table(:quick_tag_tabs) do + add :title, :string, null: false + add :position, :integer, null: false + end + + create table(:default_quick_tags) do + add :quick_tag_tab_id, + references(:quick_tag_tabs, on_update: :update_all, on_delete: :delete_all), + null: false + + add :category, :string, null: false + add :tags, {:array, :string}, null: false + end + + create table(:shorthand_quick_tag_categories) do + add :quick_tag_tab_id, + references(:quick_tag_tabs, on_update: :update_all, on_delete: :delete_all), + null: false + + add :category, :string, null: false + end + + create table(:shorthand_quick_tags) do + add :shorthand_quick_tag_category_id, + references(:shorthand_quick_tag_categories, + on_update: :update_all, + on_delete: :delete_all + ), + null: false + + add :shorthand, :string, null: false + add :tag, :string, null: false + end + + create table(:shipping_quick_tags) do + add :quick_tag_tab_id, + references(:quick_tag_tabs, on_update: :update_all, on_delete: :delete_all), + null: false + + add :category, :string, null: false + add :implying, {:array, :string}, default: [] + add :not_implying, {:array, :string}, default: [] + end + + create table(:season_quick_tags) do + add :quick_tag_tab_id, + references(:quick_tag_tabs, on_update: :update_all, on_delete: :delete_all), + null: false + + add :episode, :integer, null: false + add :tag, :string, null: false + end + end +end diff --git a/priv/repo/migrations/20251014181203_create_avatar_shapes.exs b/priv/repo/migrations/20251014181203_create_avatar_shapes.exs new file mode 100644 index 000000000..748fff18c --- /dev/null +++ b/priv/repo/migrations/20251014181203_create_avatar_shapes.exs @@ -0,0 +1,33 @@ +defmodule Philomena.Repo.Migrations.CreateAvatarShapes do + use Ecto.Migration + + def change do + create table(:avatar_parts) do + add :name, :string, null: false + add :priority, :integer, default: 1, null: false + end + + create table(:avatar_kinds) do + add :name, :string, null: false + end + + create table(:avatar_shapes) do + add :avatar_part_id, + references(:avatar_parts, on_update: :update_all, on_delete: :delete_all), + null: false + + add :shape, :string, null: false + add :any_kind, :boolean, default: false, null: false + end + + create table(:avatar_shape_kinds) do + add :avatar_shape_id, + references(:avatar_shapes, on_update: :update_all, on_delete: :delete_all), + null: false + + add :avatar_kind_id, + references(:avatar_kinds, on_update: :update_all, on_delete: :delete_all), + null: false + end + end +end diff --git a/priv/repo/migrations/20251014191009_create_site_config.exs b/priv/repo/migrations/20251014191009_create_site_config.exs new file mode 100644 index 000000000..32d3d7d9c --- /dev/null +++ b/priv/repo/migrations/20251014191009_create_site_config.exs @@ -0,0 +1,17 @@ +defmodule Philomena.Repo.Migrations.CreateSiteConfig do + use Ecto.Migration + + def change do + create table(:configs) do + add :key, :string, null: false + add :value, :string, null: false + end + + create table(:system_images) do + add :key, :string, null: false + end + + create index(:configs, [:key], unique: true) + create index(:system_images, [:key], unique: true) + end +end diff --git a/priv/repo/migrations/20251018172400_add_invalid_to_tags.exs b/priv/repo/migrations/20251018172400_add_invalid_to_tags.exs new file mode 100644 index 000000000..afebb332d --- /dev/null +++ b/priv/repo/migrations/20251018172400_add_invalid_to_tags.exs @@ -0,0 +1,9 @@ +defmodule Philomena.Repo.Migrations.AddInvalidToTags do + use Ecto.Migration + + def change do + alter table("tags") do + add :invalid, :boolean, default: false, null: false + end + end +end diff --git a/priv/repo/migrations/20251103100906_add_commission_taggings.exs b/priv/repo/migrations/20251103100906_add_commission_taggings.exs new file mode 100644 index 000000000..922bcdd99 --- /dev/null +++ b/priv/repo/migrations/20251103100906_add_commission_taggings.exs @@ -0,0 +1,50 @@ +defmodule Philomena.Repo.Migrations.AddCommissionTaggings do + use Ecto.Migration + + def change do + # First create commission taggings + create table("commission_taggings", primary_key: false) do + add :commission_id, references(:commissions, on_delete: :delete_all), primary_key: true + add :tag_id, references(:tags, on_delete: :delete_all), primary_key: true + end + + create unique_index(:commission_taggings, [:commission_id, :tag_id]) + + # Add a column to indicate if the artist is accepting requests + change table("commissions") do + add :accepting_requests, :boolean, default: false, null: false + end + + # Then convert existing categories to tags + execute(""" + WITH cs AS (SELECT c.id, unnest(c.categories) AS category FROM commissions c), + ct AS (SELECT cs.id, unnest( + CASE category + WHEN 'Anthro' THEN ARRAY['anthro'] + WHEN 'Comics' THEN ARRAY['comic'] + WHEN 'Fetish Art' THEN ARRAY['fetish'] + WHEN 'Human and EqG' THEN ARRAY['human', 'humanoid'] + WHEN 'NSFW' THEN ARRAY['explicit', 'questionable', 'suggestive'] + WHEN 'Original Characters' THEN ARRAY['oc'] + WHEN 'Original Species' THEN ARRAY['original species'] + WHEN 'Pony' THEN ARRAY['my little pony'] + WHEN 'Safe' THEN ARRAY['safe'] + WHEN 'Shipping' THEN ARRAY['shipping'] + WHEN 'Violence and Gore' THEN ARRAY['gore', 'violence'] + WHEN 'Franchise Fan Art' THEN ARRAY['fanart'] + ) AS tag_name) FROM cs, + t AS (SELECT ct.id AS commission_id, t.id AS tag_id FROM ct INNER JOIN tags t ON t.name=ct.tag_name) + INSERT INTO commission_taggings (commission_id, tag_id) + SELECT DISTINCT commission_id, tag_id FROM t; + """) + + # And set accepting_requests based on whether "Requests" category was present + execute(""" + UPDATE commissions + SET accepting_requests = true + WHERE 'Requests' = ANY(categories) + """) + + # TODO (release 2.0): cleanup categories column + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 31f7e92cb..2068d42ac 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -24,7 +24,10 @@ alias Philomena.{ Tags.Tag, TagChanges.TagChange, Users.User, - StaticPages.StaticPage + StaticPages.StaticPage, + FooterLinks, + QuickTags, + Avatars } alias PhilomenaQuery.Search @@ -33,6 +36,16 @@ alias Philomena.Tags alias Philomena.Filters import Ecto.Query +defmodule Philomena.SeedLoader do + def load_resource(res) do + "priv/repo/seeds/data/#{res}.json" + |> File.read!() + |> JSON.decode!() + end +end + +alias Philomena.SeedLoader + IO.puts("---- Creating search indices") for model <- [Image, Comment, Gallery, Tag, TagChange, Post, Report, Filter] do @@ -40,22 +53,191 @@ for model <- [Image, Comment, Gallery, Tag, TagChange, Post, Report, Filter] do Search.create_index!(model) end -resources = - "priv/repo/seeds.json" - |> File.read!() - |> JSON.decode!() - IO.puts("---- Generating rating tags") -for tag_name <- resources["rating_tags"] do +for tag_name <- SeedLoader.load_resource("rating_tags") do %Tag{category: "rating"} |> Tag.creation_changeset(%{name: tag_name}) |> Repo.insert(on_conflict: :nothing) end +IO.puts("---- Generating invalid tags") + +for tag_name <- SeedLoader.load_resource("invalid_tags") do + %Tag{invalid: true} + |> Tag.creation_changeset(%{name: tag_name}) + |> Repo.insert(on_conflict: :nothing) +end + +IO.puts("---- Generating footer links and categories") + +footer_data = SeedLoader.load_resource("footer") + +for {category, cat_index} <- Enum.with_index(footer_data["categories"]) do + {:ok, footer_category} = + %FooterLinks.Category{title: category, position: cat_index} + |> Repo.insert(on_conflict: :nothing) + + for {link_def, link_index} <- Enum.with_index(footer_data[category]) do + %FooterLinks.Link{} + |> FooterLinks.Link.changeset(%{ + title: link_def["title"], + url: link_def["url"], + bold: link_def["bold"] || false, + new_tab: link_def["new_tab"] || false, + position: link_index, + footer_category_id: footer_category.id + }) + |> Repo.insert(on_conflict: :nothing) + end +end + +IO.puts("---- Generating quick tags table") + +quick_tags = SeedLoader.load_resource("quick_tags") + +IO.puts(" ...tabs") + +quick_tag_tabs = + quick_tags["quick_tag_tabs"] + |> Enum.map(fn tab -> + {:ok, tab_record} = + %QuickTags.Tab{title: tab["title"], position: tab["position"]} + |> Repo.insert(on_conflict: :nothing) + + {tab["title"], tab_record} + end) + |> Map.new() + +IO.puts(" ...shorthand categories") + +shorthand_categories = + quick_tags["shorthand_quick_tag_categories"] + |> Enum.map(fn cat -> + {:ok, cat_record} = + %QuickTags.ShorthandCategory{ + category: cat["category"], + quick_tag_tab_id: quick_tag_tabs[cat["tab_title"]] + } + |> Repo.insert(on_conflict: :nothing) + + {cat["category"], cat_record} + end) + |> Map.new() + +IO.puts(" ...default quick tags") + +for tag <- quick_tags["default_quick_tags"] do + {:ok, _} = + %QuickTags.Default{ + category: tag["category"], + tags: tag["tags"], + quick_tag_tab_id: quick_tag_tabs[tag["tab_title"]].id + } + |> Repo.insert(on_conflict: :nothing) +end + +IO.puts(" ...shorthand quick tags") + +for tag <- quick_tags["shorthand_quick_tags"] do + {:ok, _} = + %QuickTags.Shorthand{ + shorthand: tag["shorthand"], + tag: tag["tag"], + shorthand_quick_tag_category_id: shorthand_categories[tag["category"]].id + } + |> Repo.insert(on_conflict: :nothing) +end + +IO.puts(" ...season quick tags") + +for tag <- quick_tags["season_quick_tags"] do + {:ok, _} = + %QuickTags.Season{ + episode: tag["episode"], + tag: tag["tag"], + quick_tag_tab_id: quick_tag_tabs[tag["tab_title"]].id + } + |> Repo.insert(on_conflict: :nothing) +end + +IO.puts(" ...shipping quick tags") + +for tag <- quick_tags["shipping_quick_tags"] do + {:ok, _} = + %QuickTags.Shipping{ + category: tag["category"], + implying: tag["implying"], + not_implying: tag["not_implying"], + quick_tag_tab_id: quick_tag_tabs[tag["tab_title"]].id + } + |> Repo.insert(on_conflict: :nothing) +end + +IO.puts("---- Generating avatar data") + +avatar_data = SeedLoader.load_resource("avatar") + +IO.puts(" ... parts") + +avatar_parts = + Enum.with_index(avatar_data["parts"]) + |> Enum.map(fn {part, index} -> + {:ok, record} = + %Avatars.Part{} + |> Avatars.Part.changeset(%{ + name: part, + priority: index + }) + |> Repo.insert(on_conflict: :nothing) + + {part, record} + end) + |> Map.new() + +IO.puts(" ...kinds") + +avatar_kinds = + Enum.with_index(avatar_data["kinds"]) + |> Enum.map(fn {kind, index} -> + {:ok, record} = + %Avatars.Kind{} + |> Avatars.Kind.changeset(%{ + name: kind + }) + |> Repo.insert(on_conflict: :nothing) + + {kind, record} + end) + |> Map.new() + +IO.puts(" ...shapes") + +for {part, shapes} <- avatar_data["shapes"] do + for shape_def <- shapes do + {:ok, shape_record} = + %Avatars.Shape{} + |> Avatars.Shape.changeset(%{ + avatar_part_id: avatar_parts[part].id, + shape: shape_def["shape"], + any_kind: shape_def["any_kind"] || false + }) + |> Repo.insert(on_conflict: :nothing) + + for kind <- shape_def["kinds"] do + %Avatars.ShapeKind{} + |> Avatars.ShapeKind.changeset(%{ + avatar_shape_id: shape_record.id, + avatar_kind_id: avatar_kinds[kind].id + }) + |> Repo.insert(on_conflict: :nothing) + end + end +end + IO.puts("---- Generating system filters") -for filter_def <- resources["system_filters"] do +for filter_def <- SeedLoader.load_resource("system_filters") do spoilered_tag_list = Enum.join(filter_def["spoilered"], ",") hidden_tag_list = Enum.join(filter_def["hidden"], ",") @@ -78,7 +260,7 @@ end IO.puts("---- Generating forums") -for forum_def <- resources["forums"] do +for forum_def <- SeedLoader.load_resource("forums") do %Forum{} |> Forum.changeset(forum_def) |> Repo.insert(on_conflict: :nothing) @@ -86,7 +268,7 @@ end IO.puts("---- Generating users") -for user_def <- resources["users"] do +for user_def <- SeedLoader.load_resource("users") do {:ok, user} = Users.register_user(user_def) user @@ -98,7 +280,7 @@ end IO.puts("---- Generating roles") -for role_def <- resources["roles"] do +for role_def <- SeedLoader.load_resource("roles") do %Role{name: role_def["name"], resource_type: role_def["resource_type"]} |> Role.changeset(%{}) |> Repo.insert(on_conflict: :nothing) @@ -106,8 +288,12 @@ end IO.puts("---- Generating static pages") -for page_def <- resources["pages"] do - %StaticPage{title: page_def["title"], slug: page_def["slug"], body: page_def["body"]} +for page_def <- SeedLoader.load_resource("pages") do + %StaticPage{ + title: page_def["title"], + slug: page_def["slug"], + body: File.read!("priv/repo/seeds/data/pages/#{page_def["slug"]}.md") + } |> StaticPage.changeset(%{}) |> Repo.insert(on_conflict: :nothing) end diff --git a/priv/repo/seeds.json b/priv/repo/seeds.json deleted file mode 100644 index 546d47069..000000000 --- a/priv/repo/seeds.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "system_filters": [ - { - "name": "Default", - "description": "The site's default filter.", - "hidden": ["explicit", "grotesque"], - "spoilered": ["questionable"] - }, - { - "name": "Everything", - "description": "This filter won't filter out anything at all.", - "hidden": [], - "spoilered": [] - } - ], - "forums": [ - { - "name": "General Discussion", - "short_name": "dis", - "description": "This is a discussion forum for everything unrelated to the show or other forums", - "access_level": "normal" - }, - { - "name": "Shows and Movies", - "short_name": "pony", - "description": "Discuss TV shows and movies, as well as their characters and theories.", - "access_level": "normal" - }, - { - "name": "Site and Policy", - "short_name": "meta", - "description": "For site discussion and policy discussion", - "access_level": "normal" - }, - { - "name": "Art Chat", - "short_name": "art", - "description": "Discuss art of any form, and share techniques and tips", - "access_level": "normal" - }, - { - "name": "Roleplaying", - "short_name": "rp", - "description": "Roleplaying forum, in which people play roles", - "access_level": "normal" - }, - { - "name": "Site Assistant Discussion", - "short_name": "helper", - "description": "Restricted - Assistants and Staff", - "access_level": "assistant" - }, - { - "name": "Moderation Discussion", - "short_name": "mod", - "description": "Restricted - Staff only", - "access_level": "staff" - } - ], - "users": [ - { - "name": "Administrator", - "email": "admin@example.com", - "password": "philomena123", - "role": "admin" - } - ], - "rating_tags": ["safe", "suggestive", "questionable", "explicit", "semi-grimdark", "grimdark", "grotesque"], - "roles": [ - { "name": "moderator", "resource_type": "Image" }, - { "name": "moderator", "resource_type": "DuplicateReport" }, - { "name": "moderator", "resource_type": "Comment" }, - { "name": "moderator", "resource_type": "Tag" }, - { "name": "moderator", "resource_type": "ArtistLink" }, - { "name": "admin", "resource_type": "Tag" }, - { "name": "moderator", "resource_type": "User" }, - { "name": "admin", "resource_type": "SiteNotice" }, - { "name": "admin", "resource_type": "Badge" }, - { "name": "admin", "resource_type": "Role" }, - { "name": "batch_update", "resource_type": "Tag" }, - { "name": "moderator", "resource_type": "Topic" }, - { "name": "admin", "resource_type": "Advert" }, - { "name": "admin", "resource_type": "StaticPage" }, - { "name": "admin", "resource_type": "Image" } - ], - "pages": [] -} diff --git a/priv/repo/seeds/data/avatar.json b/priv/repo/seeds/data/avatar.json new file mode 100644 index 000000000..4769fe7b8 --- /dev/null +++ b/priv/repo/seeds/data/avatar.json @@ -0,0 +1,128 @@ +{ + "parts": ["background", "body", "tail", "hair", "extra"], + "kinds": ["unicorn", "pegasus", "earthpony"], + "shapes": { + "background": [ + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + } + ], + "body": [ + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + } + ], + "tail": [ + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + } + ], + "hair": [ + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + } + ], + "extra": [ + { + "shape": "", + "kinds": ["unicorn"] + }, + { + "shape": "", + "kinds": ["pegasus"] + }, + { + "shape": "", + "kinds": ["unicorn", "pegasus", "earthpony"] + } + ] + } +} diff --git a/priv/repo/seeds/data/development/comments.json b/priv/repo/seeds/data/development/comments.json new file mode 100644 index 000000000..9c1241367 --- /dev/null +++ b/priv/repo/seeds/data/development/comments.json @@ -0,0 +1,6 @@ +[ + "bold is **bold**, italic is _italic_, spoiler is ||spoiler||, code is `code`, underline is __underline__, strike is ~~strike~~, sup is ^sup^, sub is %sub%.", + "inline embedded thumbnails (tsp): >>1t >>1s >>1p", + "embedded image inside a spoiler: ||who needs it anyway >>1s||", + "spoilers inside of a table\n\nHello | World\n--- | ---:\n`||cool beans!||` | ||cool beans!||" +] diff --git a/priv/repo/seeds/data/development/forum_posts.json b/priv/repo/seeds/data/development/forum_posts.json new file mode 100644 index 000000000..3428a2b17 --- /dev/null +++ b/priv/repo/seeds/data/development/forum_posts.json @@ -0,0 +1,24 @@ +[ + { + "forum": "dis", + "topics": [ + { + "title": "Example Topic", + "posts": ["example post", "yet another example post"] + }, + { + "title": "Second Example Topic", + "posts": ["post", "post 2"] + } + ] + }, + { + "forum": "art", + "topics": [ + { + "title": "Embedded Images", + "posts": [">>1t >>1s >>1p", ">>1", "non-existent: >>1000t >>1000s >>1000p >>1000"] + } + ] + } +] diff --git a/priv/repo/seeds/data/development/images.json b/priv/repo/seeds/data/development/images.json new file mode 100644 index 000000000..1e56070cd --- /dev/null +++ b/priv/repo/seeds/data/development/images.json @@ -0,0 +1,33 @@ +[ + { + "url": "https://derpicdn.net/img/2015/9/26/988000/thumb.gif", + "sources": ["https://derpibooru.org/988000"], + "description": "Fairly large GIF (~23MB), use to test WebM stuff.", + "tag_input": "alicorn, angry, animated, art, artist:assasinmonkey, artist:equum_amici, badass, barrier, crying, dark, epic, female, fight, force field, glare, glow, good vs evil, lord tirek, low angle, magic, mare, messy mane, metal as fuck, perspective, plot, pony, raised hoof, safe, size difference, spread wings, stomping, twilight's kingdom, twilight sparkle, twilight sparkle (alicorn), twilight vs tirek, underhoof" + }, + { + "url": "https://derpicdn.net/img/2012/1/2/25/large.png", + "sources": ["https://derpibooru.org/25"], + "tag_input": "artist:moe, canterlot, castle, cliff, cloud, detailed background, fog, forest, grass, mountain, mountain range, nature, no pony, outdoors, path, river, safe, scenery, scenery porn, signature, source needed, sunset, technical advanced, town, tree, useless source url, water, waterfall, widescreen, wood" + }, + { + "url": "https://derpicdn.net/img/2018/6/28/1767886/full.webm", + "sources": ["http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309"], + "tag_input": "3d, animated, architecture, artist:hydrusbeta, castle, cloud, crystal empire, crystal palace, flag, flag waving, no pony, no sound, safe, scenery, webm" + }, + { + "url": "https://derpicdn.net/img/view/2015/2/19/832750.jpg", + "sources": ["http://sovietrussianbrony.tumblr.com/post/111504505079/this-image-actually-took-me-ages-to-edit-the"], + "tag_input": "artist:rhads, artist:the sexy assistant, canterlot, cloud, cloudsdale, cloudy, edit, lens flare, no pony, ponyville, rainbow, river, safe, scenery, sweet apple acres" + }, + { + "url": "https://derpicdn.net/img/view/2016/3/17/1110529.jpg", + "sources": ["https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926"], + "tag_input": "artist:devinian, aurora crystialis, bridge, cloud, crepuscular rays, crystal empire, crystal palace, edit, flower, forest, grass, log, mountain, no pony, river, road, safe, scenery, scenery porn, source needed, stars, sunset, swing, tree, wallpaper" + }, + { + "url": "https://derpicdn.net/img/view/2019/6/16/2067468.svg", + "sources": ["https://derpibooru.org/2067468"], + "tag_input": "artist:cheezedoodle96, babs seed, bloom and gloom, cutie mark, cutie mark only, no pony, safe, scissors, simple background, svg, .svg available, transparent background, vector" + } +] diff --git a/priv/repo/seeds/data/development/users.json b/priv/repo/seeds/data/development/users.json new file mode 100644 index 000000000..0cd9ff8c7 --- /dev/null +++ b/priv/repo/seeds/data/development/users.json @@ -0,0 +1,20 @@ +[ + { + "name": "Hot Pocket Consumer", + "email": "moderator@example.com", + "password": "philomena123", + "role": "moderator" + }, + { + "name": "Hoping For a Promotion", + "email": "assistant@example.com", + "password": "philomena123", + "role": "assistant" + }, + { + "name": "Pleb", + "email": "user@example.com", + "password": "philomena123", + "role": "user" + } +] diff --git a/config/footer.json b/priv/repo/seeds/data/footer.json similarity index 80% rename from config/footer.json rename to priv/repo/seeds/data/footer.json index 24102ca3d..acc756ab9 100644 --- a/config/footer.json +++ b/priv/repo/seeds/data/footer.json @@ -1,5 +1,5 @@ { - "cols": ["Site Resources", "Help & Information", "Community"], + "categories": ["Site Resources", "Help & Information", "Community"], "Site Resources": [ { "title": "Site Rules", @@ -36,9 +36,9 @@ "Help & Information": [ { "title": "Changelog", - "url": "https://github.com/derpibooru/philomena/commits/master", + "url": "https://github.com/philomena-dev/philomena/commits/master", "bold": true, - "target": "_blank" + "new_tab": true }, { "title": "FAQs", @@ -56,10 +56,6 @@ { "title": "Advertising", "url": "/pages/advertising" - }, - { - "title": "Onion Service", - "url": "/pages/onion" } ], "Community": [ @@ -84,11 +80,6 @@ { "title": "About", "url": "/pages/about" - }, - { - "title": "Twitter", - "url": "https://twitter.com/Derpibooru", - "target": "_blank" } ] } diff --git a/priv/repo/seeds/data/forums.json b/priv/repo/seeds/data/forums.json new file mode 100644 index 000000000..700b6c676 --- /dev/null +++ b/priv/repo/seeds/data/forums.json @@ -0,0 +1,44 @@ +[ + { + "name": "General Discussion", + "short_name": "dis", + "description": "This is a discussion forum for everything unrelated to the show or other forums", + "access_level": "normal" + }, + { + "name": "Shows and Movies", + "short_name": "movies", + "description": "Discuss TV shows and movies, as well as their characters and theories.", + "access_level": "normal" + }, + { + "name": "Site and Policy", + "short_name": "meta", + "description": "For site discussion and policy discussion", + "access_level": "normal" + }, + { + "name": "Art Chat", + "short_name": "art", + "description": "Discuss art of any form, and share techniques and tips", + "access_level": "normal" + }, + { + "name": "Roleplaying", + "short_name": "rp", + "description": "Roleplaying forum, in which people play roles", + "access_level": "normal" + }, + { + "name": "Site Assistant Discussion", + "short_name": "helper", + "description": "Restricted - Assistants and Staff", + "access_level": "assistant" + }, + { + "name": "Moderation Discussion", + "short_name": "mod", + "description": "Restricted - Staff only", + "access_level": "staff" + } +] diff --git a/priv/repo/seeds/data/invalid_tags.json b/priv/repo/seeds/data/invalid_tags.json new file mode 100644 index 000000000..8b2a30ac7 --- /dev/null +++ b/priv/repo/seeds/data/invalid_tags.json @@ -0,0 +1,18 @@ +[ + "tagme", + "tag me", + "not tagged", + "no tag", + "notag", + "notags", + "upvotes galore", + "downvotes galore", + "wall of faves", + "drama in the comments", + "drama in comments", + "tag needed", + "paywall", + "cringeworthy", + "solo oc", + "tag your shit" +] diff --git a/priv/repo/seeds/data/pages.json b/priv/repo/seeds/data/pages.json new file mode 100644 index 000000000..711de40bd --- /dev/null +++ b/priv/repo/seeds/data/pages.json @@ -0,0 +1,66 @@ +[ + { + "slug": "rules", + "title": "Site Rules" + }, + { + "slug": "privacy", + "title": "Privacy Policy" + }, + { + "slug": "tags", + "title": "Tag Guidelines" + }, + { + "slug": "uploading", + "title": "About Uploading" + }, + { + "slug": "spoilers", + "title": "Spoiler Guidelines" + }, + { + "slug": "takedowns", + "title": "Takedown Requests" + }, + { + "slug": "faq", + "title": "FAQs" + }, + { + "slug": "api", + "title": "API Docs" + }, + { + "slug": "shortcuts", + "title": "Keyboard Shortcuts" + }, + { + "slug": "advertising", + "title": "Advertising" + }, + { + "slug": "about", + "title": "About" + }, + { + "slug": "contact", + "title": "Contact" + }, + { + "slug": "donations", + "title": "Donations" + }, + { + "slug": "approval", + "title": "Approval Queue" + }, + { + "slug": "markdown", + "title": "Markdown Guide" + }, + { + "slug": "start", + "title": "Getting Started" + } +] diff --git a/priv/repo/seeds/data/pages/about.md b/priv/repo/seeds/data/pages/about.md new file mode 100644 index 000000000..c7d14814d --- /dev/null +++ b/priv/repo/seeds/data/pages/about.md @@ -0,0 +1,7 @@ +# About Philomena + +Philomena is a state of the art software for powering image boorus (image sharing/commenting/voting sites). At the forefront of its goals is to be the easiest-to-use and most intuitive imageboard software around. It is also designed to be easy to scale on a technical level, requiring less system resources than other booru software, saving you the hassle of getting big servers with serious hardware. + +It was initially written for [Derpibooru](https://derpibooru.org), the largest image sharing site aimed at the fans of the My Little Pony cartoon, from scratch with a view to making a really good web application for sharing images. It has since outgrown Derpibooru, and now powers many other image sharing sites, such as [Furbooru](https://furbooru.org). + +We hope you enjoy using this software! Please make sure to share any suggestions and report any issues you may find with it [in the issues section of our GitHub repository](https://github.com/philomena-dev/philomena/issues). diff --git a/priv/repo/seeds/data/pages/advertising.md b/priv/repo/seeds/data/pages/advertising.md new file mode 100644 index 000000000..67818b667 --- /dev/null +++ b/priv/repo/seeds/data/pages/advertising.md @@ -0,0 +1,11 @@ +# Advertising + +This is the default advertisement policy page of Philomena! Edit this with your site's advertising prices, requirements and contact details. A good advertisement page mentions what kind of advertisements are accepted (e.g. community ads only), NSFW advertisement policy, free advertisement policy (e.g. if you'd like to offer free short-term ads to independent artists), paid advertisement policy (pricing per month, terms, required advance notice, etc), technical requirements of the advertisement banner image (see below), and anything else you might want a potential advertiser to know. Don't forget to mention how a potential advertiser might contact you about advertisement inquiries. + +Philomena is designed to accept advertisements with the following parameters: + +**File size:** no larger than 500 kilobytes (500000 bytes) +**Format:** PNG, GIF or JPG/JPEG +**Max resolution:** 729x91 pixels +**Min resolution:** 699x79 pixels +(we suggest 728x90 resolution for optimal experience) diff --git a/priv/repo/seeds/data/pages/api.md b/priv/repo/seeds/data/pages/api.md new file mode 100644 index 000000000..fcc6c6315 --- /dev/null +++ b/priv/repo/seeds/data/pages/api.md @@ -0,0 +1,1157 @@ +We provide a JSON API for major site functionality, which can be freely used by anyone wanting to produce tools for the site or other web applications that use the data provided within this website. + +## Licensing + +Anyone may use the API. Users making abusively high numbers of requests or excessively expensive requests will be asked to stop, and banned if they do not. Your application must properly cache, and respect server-side cache expiry times. Your client must gracefully back off if requests fail, preferably exponentially or fatally. + +If images are used, the artist must always be credited (if provided) and the original source URL must be displayed alongside the image, either in textual form or as a link. A link to the image page on this website is optional but recommended. The `https:` protocol must be specified on all URLs. + +## Parameters + +This is a list of general parameters that are useful when working with the API. Not all parameters may be used in every request. + +| Name | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | +| `filter_id` Assuming | the user can access the filter ID given by the parameter, overrides the current filter for this request. This is primarily `useful` | for unauthenticated API access. | +| `key` | An optional authentication token. If omitted, no user will be authenticated. You can find your authentication token in your [account settings](/registration/edit). | +| `page` | Controls the current page of the response, if the response is paginated. Empty values default to the first page. | +| `per_page` | Controls the number of results per page, up to a limit of 50, if the response is paginated. The default is 25. | +| `q` | The current search query, if the request is a search request. | +| `sd` | The current sort direction, if the request is a search request. | +| `sf` | The current sort field, if the request is a search request. | + +## Routes + +The interested reader may find the implementations of these endpoints [here](https://github.com/derpibooru/philomena/tree/master/lib/philomena_web/controllers/api). For the purposes of this document, a brief overview is given. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodPathAllowed Query ParametersDescriptionResponse FormatExample
GET/api/v1/json/comments/:comment_idFetches a comment response for the comment ID referenced by the comment_id URL parameter.{"comment":comment-response}/api/v1/json/comments/1
GET/api/v1/json/images/:image_idkey, filter_idFetches an image response for the image ID referenced by the image_id URL parameter.{"image":image-response}/api/v1/json/images/1
POST/api/v1/json/imageskey, urlSubmits a new image. Both key and url are required. Errors will result in an {"errors":image-errors-response}.{"image":image-response}Posting images
GET/api/v1/json/images/featuredFetches an image response for the for the current featured image.{"image":image-response}/api/v1/json/images/featured
GET/api/v1/json/tags/:tag_idFetches a tag response for the tag slug given by the tag_id URL parameter. The tag's ID is not used.{"tag":tag-response}/api/v1/json/tags/artist-colon-atryl
GET/api/v1/json/posts/:post_idFetches a post response for the post ID given by the post_id URL parameter.{"post":post-response}/api/v1/json/posts/2730144
GET/api/v1/json/profiles/:user_idFetches a profile response for the user ID given by the user_id URL parameter.{"user":user-response}/api/v1/json/profiles/1
GET/api/v1/json/filters/:filter_idkeyFetches a filter response for the filter ID given by the filter_id URL parameter.{"filter":filter-response}/api/v1/json/filters/1
GET/api/v1/json/filters/systempageFetches a list of filter responses that are flagged as being system filters (and thus usable by anyone).{"filters":[filter-response]}/api/v1/json/filters/system
GET/api/v1/json/filters/userkey, pageFetches a list of filter responses that belong to the user given by key. If no key is given or it is invalid, will return a 403 Forbidden error.{"filters":[filter-response]}/api/v1/json/filters/user
GET/api/v1/json/oembedurlFetches an oEmbed response for the given app link or CDN URL.oembed-response/api/v1/json/oembed?url=https://cdn.philomena.local/img/2012/1/2/3/full.png
GET/api/v1/json/search/commentskey, pageExecutes the search given by the q query parameter, and returns comment responses sorted by descending creation time.{"comments":[comment-response]}/api/v1/json/search/comments?q=image_id:1000000
GET/api/v1/json/search/gallerieskey, pageExecutes the search given by the q query parameter, and returns gallery responses sorted by descending creation time.{"galleries":[gallery-response]}/api/v1/json/search/galleries?q=title:mean*
GET/api/v1/json/search/postskey, pageExecutes the search given by the q query parameter, and returns post responses sorted by descending creation time.{"posts":[post-response]}/api/v1/json/search/posts?q=subject:time wasting thread
GET/api/v1/json/search/imageskey, filter_id, page, per_page, q, sd, sfExecutes the search given by the q query parameter, and returns image responses.{"images":[image-response]}/api/v1/json/search/images?q=safe
GET/api/v1/json/search/tagspageExecutes the search given by the q query parameter, and returns tag responses sorted by descending image count.{"tags":[tag-response]}/api/v1/json/search/tags?q=analyzed_name:wing
POST/api/v1/json/search/reversekey, url, distanceReturns image responses based on the results of reverse-searching the image given by the url query parameter.{"images":[image-response]}/api/v1/json/search/reverse?url=https://cdn.philomena.local/img/2019/12/24/2228439/full.jpg
GET/api/v1/json/forumsFetches a list of forum responses.{"forums":forum-response}/api/v1/json/forums
GET/api/v1/json/forums/:short_nameFetches a forum response for the abbreviated name given by the short_name URL parameter.{"forum":forum-response}/api/v1/json/forums/dis
GET/api/v1/json/forums/:short_name/topicspageFetches a list of topic responses for the abbreviated forum name given by the short_name URL parameter.{"topics":topic-response}/api/v1/json/forums/dis/topics
GET/api/v1/json/forums/:short_name/topics/:topic_slugFetches a topic response for the abbreviated forum name given by the short_name and topic given by topic_slug URL parameters.{"topic":topic-response}/api/v1/json/forums/dis/topics/ask-the-mods-anything
GET/api/v1/json/forums/:short_name/topics/:topic_slug/postspageFetches a list of post responses for the abbreviated forum name given by the short_name and topic given by topic_slug URL parameters.{"posts":post-response}/api/v1/json/forums/dis/topics/ask-the-mods-anything/posts
GET/api/v1/json/forums/:short_name/topics/:topic_slug/posts/:post_idFetches a post response for the abbreviated forum name given by the short_name, topic given by topic_slug and post given by post_id URL parameters.{"post":post-response}/api/v1/json/forums/dis/topics/ask-the-mods-anything/posts/2761095
+ +## Posting Images + +Posting images should be done via request body parameters. An example with all parameters included is shown below. + +You are _strongly recommended_ to test code using this endpoint using a local copy of the website's source code. Abuse of the endpoint **will result in a ban**. + +You _must_ provide the direct link to the image in the `url` parameter. + +You _must_ set the `content-type` header to `application/json` for the site to process your request. + +``` +POST /api/v1/json/images?key=API_KEY +``` + +``` +{ +"image": { + "description": "[bq]Hey there this is a test post![/bq]\nDescriptions are *weird*.\nHave a >>0 re-upload :)\n", + "tag_input": "artist needed, safe, derpy hooves, pegasus, pony, adventure in the comments, bag, building, chair, cigar, derpibooru legacy, eyes, featured image, female, grin, gritted teeth, hilarious in hindsight, image macro, it begins, j. jonah jameson, letter, mail, male, mare, meme, muffin, necktie, paper, parody, phone, ponified, sitting, smiling, smoking, song in the comments, spider-man, stallion, swinging", + "source_url": "https://philomena.local/images/0" +}, +"url": "https://cdn.philomena.local/img/view/2012/1/2/0.jpg" +} +``` + +

Image Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
animatedBooleanWhether the image is animated.
aspect_ratioFloatThe image's width divided by its height.
comment_countIntegerThe number of comments made on the image.
created_atRFC3339 datetimeThe creation time, in UTC, of the image.
deletion_reasonStringThe hide reason for the image, or null if none provided. This will only have a value on images which are deleted for a rule violation.
descriptionStringThe image's description.
downvotesIntegerThe number of downvotes the image has.
duplicate_ofIntegerThe ID of the target image, or null if none provided. This will only have a value on images which are merged into another image.
durationFloatThe number of seconds the image lasts, if animated, otherwise .04.
favesIntegerThe number of faves the image has.
first_seen_atRFC3339 datetimeThe time, in UTC, the image was first seen (before any duplicate merging).
formatStringThe file extension of the image. One of "gif", "jpg", "jpeg", "png", "svg", "webm".
heightIntegerThe image's height, in pixels.
hidden_from_usersBooleanWhether the image is hidden. An image is hidden if it is merged or deleted for a rule violation.
idIntegerThe image's ID.
intensitiesObjectOptional object of internal image intensity data for deduplication purposes. May be null if intensities have not yet been generated.
mime_typeStringThe MIME type of this image. One of "image/gif", "image/jpeg", "image/png", "image/svg+xml", "video/webm".
nameStringThe filename that the image was uploaded with.
orig_sha512_hashStringThe SHA512 hash of the image as it was originally uploaded.
processedBooleanWhether the image has finished optimization.
representationsObjectA mapping of representation names to their respective URLs. Contains the keys "full", "large", "medium", "small", "tall", "thumb", "thumb_small", "thumb_tiny".
scoreIntegerThe image's number of upvotes minus the image's number of downvotes.
sha512_hashStringThe SHA512 hash of this image after it has been processed.
sizeIntegerThe number of bytes the image's file contains.
source_urlString(Deprecated - Use source_urls field instead) Provides the first source URL of the image as stored in the database, intended for legacy applications only.
source_urlsString[]A list of all source URLs provided for the image, may be empty.
spoileredBooleanWhether the image is hit by the current filter.
tag_countIntegerThe number of tags present on the image.
tag_idsArrayA list of tag IDs the image contains.
tagsArrayA list of tag names the image contains.
thumbnails_generatedBooleanWhether the image has finished thumbnail generation. Do not attempt to load images from view_url or representations if this is false.
updated_atRFC3339 datetimeThe time, in UTC, the image was last updated.
uploaderStringThe image's uploader.
uploader_idIntegerThe ID of the image's uploader. null if uploaded anonymously.
upvotesIntegerThe image's number of upvotes.
view_urlStringThe image's view URL, including tags.
widthIntegerThe image's width, in pixels.
wilson_scoreFloatThe lower bound of the Wilson score interval for the image, based on its upvotes and downvotes, given a z-score corresponding to a confidence of 99.5%.
+

Comment Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
authorStringThe comment's author.
avatarStringThe URL of the author's avatar. May be a link to the CDN path, or a data: URI.
bodyStringThe comment text.
created_atRFC3339 datetimeThe creation time, in UTC, of the comment.
edit_reasonStringThe edit reason for this comment, or null if none provided.
edited_atRFC3339 datetimeThe time, in UTC, this comment was last edited at, or null if it was not edited.
idIntegerThe comment's ID.
image_idIntegerThe ID of the image the comment belongs to.
updated_atRFC3339 datetimeThe time, in UTC, the comment was last updated at.
user_idIntegerThe ID of the user the comment belongs to, if any.
+

Forum Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
nameStringThe forum's name.
short_nameStringThe forum's short name (used to identify it).
descriptionStringThe forum's description.
topic_countIntegerThe amount of topics in the forum.
post_countIntegerThe amount of posts in the forum.
+

Topic Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
slugStringThe topic's slug (used to identify it).
titleStringThe topic's title.
post_countIntegerThe amount of posts in the topic.
view_countIntegerThe amount of views the topic has received.
stickyBooleanWhether the topic is sticky.
last_replied_to_atRFC3339 datetimeThe time, in UTC, when the last reply was made.
lockedBooleanWhether the topic is locked.
user_idIntegerThe ID of the user who made the topic. null if posted anonymously.
authorStringThe name of the user who made the topic.
+

Post Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
authorStringThe post's author.
avatarStringThe URL of the author's avatar. May be a link to the CDN path, or a data: URI.
bodyStringThe post text.
created_atRFC3339 datetimeThe creation time, in UTC, of the post.
edit_reasonStringThe edit reason for this post.
edited_atRFC3339 datetimeThe time, in UTC, this post was last edited at, or null if it was not edited.
idIntegerThe post's ID (used to identify it).
updated_atRFC3339 datetimeThe time, in UTC, the post was last updated at.
user_idIntegerThe ID of the user the post belongs to, if any.
+

Tag Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
aliased_tagStringThe slug of the tag this tag is aliased to, if any.
aliasesArrayThe slugs of the tags aliased to this tag.
categoryStringThe category class of this tag. One of "character", "content-fanmade", "content-official", "error", "oc", "origin", "rating", "species", "spoiler".
descriptionStringThe long description for the tag.
dnp_entriesArrayAn array of objects containing DNP entries claimed on the tag.
idIntegerThe tag's ID.
imagesIntegerThe image count of the tag.
implied_by_tagsArrayThe slugs of the tags this tag is implied by.
implied_tagsArrayThe slugs of the tags this tag implies.
nameStringThe name of the tag.
name_in_namespaceStringThe name of the tag in its namespace.
namespaceStringThe namespace of the tag.
short_descriptionStringThe short description for the tag.
slugStringThe slug for the tag.
spoiler_image_uriStringThe spoiler image for the tag.
+

User Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
idIntegerThe ID of the user.
nameStringThe name of the user.
slugStringThe slug of the user.
roleStringThe role of the user.
descriptionStringThe description (bio) of the user.
avatar_urlStringThe URL of the user's thumbnail. null if the avatar is not set.
created_atRFC3339 datetimeThe creation time, in UTC, of the user.
comments_countIntegerThe comment count of the user.
uploads_countIntegerThe upload count of the user.
posts_countIntegerThe forum posts count of the user.
topics_countIntegerThe forum topics count of the user.
linksObjectThe links the user has registered. See links-response.
awardsObjectThe awards/badges of the user. See awards-response.
+

Filter Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
idIntegerThe id of the filter.
nameStringThe name of the filter.
descriptionStringThe description of the filter.
user_idIntegerThe id of the user the filter belongs to. null if it isn't assigned to a user (usually system filters only).
user_countIntegerThe amount of users employing this filter.
systemBooleanIf true, is a system filter. System filters are usable by anyone and don't have a user_id set.
publicBooleanIf true, is a public filter. Public filters are usable by anyone.
spoilered_tag_idsArrayA list of tag IDs (as integers) that this filter will spoil.
spoilered_complexStringThe complex spoiled filter.
hidden_tag_idsArrayA list of tag IDs (as integers) that this filter will hide.
hidden_complexStringThe complex hidden filter.
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
user_idIntegerThe ID of the user who owns this link.
created_atRFC3339 datetimeThe creation time, in UTC, of this link.
stateStringThe state of this link.
tag_idIntegerThe ID of an associated tag for this link. null if no tag linked.
+

Awards Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
image_urlStringThe URL of this award.
titleStringThe title of this award.
idIntegerThe ID of the badge this award is derived from.
labelStringThe label of this award.
awarded_onRFC3339 datetimeThe time, in UTC, when this award was given.
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
descriptionStringThe gallery's description.
idIntegerThe gallery's ID.
spoiler_warningStringThe gallery's spoiler warning.
thumbnail_idIntegerThe ID of the cover image for the gallery.
titleStringThe gallery's title.
userStringThe name of the gallery's creator.
user_idIntegerThe ID of the gallery's creator.
+

Image Errors Responses

+

Each field is optional and is an Array of Strings.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
imageArrayErrors in the submitted image
image_aspect_ratioArrayErrors in the submitted image
image_formatArrayWhen an image is unsupported (ex. WEBP)
image_heightArrayErrors in the submitted image
image_widthArrayErrors in the submitted image
image_sizeArrayUsually if an image that is too large is uploaded.
image_is_animatedArrayErrors in the submitted image
image_mime_typeArrayErrors in the submitted image
image_orig_sha512_hashArrayErrors in the submitted image. If has already been taken is present, means the image already exists in the database.
image_sha512_hashArrayErrors in the submitted image
tag_inputArrayErrors with the tag metadata.
uploaded_imageArrayErrors in the submitted image
+

Oembed Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
author_nameStringThe comma-delimited names of the image authors.
author_urlStringThe source URL of the image.
cache_ageIntegerAlways 7200.
derpibooru_commentsIntegerThe number of comments made on the image.
derpibooru_idIntegerThe image's ID.
derpibooru_scoreIntegerThe image's number of upvotes minus the image's number of downvotes.
derpibooru_tagsArrayThe names of the image's tags.
provider_nameStringAlways "Derpibooru".
provider_urlStringAlways "https://derpibooru.org".
titleStringThe image's ID and associated tags, as would be given on the title of the image page.
typeStringAlways "photo".
versionStringAlways "1.0".
diff --git a/priv/repo/seeds/data/pages/approval.md b/priv/repo/seeds/data/pages/approval.md new file mode 100644 index 000000000..5247f1c29 --- /dev/null +++ b/priv/repo/seeds/data/pages/approval.md @@ -0,0 +1,43 @@ +# Approval Queue and User Verification + +If you are here, you must be wondering why your upload got held up in an approval queue instead of being posted directly. + +Shortly speaking - **this is to protect our users from illegal imagery.** + +# Images + +#### **What happens to the image while it's not approved?** + +The image will not appear in any search results and will be impossible to be linked to via our on-site image embedding syntax. It is still viewable via a direct URL. + +#### **Why did my upload require approval?** + +We require that all uploads from users without an account, as well as unverified registered users go through the approval queue where a staff member can decide whether it's illegal content (Rule #5) or not. + +#### **Will my image lose views because of this?** + +No! Once approved, your image appears in search results and on the home page as if it was uploaded at the time of approval. + +#### **Is this some kind of censorship?** + +No. We are strictly checking for whether or not the imagery is illegal (Rule #5) or not. This is not "Quality Control" in any way whatsoever. + +#### **How do I get verified?** + +First of all - register an account. Once you upload a certain (small) amount of images that get approved by the staff members, your account will be evaluated and depending on staff evaluation, you will be granted verification. Once verified, your uploads will bypass the approval queue and be automatically approved. Please note that staff evaluation of accounts may take up to a week, but will usually take a day or two. + +#### **Where can I check if I'm verified?** + +As of the moment of writing this article - you cannot. If you're unsure if you're verified or not, simply ask a staff member via PMs. + +# Comments and forum posts + +Comments and forum posts are also subject to additional moderation measures now. + +#### **Why can't I embed external images into my comments or posts?** + +Users without an account cannot use the image embed syntax (`![](image url)`) at all. + +#### **Why does my comment/post require additional approval?** + +If your account is relatively new, any of your posts/comments that contain the image embed syntax (`![](image url)`) must go through staff approval before they're publicly visible and searchable. diff --git a/priv/repo/seeds/data/pages/contact.md b/priv/repo/seeds/data/pages/contact.md new file mode 100644 index 000000000..211ceb48d --- /dev/null +++ b/priv/repo/seeds/data/pages/contact.md @@ -0,0 +1,3 @@ +# Contact + +Fill this page in with your site's contact information. diff --git a/priv/repo/seeds/data/pages/donations.md b/priv/repo/seeds/data/pages/donations.md new file mode 100644 index 000000000..e3d75f356 --- /dev/null +++ b/priv/repo/seeds/data/pages/donations.md @@ -0,0 +1,3 @@ +# Donations + +Fill this page in with ways your users could donate to help run your website. diff --git a/priv/repo/seeds/data/pages/faq.md b/priv/repo/seeds/data/pages/faq.md new file mode 100644 index 000000000..08d28a48d --- /dev/null +++ b/priv/repo/seeds/data/pages/faq.md @@ -0,0 +1,3 @@ +# FAQ + +Fill this page in with any common questions your users might have. diff --git a/priv/repo/seeds/data/pages/markdown.md b/priv/repo/seeds/data/pages/markdown.md new file mode 100644 index 000000000..93a6a2a62 --- /dev/null +++ b/priv/repo/seeds/data/pages/markdown.md @@ -0,0 +1,419 @@ +This page is here to help you get a better grasp on the syntax of Markdown, the text processing engine this site uses. + +## Inline formatting + +Inline formatting is the most commonly seen type of text formatting in Markdown. It can be applied almost anywhere else and doesn't depend on specific context (most of the time). + +| Operator | Example | Result | +| ------------- | ------------------------------------- | ---------------------------------- | +| Bold | `This is **huge**` | This is **huge** | +| Italic | `*very* clever, Connor... _very..._` | _very_ clever, Connor... _very..._ | +| Underline | `And I consider this __important__` | And I consider this **important** | +| Strikethrough | `I am ~~wrong~~ right` | I am ~~wrong~~ right | +| Superscript | `normal text ^superscripted text^` | normal text ^superscripted text^ | +| Subscript | `normal text ~subscripted text~` | normal text ~subscripted text~ | +| Spoiler | `Psst! ||Count Dracula is a vampire!||` | Psst! ||Count Dracula is a vampire!|| | +| Code | ``Use `**bold**` to make text bold!`` | Use `**bold**` to make text bold! | + +#### Multi-line inlines + +Most inline formatting can extend beyond just a single line and travel to other lines. However, it does have certain quirks, especially if you're unused to the Markdown syntax. + +``` +**I am a very +bold text** +``` + +
+
+ Result +
+
+
+ I am a very
bold text
+
+
+
+ +However, if you try to insert a newline in the middle of it, it won't work. + +``` +**I am not a very + +bold text** +``` + +
+
+ Result +
+
+
**I am not a very
+
bold text**
+
+
+ +If you really need an empty line in the middle of your inline-formatted text, you must _escape_ the line ending. In order to do so, Markdown provides us with the `\` (backslash) character. Backslash is a very special character and is used for _escaping_ other special characters. _Escaping_ forces the character immediately after the backslash to be ignored by the parser. + +As such, we can write our previous example like so to preserve the empty line: + +``` +**I am a very +\ +bold text** +``` + +
+
+ Result +
+
+
I am a very
+
+ bold text
+
+
+ +#### Combining inlines + +Most inline operators may be combined with each other (with the exception of the `code` syntax). + +``` +_I am an italic text **with some bold in it**._ +``` + +
+
+ Result +
+
+
I am an italic text with some bold in it.
+
+
+ +## Block formatting + +Block formatting is the kind of formatting that cannot be written within a single line and typically requires to be written on its own line. Many block formatting styles extend past just one line. + +#### Blockquotes + +Philomena's flavor of Markdown makes some changes to the blockquote syntax compared to regular CommonMark. The basic syntax is a > followed by a space. + +``` +> quote text +``` + +> quote text + +--- + +Please note, that if > is not followed by a space, it will not become a blockquote! + +``` +>not a quote +``` + +> not a quote + +--- + +Same goes for >>, even if followed by a space. + +``` +>> not a quote +``` + +> > not a quote + +--- + +You may continue a quote by adding > followed by a space on a new line, even if the line is otherwise empty. + +``` +> quote text +> +> continuation of quote +``` + +> quote text +> +> continuation of quote + +--- + +To nest a quote, simply repeat > followed by a space as many times as you wish to have nested blockquotes. + +``` +> quote text +> > nested quote +> > > even deeper nested quote +``` + +> quote text +> +> > nested quote +> > +> > > even deeper nested quote + +#### Headers + +Markdown supports adding headers to your text. The syntax is # repeated up to 6 times. + +``` +# Header 1 +## Header 2 +### Header 3 +#### Header 4 +##### Header 5 +###### Header 6 +``` + +# Header 1 + +## Header 2 + +### Header 3 + +#### Header 4 + +##### Header 5 + +###### Header 6 + +#### Code block + +Another way to write code is by writing a code block. Code blocks, unlike inline code syntax, are styled similar to blockquotes and are more appropriate for sharing larger snippets of code. In fact, this very page has been using this very structure to show examples of code. + +```` +``` +
+

Hello World!

+
+``` +```` + +``` +
+

Hello World!

+
+``` + +Code blocks may also use tildes (\~) instead of backticks (\`). + +``` +~~~ +code block +~~~ +``` + +``` +code block +``` + +## Links + +Links have the basic syntax of + +``` +[Link Text](https://example.com) +``` + +[Link Text](https://example.com) + +Most links pasted as plaintext will be automatically converted into a proper clickable link, as long as they don't begin with dangerous protocols. +As such... + +``` +https://example.com +``` + +https://example.com + +On-site links may be written as either a relative or absolute path. If the on-site link is written as the absolute path, it will be automatically converted into a relative link for the convenience of other users. + +``` +[Link to the first image](https://philomena.local/images/0) +[Link to the first image](/images/0) +``` + +[Link to the first image](https://philomena.local/images/0) +[Link to the first image](/images/0) + +## On-site images + +If you wish to link an on-site image, you should use the >>:id syntax. It respects filters currently in-use by the reader and spoilers content they do not wish to see. +**You should always use this for on-site uploads!** (as this will let other users filter the image if they wish to, and it is against the rules to not show content with care) +Here's a brief explanation of its usage. + +| Operator | Description of result | +| -------- | ---------------------------------------- | +| \>\>5 | Simple link to image | +| \>\>5s | Small (150x150) thumbnail of the image | +| \>\>5t | Regular (320x240) thumbnail of the image | +| \>\>5p | Preview (800x600) size of the image | + +> > 5 +> > 5s +> > 5t +> > 5p + +## External images + +Some images you may wish to link may not exist on the site. To link them Markdown provides us with a special syntax. All images embedded this way are proxied by our image proxy (Go-Camo). + +``` +![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +``` + +![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) + +You may control the size of your externally-linked image by specifying the alt text. Certain keywords are recognized as size modifiers. The modifiers are case-sensitive! + +| Modifier | Resulting size | +| --------------- | -------------------------- | +| tiny | 64x64 | +| small | 128x128 | +| medium | 256x256 | +| large | 512x512 | +| (anything else) | (actual size of the image) | + +``` +![tiny](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![small](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![medium](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![large](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +``` + +![tiny](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![small](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![medium](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![large](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) +![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg) + +#### Image links + +To make an image link, simply combine the external image syntax with the link syntax. + +``` +[![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg)](https://github.com/philomena-dev/philomena) +``` + +[![](https://raw.githubusercontent.com/philomena-dev/philomena/master/assets/static/images/phoenix.svg)](https://github.com/philomena-dev/philomena) + +## Lists + +#### Unordered list + +Unordered lists can be written fairly intuitively, by putting one of the special characters in front of each line that should be a part of the list. + +``` +Shopping list: +* Milk +* Eggs +* Soda +``` + +Shopping list: + +- Milk +- Eggs +- Soda + +You may use any of the following characters at the beginning of the line to make an unordered list: + +``` +* ++ +- +``` + +Lists may be nested and have sublists within them. Simply prefix your sublist items with three spaces while within another list. + +``` +* Item one +* Item two + * Sublist item one + * Sublist item two +``` + +- Item one +- Item two + - Sublist item one + - Sublist item two + +#### Ordered list + +To write an ordered list, simply put a number at the beginning of the line followed by a dot or closing bracket. It doesn't actually matter which order your numbers are written in, the list will always maintain its incremental order. Note the 4 in the example, it isn't a typo. + +``` +1. Item one +2. Item two +4. Item three +``` + +1. Item one +2. Item two +3. Item three + +**Ordered lists cannot be sublists to other ordered lists.** They can, however, be sublists to unordered lists. Unordered lists, in turn, may be sublists in ordered lists. + +``` +1) Item one +2) Item two + * Sublist item one + * Sublist item two +``` + +1. Item one +2. Item two + - Sublist item one + - Sublist item two + +## Tables + +Philomena's Markdown implementation supports GitHub-style tables. This isn't a part of the core Markdown specification, but we support them. The colons are used to specify the alignment of columns. + +``` +| Left | Center | Right | +| ------------ |:--------------:| -------------:| +| left-aligned | center-aligned | right-aligned | +| *formatting* | **works** | __here__ | +``` + +| Left | Center | Right | +| ------------ | :------------: | ------------: | +| left-aligned | center-aligned | right-aligned | +| _formatting_ | **works** | **here** | + +In tables, the pipes (|) at the edges of the table are optional. To separate table head from body, you need to put in at least three - symbols. As such, example above could have also been written like so: + +``` +Left | Center | Right +--- | :---: | ---: +left-aligned | center-aligned | right-aligned +*formatting* | **works** | __here__ +``` + +| Left | Center | Right | +| ------------ | :------------: | ------------: | +| left-aligned | center-aligned | right-aligned | +| _formatting_ | **works** | **here** | + +# Escaping the syntax. + +Sometimes you may wish certain characters to not be interpreted as Markdown syntax. This is where the backslash comes in! Prefixing any markup with a backslash will cause the markup immediately following the backslash to not be parsed, for example: + +``` +\*\*grr grr, I should not be bold!\*\* +``` + +\*\*grr grr, I should not be bold\*\* + +Code blocks and code inlines will also escape the syntax to a limited extent (except for backticks themselves). + +``` +`**not bold!**` +``` + +`**not bold!**` diff --git a/priv/repo/seeds/data/pages/privacy.md b/priv/repo/seeds/data/pages/privacy.md new file mode 100644 index 000000000..7b70d09db --- /dev/null +++ b/priv/repo/seeds/data/pages/privacy.md @@ -0,0 +1,125 @@ +We may update this document in the future, and will provide a site notice when we do. + +--- + +## The short version + +We collect only the bare minimum amount of information that is necessary to protect the service against abuse. We do not sell your information to third parties, and we only use it as this document describes. We aim to be compliant with the EU GDPR. + +--- + +## What information does this site collect and why + +**Information from webserver logs** + +We collect the following information in webserver logs from every visitor: + +- The Internet Protocol (IP) address +- The date and time of the request +- The page that was requested +- The browser user agent string + +These items are collected to ensure the security of the service (see "legitimate interests" in the GDPR), and are deleted after 14 days to balance it with user privacy. + +**Browser fingerprints** + +Browser fingerprints are a tool used to identify users of the service in such a way that administrators will have no knowledge of the individual components of a fingerprint. They are irretrievably hashed (by a browser script) from various browser attributes. As of June 2024, we maintain two distinct versions of the algorithm, which use different properties as components to generate the hash. These are: + +**Version 3 (past, used prior to June 15, 2024)** + +- Browser user agent string +- Screen width, height, and color depth +- Timezone offset +- Language +- OS name + +**Version 4 (current)** + +- Browser identity and version +- Screen width, height, and color depth +- Timezone offset +- Language and keyboard layout +- Hardware information (amount of CPU cores and RAM) +- Multi-touch support +- OS information (name, mobile/desktop) + +**Information in cookies** + +Our cookies for any users of the service may contain this information: + +- The unique session token for the website +- User preference for loading high-resolution images +- User preference for loading video previews of animated images +- User preference for website layout customization +- User preference for filtering settings +- One or more "flash" messages (temporary notifications of an action's success or failure, to be displayed at the top of the next page load and then deleted) +- A browser fingerprint + +Additionally, cookies of users that are logged in will contain this information: + +- An encrypted authentication secret unique to the user to persist their login + +Because these are required for authentication, user security, or customization, which are all "legitimate interests", we cannot ask for consent to use cookies. + +**Information in user-submitted content** + +User-submitted content is considered by us to collectively refer to any content that you may submit to the site, which includes, but is not limited to, comments, images, messages, posts, reports, source changes, tag changes, and votes. + +User-submitted content by users (authenticated or not) may have any or all of the following information collected at the time of submission attached, visible only to site staff: + +- The IP address +- The browser fingerprint +- The browser user agent string +- The page that initiated the submission + +These items are only used for the "legitimate interests" of identifying and controlling abuse of the service and are not shared with any external party. + +--- + +## Information from users with accounts + +If you **create an account**, we require some basic information at the time of account creation, as follows: + +- a username, shown on your profile and non-anonymous user-submitted content +- a password, stored only as a cryptographic hash +- an email address, shown only to site staff and used only as a means of contact for account control (verification emails, password reset emails, and account unlock emails) + +We also store your IP address and browser fingerprint whenever you log in for security reasons. + +--- + +## Information shared with third-party services + +We use a few services for security purposes which use personal information. These are as follows: + +- To protect against spam, hCaptcha is used. Their privacy policy can be found [here](https://www.hcaptcha.com/privacy). + +--- + +## Information sharing with other parties + +Besides services we rely on for security purposes, we only share personal information with third parties in response to court orders. + +We display certain statistics about how users use our site (for example, about uploads), without any personal or personally-identifying information. + +Many forms of user-submitted content (such as comments or uploads) are viewable by anyone, and as such, may be accessed freely by third parties, including search engines. If a person's personal information is put in such content, we may remove if it we deem it to be too sensitive; inform us if you believe something has been shared that is sensitive. + +--- + +## How we secure your information + +We take all measures reasonably necessary to protect account information from unauthorized access, alteration, or destruction. + +We use a restrictive content security policy to protect against page hijacking and information leakage to third parties, an image proxy server to avoid leaking user IP address information from embedded images on the site, a cross-origin resource sharing (CORS) policy to restrict third-party usage, a strict referrer policy to prevent leaking data for external links, and an frame policy to prevent clickjacking. + +Passwords are hashed using bcrypt at 2^10 iterations with a 128-bit per-user salt. + +No method of transmission, or method of electronic storage, is 100% secure. Therefore, we cannot guarantee its absolute security; we only make a best effort. + +--- + +## Complaints and account Personally-Identifiable Information wiping + +If you have concerns or objections about the way we handle your personal information, please let us know immediately. You may contact us by PMing a [staff member](/staff). + +If you wish to have all stored personal information related to an account removed, you can submit a request for a wipe of personally-identifiable information (PII). If approved (that is, if we do not believe we have a legitimate interest in keeping the information around, such as to preserve evidence of site abuse), the account will be deactivated (can no longer be logged in to) and all personally-identifying information on it, as well as on content submitted with it, will be removed. Since this removes the email address, which is necessary to log in, it is **irreversible**, unlike account deactivation on its own. diff --git a/priv/repo/seeds/data/pages/rules.md b/priv/repo/seeds/data/pages/rules.md new file mode 100644 index 000000000..0f15cbc15 --- /dev/null +++ b/priv/repo/seeds/data/pages/rules.md @@ -0,0 +1,52 @@ +# The Rules + +0. Be nice to each other. **The most important rule.** + - Do not post content whose sole purpose is to cause anger or controversy + - Do not post hate speech + - Do not harass others or try to incite violence against them + - Do not discriminate based on physical characteristics (e.g. gender, race, etc) or religious beliefs + - Criticism is welcome and encouraged, but only when it's polite + - This rule applies to all content posted on site (images, comments, and everything else) +1. Respect the authors of the content + - Do not upload without permission from the author + - Do not upload private content (e.g. paid-for exclusive content) unless permitted by the author + - Do not upload copyrighted content (e.g. snippets from movies or TV shows) unless permitted by the author or applicable law + - Do not remove signatures or watermarks from images before uploading + - Respect the Do-Not-Post list +2. Tag images appropriately + - Pick the rating tag(s) appropriately + - Try to use as many tags as you can + - Use tags appropriate to the image +3. Do not post content unrelated to this website + - Do not post content unrelated to the theme of this website + - Images are not considered "related" by the presence of themed content; images must meet a threshold of artistic effort to be uploaded +4. Use filters to hide content you don't wish to see + - Do not post comments expressing dislike for general themes; use filters to hide images with these themes + - If you believe something violates the site rules, report it instead of commenting about it + - Filter content which you cannot view in your current jurisdiction +5. Do not post content illegal in this website's jurisdiction + - Do not upload real-life underage pornography or art which attempts to emulate it + - Do not upload real-life bestiality or art which attempts to emulate it + - Do not upload content promoting or glorifying ideologies restricted in our jurisdiction, such as Nazism +6. Do not abuse site functionality + - Do not use multiple accounts to evade punishments or to pretend to be two different people in the same discussion + - Do not upload spammy images, such as "reminder" style posts + - Do not create multiple comments/posts in a row + - Do not post barely-related single-word comments on images + - Do not post unsolicited advertisement + - Do not post off-topic comments or derail discussion + - Vote on images based on their artistic merit; do not indiscriminately vote based on a filterable characteristic of the image + - Do not impersonate staff members +7. Spoiler content which cannot be filtered + - Respect the sexual rating of where you post; reserve NSFW posts for forum topics clearly marked as NSFW + - Use the `||spoiler syntax||` to spoiler above-rating text, which built-in site functionality cannot automatically filter + - Avatars must not contain sexual content or flickering animations which may induce seizures +8. Respect privacy + - Do not reveal any personal information about artists or other users without consent + - If you are aware of an artist or a user using multiple aliases to separate their work, do not reveal this information publicly without their consent +9. Advertise commissions in good faith + - All commissions must contain only the artwork created by the person advertising the commission + - You may not advertise a commission on behalf of another person + - Site staff are not responsible for any commission arrangements or outcomes based on commission advertisements + +Rules are not exhaustive, the site staff have the final say. diff --git a/priv/repo/seeds/data/pages/shortcuts.md b/priv/repo/seeds/data/pages/shortcuts.md new file mode 100644 index 000000000..b28f04c0a --- /dev/null +++ b/priv/repo/seeds/data/pages/shortcuts.md @@ -0,0 +1,27 @@ +Various functions of Philomena have keyboard shortcuts to make browsing easier and faster. + +## Image Lists + +| Key | Action | +| --- | -------------------------------------------------------------- | +| J | Previous page | +| K | Next page | +| F | Favourite image (image under the mouse pointer) | +| U | Upvote image (image under the mouse pointer) | +| O | View full image in current tab (image under the mouse pointer) | +| V | View full image in new tab (image under the mouse pointer) | + +## Image Pages + +| Key | Action | +| --- | ---------------------------------------------------------------------------------------- | +| J | Previous image (in global index terms, the image posted after the one you're looking at) | +| K | Next image (image posted before current one) | +| R | Random image | +| S | Open source URL | +| L | Open tags for editing | +| I | Go to index page containing the image being displayed | +| F | Favourite image | +| U | Upvote image | +| O | View full image in current tab (image under the mouse pointer) | +| V | View full image in new tab (image under the mouse pointer) | diff --git a/priv/repo/seeds/data/pages/spoilers.md b/priv/repo/seeds/data/pages/spoilers.md new file mode 100644 index 000000000..410f633e3 --- /dev/null +++ b/priv/repo/seeds/data/pages/spoilers.md @@ -0,0 +1,3 @@ +# Spoiler Guidelines + +Fill this page in with how you expect your users to avoid spoilering media to your other users. diff --git a/priv/repo/seeds/data/pages/start.md b/priv/repo/seeds/data/pages/start.md new file mode 100644 index 000000000..7412fbb83 --- /dev/null +++ b/priv/repo/seeds/data/pages/start.md @@ -0,0 +1,3 @@ +# Getting Started + +Fill this page in with a brief description for new users of how to get started in your community. diff --git a/priv/repo/seeds/data/pages/tags.md b/priv/repo/seeds/data/pages/tags.md new file mode 100644 index 000000000..d5a262fbc --- /dev/null +++ b/priv/repo/seeds/data/pages/tags.md @@ -0,0 +1,3 @@ +# Tagging Guidelines + +Fill this page in with your tagging policies and guidelines diff --git a/priv/repo/seeds/data/pages/takedowns.md b/priv/repo/seeds/data/pages/takedowns.md new file mode 100644 index 000000000..78131e295 --- /dev/null +++ b/priv/repo/seeds/data/pages/takedowns.md @@ -0,0 +1,3 @@ +# Takedown Policy + +Fill this page in with your site's content takedown policies. diff --git a/priv/repo/seeds/data/pages/uploading.md b/priv/repo/seeds/data/pages/uploading.md new file mode 100644 index 000000000..23904b39c --- /dev/null +++ b/priv/repo/seeds/data/pages/uploading.md @@ -0,0 +1,47 @@ +Uploading content is a piece of cake. You just hit the 'Upload' button and fill in the form. However, there's a few little details we'd like to explain better to you if you're interested. + +## Metadata + +We provide a few fields for metadata - tags are designed to let you group images together, and describe things in terms of their content. We also have a description field, which is intended mostly for people uploading original content to the site, or for more detailed description of images or context around the image. It can also be used for audio description of images for people with screenreaders. + +For instance, the fact that an image contains **count dracula** belongs as a tag, and if it's the **Christmas**, that's a tag, too. Fully describing an image like `Count Dracula standing in the snow beside a Christmas tree` should be done in the description. + +We also have some "meta" tags — **artist:artist name** tags should be used to link an artist name to an image. There are also spoilered or hidden by default tags, which stop NSFW things from popping up when not wanted. These should be used where appropriate. + +Finally, there is the source URL field. This should link to the page on which the image was originally found. If you don't know, leave it blank, but try to find it first. + +## Scalable Vector Graphic uploads + +We support SVG uploads - once we get them on the server we make PNG images out of them, but people can still download and view the SVG version on the links on the image. `librsvg` is used to render the images. + +We recommend you provide a sensible default resolution with your document - a couple of thousand pixels is plenty! + +## Optimization + +When you upload a GIF, JPEG or PNG, we do some checks on the image once it's been uploaded. Most images have un-needed data in them, which can be safely removed without affecting quality. We use a few tools to do this on your uploads, resulting in smaller file sizes for us to store, and faster page loads for everybody. + +#### PNG + +We use `optipng` to deinterlace and compress PNG images, fixing any encoding issues on the way. + +#### JPEG + +We use `jpegtran` to sort out JPEGs, which supports lossless optimization of the entropy encoding scheme used in JPEG compression. + +#### GIF + +GIFs are a bit more complex, as we treat all GIFs as probably animated, and so have to deal with all the frame processing. We use `gifsicle` and `ffmpeg` to process GIFs. + +#### SVG + +SVG images are left unchanged by uploads. + +## Deduplication + +We perform perceptual image deduplication using a simple image intensity based mechanism which has proven to be scalable and reasonably reliable over the years. We also provide SHA512 hashes of images in the site, though these are no longer used internally for deduplication. + +## Workflow + +We do all the processing in the background, and while we're doing it, we continue to serve the unoptimized file, so there's no noticeable difference for anyone. It is, however, noteworthy if you intend to download a file, you may wish to wait for the fully processed image to be available. + +Basically, upload, and don't worry about it! We'll handle all the heavy lifting on our end, and once we finish processing the image, it'll be served instead of the old unoptimized one immediately. diff --git a/priv/repo/seeds/data/quick_tags.json b/priv/repo/seeds/data/quick_tags.json new file mode 100644 index 000000000..205e0b8e4 --- /dev/null +++ b/priv/repo/seeds/data/quick_tags.json @@ -0,0 +1,447 @@ +{ + "quick_tag_tabs": [ + { "title": "Main", "position": 0 }, + { "title": "Species", "position": 1 }, + { "title": "Shorthands A", "position": 2 }, + { "title": "B", "position": 3 }, + { "title": "Ships ts", "position": 4 }, + { "title": "rd", "position": 5 }, + { "title": "ry", "position": 6 }, + { "title": "aj", "position": 7 }, + { "title": "fs", "position": 8 }, + { "title": "pp", "position": 9 }, + { "title": "misc", "position": 10 }, + { "title": "Season 9", "position": 11 }, + { "title": "8", "position": 12 }, + { "title": "7", "position": 13 }, + { "title": "6", "position": 14 }, + { "title": "5", "position": 15 }, + { "title": "4", "position": 16 }, + { "title": "3", "position": 17 }, + { "title": "2", "position": 18 }, + { "title": "1", "position": 19 } + ], + "default_quick_tags": [ + { + "tab_title": "Main", + "category": "Ratings", + "tags": ["safe", "suggestive", "questionable", "explicit", "semi-grimdark", "grimdark", "grotesque"] + }, + { + "tab_title": "Main", + "category": "General Spoilers", + "tags": ["spoiler:comic", "spoiler:g5", "spoiler:pony life", "spoilers for another series"] + }, + { + "tab_title": "Main", + "category": "Species", + "tags": ["anthro", "equestria girls", "human", "humanized", "pony", "earth pony", "pegasus", "unicorn", "alicorn"] + }, + { + "tab_title": "Main", + "category": "Often Forgotten", + "tags": [ + "solo", + "screencap", + "image macro", + "monochrome", + "oc", + "oc only", + "photo", + "Tag original characters oc:name" + ] + }, + { + "tab_title": "Species", + "category": "Major", + "tags": [ + "bat pony", + "changeling", + "changedling", + "deer", + "dragon", + "griffon", + "classical hippogriff", + "yak", + "zebra" + ] + }, + { + "tab_title": "Species", + "category": "Minor", + "tags": [ + "abyssinian", + "breezie", + "centaur", + "diamond dog", + "draconequus", + "kirin", + "seapony (g4)", + "siren", + "sphinx" + ] + }, + { + "tab_title": "Species", + "category": "Animal", + "tags": [ + "chimera", + "cockatrice", + "hydra", + "manticore", + "parasprite", + "phoenix", + "tatzlwurm", + "timber wolf", + "windigo" + ] + }, + { + "tab_title": "Species", + "category": "Misc", + "tags": ["original species", "hybrid"] + } + ], + "shorthand_quick_tag_categories": [ + { "tab_title": "Shorthands A", "category": "Mane Cast" }, + { "tab_title": "Shorthands A", "category": "Secondary Cast" }, + { "tab_title": "Shorthands A", "category": "More Ponies" }, + { "tab_title": "Shorthands A", "category": "More Apples" }, + { "tab_title": "Shorthands A", "category": "Fillies and Colts" }, + { "tab_title": "B", "category": "Background Ponies" }, + { "tab_title": "B", "category": "Student Six" }, + { "tab_title": "B", "category": "Uncategorized" }, + { "tab_title": "B", "category": "Other Things" } + ], + "shorthand_quick_tags": [ + { "category": "Mane Cast", "shorthand": "m6", "tag": "mane six" }, + { "category": "Mane Cast", "shorthand": "pt", "tag": "princess twilight" }, + { "category": "Mane Cast", "shorthand": "ts", "tag": "twilight sparkle" }, + { "category": "Mane Cast", "shorthand": "rd", "tag": "rainbow dash" }, + { "category": "Mane Cast", "shorthand": "ry", "tag": "rarity" }, + { "category": "Mane Cast", "shorthand": "aj", "tag": "applejack" }, + { "category": "Mane Cast", "shorthand": "fs", "tag": "fluttershy" }, + { "category": "Mane Cast", "shorthand": "pp", "tag": "pinkie pie" }, + { "category": "Mane Cast", "shorthand": "sp", "tag": "spike" }, + { "category": "Secondary Cast", "shorthand": "cmc", "tag": "cutie mark crusaders" }, + { "category": "Secondary Cast", "shorthand": "ab", "tag": "apple bloom" }, + { "category": "Secondary Cast", "shorthand": "sl", "tag": "scootaloo" }, + { "category": "Secondary Cast", "shorthand": "sb", "tag": "sweetie belle" }, + { "category": "Secondary Cast", "shorthand": "tia", "tag": "princess celestia" }, + { "category": "Secondary Cast", "shorthand": "luna", "tag": "princess luna" }, + { "category": "Secondary Cast", "shorthand": "pcd", "tag": "princess cadance" }, + { "category": "Secondary Cast", "shorthand": "sa", "tag": "shining armor" }, + { "category": "Secondary Cast", "shorthand": "sg", "tag": "starlight glimmer" }, + { "category": "More Ponies", "shorthand": "tx", "tag": "trixie" }, + { "category": "More Ponies", "shorthand": "sus", "tag": "sunset shimmer" }, + { "category": "More Ponies", "shorthand": "sombra", "tag": "king sombra" }, + { "category": "More Ponies", "shorthand": "qc", "tag": "queen chrysalis" }, + { "category": "More Ponies", "shorthand": "dc", "tag": "discord" }, + { "category": "More Ponies", "shorthand": "nmm", "tag": "nightmare moon" }, + { "category": "More Ponies", "shorthand": "sf", "tag": "spitfire" }, + { "category": "More Ponies", "shorthand": "sn", "tag": "soarin'" }, + { "category": "More Ponies", "shorthand": "ld", "tag": "lightning dust" }, + { "category": "More Apples", "shorthand": "bm", "tag": "big macintosh" }, + { "category": "More Apples", "shorthand": "gs", "tag": "granny smith" }, + { "category": "More Apples", "shorthand": "bb", "tag": "braeburn" }, + { "category": "More Apples", "shorthand": "bs", "tag": "babs seed" }, + { "category": "Fillies and Colts", "shorthand": "ss", "tag": "silver spoon" }, + { "category": "Fillies and Colts", "shorthand": "dt", "tag": "diamond tiara" }, + { "category": "Fillies and Colts", "shorthand": "pfh", "tag": "princess flurry heart" }, + { "category": "Background Ponies", "shorthand": "dh", "tag": "derpy hooves" }, + { "category": "Background Ponies", "shorthand": "dw", "tag": "doctor whooves" }, + { "category": "Background Ponies", "shorthand": "cgt", "tag": "colgate" }, + { "category": "Background Ponies", "shorthand": "bon", "tag": "bon bon" }, + { "category": "Background Ponies", "shorthand": "oct", "tag": "octavia melody" }, + { "category": "Background Ponies", "shorthand": "dj", "tag": "vinyl scratch" }, + { "category": "Background Ponies", "shorthand": "bp", "tag": "berry punch" }, + { "category": "Background Ponies", "shorthand": "pbb", "tag": "prince blueblood" }, + { "category": "Student Six", "shorthand": "s6", "tag": "student six" }, + { "category": "Student Six", "shorthand": "ga", "tag": "gallus" }, + { "category": "Student Six", "shorthand": "ols", "tag": "ocellus" }, + { "category": "Student Six", "shorthand": "snb", "tag": "sandbar" }, + { "category": "Student Six", "shorthand": "svs", "tag": "silverstream" }, + { "category": "Student Six", "shorthand": "sm", "tag": "smolder" }, + { "category": "Student Six", "shorthand": "yn", "tag": "yona" }, + { "category": "Uncategorized", "shorthand": "maud", "tag": "maud pie" }, + { "category": "Uncategorized", "shorthand": "coco", "tag": "coco pommel" }, + { "category": "Uncategorized", "shorthand": "suri", "tag": "suri polomare" }, + { "category": "Uncategorized", "shorthand": "rg", "tag": "royal guard" }, + { "category": "Uncategorized", "shorthand": "za", "tag": "zecora" }, + { "category": "Uncategorized", "shorthand": "mm", "tag": "mayor mare" }, + { "category": "Uncategorized", "shorthand": "pdp", "tag": "pinkamena diane pie" }, + { "category": "Uncategorized", "shorthand": "owol", "tag": "owlowiscious" }, + { "category": "Uncategorized", "shorthand": "opal", "tag": "opalescence" }, + { "category": "Other Things", "shorthand": "cm", "tag": "cutie mark" }, + { "category": "Other Things", "shorthand": "eoh", "tag": "elements of harmony" }, + { "category": "Other Things", "shorthand": "nmn", "tag": "nightmare night" } + ], + "shipping_quick_tags": [ + { + "tab_title": "Ships ts", + "category": "Ships ts", + "implying": ["shipping", "twilight sparkle"], + "not_implying": [] + }, + { + "tab_title": "rd", + "category": "rd", + "implying": ["shipping", "rainbow dash"], + "not_implying": [] + }, + { + "tab_title": "ry", + "category": "ry", + "implying": ["shipping", "rarity"], + "not_implying": [] + }, + { + "tab_title": "aj", + "category": "aj", + "implying": ["shipping", "applejack"], + "not_implying": [] + }, + { + "tab_title": "fs", + "category": "fs", + "implying": ["shipping", "fluttershy"], + "not_implying": [] + }, + { + "tab_title": "pp", + "category": "pp", + "implying": ["shipping", "pinkie pie"], + "not_implying": [] + }, + { + "tab_title": "misc", + "category": "misc", + "implying": ["shipping"], + "not_implying": ["twilight sparkle", "rainbow dash", "rarity", "applejack", "fluttershy", "pinkie pie"] + } + ], + "season_quick_tags": [ + { "tab_title": "Season 9", "episode": 1, "tag": "the beginning of the end" }, + { "tab_title": "Season 9", "episode": 2, "tag": "the beginning of the end" }, + { "tab_title": "Season 9", "episode": 3, "tag": "uprooted" }, + { "tab_title": "Season 9", "episode": 4, "tag": "twilight's seven" }, + { "tab_title": "Season 9", "episode": 5, "tag": "the point of no return" }, + { "tab_title": "Season 9", "episode": 6, "tag": "common ground" }, + { "tab_title": "Season 9", "episode": 7, "tag": "she's all yak" }, + { "tab_title": "Season 9", "episode": 8, "tag": "frenemies (episode)" }, + { "tab_title": "Season 9", "episode": 9, "tag": "sweet and smoky" }, + { "tab_title": "Season 9", "episode": 10, "tag": "going to seed" }, + { "tab_title": "Season 9", "episode": 11, "tag": "student counsel" }, + { "tab_title": "Season 9", "episode": 12, "tag": "the last crusade (episode)" }, + { "tab_title": "Season 9", "episode": 13, "tag": "between dark and dawn" }, + { "tab_title": "Season 9", "episode": 14, "tag": "that's a laugh" }, + { "tab_title": "Season 9", "episode": 15, "tag": "2 4 6 greaaat" }, + { "tab_title": "Season 9", "episode": 16, "tag": "a trivial pursuit" }, + { "tab_title": "Season 9", "episode": 17, "tag": "the summer sun setback" }, + { "tab_title": "Season 9", "episode": 18, "tag": "she talks to angel" }, + { "tab_title": "Season 9", "episode": 19, "tag": "dragon dropped" }, + { "tab_title": "Season 9", "episode": 20, "tag": "a horse shoe-in" }, + { "tab_title": "Season 9", "episode": 21, "tag": "daring doubt" }, + { "tab_title": "Season 9", "episode": 22, "tag": "growing up is hard to do" }, + { "tab_title": "Season 9", "episode": 23, "tag": "the big mac question" }, + { "tab_title": "Season 9", "episode": 24, "tag": "the ending of the end" }, + { "tab_title": "Season 9", "episode": 25, "tag": "the ending of the end" }, + { "tab_title": "Season 9", "episode": 26, "tag": "the last problem" }, + { "tab_title": "8", "episode": 1, "tag": "school daze" }, + { "tab_title": "8", "episode": 2, "tag": "school daze" }, + { "tab_title": "8", "episode": 3, "tag": "the maud couple" }, + { "tab_title": "8", "episode": 4, "tag": "fake it 'til you make it" }, + { "tab_title": "8", "episode": 5, "tag": "grannies gone wild" }, + { "tab_title": "8", "episode": 6, "tag": "surf and/or turf" }, + { "tab_title": "8", "episode": 7, "tag": "horse play" }, + { "tab_title": "8", "episode": 8, "tag": "the parent map" }, + { "tab_title": "8", "episode": 9, "tag": "non-compete clause" }, + { "tab_title": "8", "episode": 10, "tag": "the break up breakdown" }, + { "tab_title": "8", "episode": 11, "tag": "molt down" }, + { "tab_title": "8", "episode": 12, "tag": "marks for effort" }, + { "tab_title": "8", "episode": 13, "tag": "the mean 6" }, + { "tab_title": "8", "episode": 14, "tag": "a matter of principals" }, + { "tab_title": "8", "episode": 15, "tag": "the hearth's warming club" }, + { "tab_title": "8", "episode": 16, "tag": "friendship university" }, + { "tab_title": "8", "episode": 17, "tag": "the end in friend" }, + { "tab_title": "8", "episode": 18, "tag": "yakity-sax" }, + { "tab_title": "8", "episode": 19, "tag": "on the road to friendship" }, + { "tab_title": "8", "episode": 20, "tag": "the washouts (episode)" }, + { "tab_title": "8", "episode": 21, "tag": "a rockhoof and a hard place" }, + { "tab_title": "8", "episode": 22, "tag": "what lies beneath" }, + { "tab_title": "8", "episode": 23, "tag": "sounds of silence" }, + { "tab_title": "8", "episode": 24, "tag": "father knows beast" }, + { "tab_title": "8", "episode": 25, "tag": "school raze" }, + { "tab_title": "8", "episode": 26, "tag": "school raze" }, + { "tab_title": "8", "episode": 0, "tag": "best gift ever" }, + { "tab_title": "7", "episode": 1, "tag": "celestial advice" }, + { "tab_title": "7", "episode": 2, "tag": "all bottled up" }, + { "tab_title": "7", "episode": 3, "tag": "a flurry of emotions" }, + { "tab_title": "7", "episode": 4, "tag": "rock solid friendship" }, + { "tab_title": "7", "episode": 5, "tag": "fluttershy leans in" }, + { "tab_title": "7", "episode": 6, "tag": "forever filly" }, + { "tab_title": "7", "episode": 7, "tag": "parental glideance" }, + { "tab_title": "7", "episode": 8, "tag": "hard to say anything" }, + { "tab_title": "7", "episode": 9, "tag": "honest apple" }, + { "tab_title": "7", "episode": 10, "tag": "a royal problem" }, + { "tab_title": "7", "episode": 11, "tag": "not asking for trouble" }, + { "tab_title": "7", "episode": 12, "tag": "discordant harmony" }, + { "tab_title": "7", "episode": 13, "tag": "the perfect pear" }, + { "tab_title": "7", "episode": 14, "tag": "fame and misfortune" }, + { "tab_title": "7", "episode": 15, "tag": "triple threat" }, + { "tab_title": "7", "episode": 16, "tag": "campfire tales" }, + { "tab_title": "7", "episode": 17, "tag": "to change a changeling" }, + { "tab_title": "7", "episode": 18, "tag": "daring done?" }, + { "tab_title": "7", "episode": 19, "tag": "it isn't the mane thing about you" }, + { "tab_title": "7", "episode": 20, "tag": "a health of information" }, + { "tab_title": "7", "episode": 21, "tag": "marks and recreation" }, + { "tab_title": "7", "episode": 22, "tag": "once upon a zeppelin" }, + { "tab_title": "7", "episode": 23, "tag": "secrets and pies" }, + { "tab_title": "7", "episode": 24, "tag": "uncommon bond" }, + { "tab_title": "7", "episode": 25, "tag": "shadow play" }, + { "tab_title": "7", "episode": 26, "tag": "shadow play" }, + { "tab_title": "6", "episode": 1, "tag": "the crystalling" }, + { "tab_title": "6", "episode": 2, "tag": "the crystalling" }, + { "tab_title": "6", "episode": 3, "tag": "the gift of the maud pie" }, + { "tab_title": "6", "episode": 4, "tag": "on your marks" }, + { "tab_title": "6", "episode": 5, "tag": "gauntlet of fire" }, + { "tab_title": "6", "episode": 6, "tag": "no second prances" }, + { "tab_title": "6", "episode": 7, "tag": "newbie dash" }, + { "tab_title": "6", "episode": 8, "tag": "a hearth's warming tail" }, + { "tab_title": "6", "episode": 9, "tag": "the saddle row review" }, + { "tab_title": "6", "episode": 10, "tag": "applejack's \"day\" off" }, + { "tab_title": "6", "episode": 11, "tag": "flutter brutter" }, + { "tab_title": "6", "episode": 12, "tag": "spice up your life" }, + { "tab_title": "6", "episode": 13, "tag": "stranger than fan fiction" }, + { "tab_title": "6", "episode": 14, "tag": "the cart before the ponies" }, + { "tab_title": "6", "episode": 15, "tag": "28 pranks later" }, + { "tab_title": "6", "episode": 16, "tag": "the times they are a changeling" }, + { "tab_title": "6", "episode": 17, "tag": "dungeons and discords" }, + { "tab_title": "6", "episode": 18, "tag": "buckball season" }, + { "tab_title": "6", "episode": 19, "tag": "the fault in our cutie marks" }, + { "tab_title": "6", "episode": 20, "tag": "viva las pegasus" }, + { "tab_title": "6", "episode": 21, "tag": "every little thing she does" }, + { "tab_title": "6", "episode": 22, "tag": "ppov" }, + { "tab_title": "6", "episode": 23, "tag": "where the apple lies" }, + { "tab_title": "6", "episode": 24, "tag": "top bolt" }, + { "tab_title": "6", "episode": 25, "tag": "to where and back again" }, + { "tab_title": "6", "episode": 26, "tag": "to where and back again" }, + { "tab_title": "5", "episode": 1, "tag": "the cutie map" }, + { "tab_title": "5", "episode": 2, "tag": "the cutie map" }, + { "tab_title": "5", "episode": 3, "tag": "castle sweet castle" }, + { "tab_title": "5", "episode": 4, "tag": "bloom and gloom" }, + { "tab_title": "5", "episode": 5, "tag": "tanks for the memories" }, + { "tab_title": "5", "episode": 6, "tag": "appleoosa's most wanted" }, + { "tab_title": "5", "episode": 7, "tag": "make new friends but keep discord" }, + { "tab_title": "5", "episode": 8, "tag": "the lost treasure of griffonstone" }, + { "tab_title": "5", "episode": 9, "tag": "slice of life (episode)" }, + { "tab_title": "5", "episode": 10, "tag": "princess spike" }, + { "tab_title": "5", "episode": 11, "tag": "party pooped" }, + { "tab_title": "5", "episode": 12, "tag": "amending fences" }, + { "tab_title": "5", "episode": 13, "tag": "do princesses dream of magic sheep" }, + { "tab_title": "5", "episode": 14, "tag": "canterlot boutique" }, + { "tab_title": "5", "episode": 15, "tag": "rarity investigates" }, + { "tab_title": "5", "episode": 16, "tag": "made in manehattan" }, + { "tab_title": "5", "episode": 17, "tag": "brotherhooves social" }, + { "tab_title": "5", "episode": 18, "tag": "crusaders of the lost mark" }, + { "tab_title": "5", "episode": 19, "tag": "the one where pinkie pie knows" }, + { "tab_title": "5", "episode": 20, "tag": "hearthbreakers" }, + { "tab_title": "5", "episode": 21, "tag": "scare master" }, + { "tab_title": "5", "episode": 22, "tag": "what about discord?" }, + { "tab_title": "5", "episode": 23, "tag": "the hooffields and mccolts" }, + { "tab_title": "5", "episode": 24, "tag": "the mane attraction" }, + { "tab_title": "5", "episode": 25, "tag": "the cutie remark" }, + { "tab_title": "5", "episode": 26, "tag": "the cutie remark" }, + { "tab_title": "4", "episode": 1, "tag": "princess twilight sparkle (episode)" }, + { "tab_title": "4", "episode": 2, "tag": "princess twilight sparkle (episode)" }, + { "tab_title": "4", "episode": 3, "tag": "castle mane-ia" }, + { "tab_title": "4", "episode": 4, "tag": "daring don't" }, + { "tab_title": "4", "episode": 5, "tag": "flight to the finish" }, + { "tab_title": "4", "episode": 6, "tag": "power ponies" }, + { "tab_title": "4", "episode": 7, "tag": "bats!" }, + { "tab_title": "4", "episode": 8, "tag": "rarity takes manehattan" }, + { "tab_title": "4", "episode": 9, "tag": "pinkie apple pie" }, + { "tab_title": "4", "episode": 10, "tag": "rainbow falls" }, + { "tab_title": "4", "episode": 11, "tag": "three's a crowd" }, + { "tab_title": "4", "episode": 12, "tag": "pinkie pride" }, + { "tab_title": "4", "episode": 13, "tag": "simple ways" }, + { "tab_title": "4", "episode": 14, "tag": "filli vanilli" }, + { "tab_title": "4", "episode": 15, "tag": "twilight time" }, + { "tab_title": "4", "episode": 16, "tag": "it ain't easy being breezies" }, + { "tab_title": "4", "episode": 17, "tag": "somepony to watch over me" }, + { "tab_title": "4", "episode": 18, "tag": "maud pie (episode)" }, + { "tab_title": "4", "episode": 19, "tag": "for whom the sweetie belle toils" }, + { "tab_title": "4", "episode": 20, "tag": "leap of faith" }, + { "tab_title": "4", "episode": 21, "tag": "testing testing 1-2-3" }, + { "tab_title": "4", "episode": 22, "tag": "trade ya" }, + { "tab_title": "4", "episode": 23, "tag": "inspiration manifestation" }, + { "tab_title": "4", "episode": 24, "tag": "equestria games (episode)" }, + { "tab_title": "4", "episode": 25, "tag": "twilight's kingdom" }, + { "tab_title": "4", "episode": 26, "tag": "twilight's kingdom" }, + { "tab_title": "3", "episode": 1, "tag": "the crystal empire" }, + { "tab_title": "3", "episode": 2, "tag": "the crystal empire" }, + { "tab_title": "3", "episode": 3, "tag": "too many pinkie pies" }, + { "tab_title": "3", "episode": 4, "tag": "one bad apple" }, + { "tab_title": "3", "episode": 5, "tag": "magic duel" }, + { "tab_title": "3", "episode": 6, "tag": "sleepless in ponyville" }, + { "tab_title": "3", "episode": 7, "tag": "wonderbolts academy" }, + { "tab_title": "3", "episode": 8, "tag": "just for sidekicks" }, + { "tab_title": "3", "episode": 9, "tag": "apple family reunion" }, + { "tab_title": "3", "episode": 10, "tag": "spike at your service" }, + { "tab_title": "3", "episode": 11, "tag": "keep calm and flutter on" }, + { "tab_title": "3", "episode": 12, "tag": "games ponies play" }, + { "tab_title": "3", "episode": 13, "tag": "magical mystery cure" }, + { "tab_title": "2", "episode": 1, "tag": "the return of harmony" }, + { "tab_title": "2", "episode": 2, "tag": "the return of harmony" }, + { "tab_title": "2", "episode": 3, "tag": "lesson zero" }, + { "tab_title": "2", "episode": 4, "tag": "luna eclipsed" }, + { "tab_title": "2", "episode": 5, "tag": "sisterhooves social" }, + { "tab_title": "2", "episode": 6, "tag": "the cutie pox" }, + { "tab_title": "2", "episode": 7, "tag": "may the best pet win" }, + { "tab_title": "2", "episode": 8, "tag": "the mysterious mare do well" }, + { "tab_title": "2", "episode": 9, "tag": "sweet and elite" }, + { "tab_title": "2", "episode": 10, "tag": "secret of my excess" }, + { "tab_title": "2", "episode": 11, "tag": "family appreciation day" }, + { "tab_title": "2", "episode": 12, "tag": "baby cakes" }, + { "tab_title": "2", "episode": 13, "tag": "hearth's warming eve (episode)" }, + { "tab_title": "2", "episode": 14, "tag": "the last roundup" }, + { "tab_title": "2", "episode": 15, "tag": "the super speedy cider squeezy 6000" }, + { "tab_title": "2", "episode": 16, "tag": "read it and weep" }, + { "tab_title": "2", "episode": 17, "tag": "hearts and hooves day (episode)" }, + { "tab_title": "2", "episode": 18, "tag": "a friend in deed" }, + { "tab_title": "2", "episode": 19, "tag": "putting your hoof down" }, + { "tab_title": "2", "episode": 20, "tag": "it's about time" }, + { "tab_title": "2", "episode": 21, "tag": "dragon quest" }, + { "tab_title": "2", "episode": 22, "tag": "hurricane fluttershy" }, + { "tab_title": "2", "episode": 23, "tag": "ponyville confidential" }, + { "tab_title": "2", "episode": 24, "tag": "mmmystery on the friendship express" }, + { "tab_title": "2", "episode": 25, "tag": "a canterlot wedding" }, + { "tab_title": "2", "episode": 26, "tag": "a canterlot wedding" }, + { "tab_title": "1", "episode": 1, "tag": "friendship is magic" }, + { "tab_title": "1", "episode": 2, "tag": "friendship is magic" }, + { "tab_title": "1", "episode": 3, "tag": "the ticket master" }, + { "tab_title": "1", "episode": 4, "tag": "applebuck season" }, + { "tab_title": "1", "episode": 5, "tag": "griffon the brush off" }, + { "tab_title": "1", "episode": 6, "tag": "boast busters" }, + { "tab_title": "1", "episode": 7, "tag": "dragonshy" }, + { "tab_title": "1", "episode": 8, "tag": "look before you sleep" }, + { "tab_title": "1", "episode": 9, "tag": "bridle gossip" }, + { "tab_title": "1", "episode": 10, "tag": "swarm of the century" }, + { "tab_title": "1", "episode": 11, "tag": "winter wrap up" }, + { "tab_title": "1", "episode": 12, "tag": "call of the cutie" }, + { "tab_title": "1", "episode": 13, "tag": "fall weather friends" }, + { "tab_title": "1", "episode": 14, "tag": "suited for success" }, + { "tab_title": "1", "episode": 15, "tag": "feeling pinkie keen" }, + { "tab_title": "1", "episode": 16, "tag": "sonic rainboom (episode)" }, + { "tab_title": "1", "episode": 17, "tag": "stare master" }, + { "tab_title": "1", "episode": 18, "tag": "the show stoppers" }, + { "tab_title": "1", "episode": 19, "tag": "a dog and pony show" }, + { "tab_title": "1", "episode": 20, "tag": "green isn't your color" }, + { "tab_title": "1", "episode": 21, "tag": "over a barrel" }, + { "tab_title": "1", "episode": 22, "tag": "a bird in the hoof" }, + { "tab_title": "1", "episode": 23, "tag": "the cutie mark chronicles" }, + { "tab_title": "1", "episode": 24, "tag": "owl's well that ends well" }, + { "tab_title": "1", "episode": 25, "tag": "party of one" }, + { "tab_title": "1", "episode": 26, "tag": "the best night ever" } + ] +} diff --git a/priv/repo/seeds/data/rating_tags.json b/priv/repo/seeds/data/rating_tags.json new file mode 100644 index 000000000..9e301da33 --- /dev/null +++ b/priv/repo/seeds/data/rating_tags.json @@ -0,0 +1 @@ +["safe", "suggestive", "questionable", "explicit", "semi-grimdark", "grimdark", "grotesque"] diff --git a/priv/repo/seeds/data/roles.json b/priv/repo/seeds/data/roles.json new file mode 100644 index 000000000..b71b664aa --- /dev/null +++ b/priv/repo/seeds/data/roles.json @@ -0,0 +1,17 @@ +[ + { "name": "moderator", "resource_type": "Image" }, + { "name": "moderator", "resource_type": "DuplicateReport" }, + { "name": "moderator", "resource_type": "Comment" }, + { "name": "moderator", "resource_type": "Tag" }, + { "name": "moderator", "resource_type": "ArtistLink" }, + { "name": "admin", "resource_type": "Tag" }, + { "name": "moderator", "resource_type": "User" }, + { "name": "admin", "resource_type": "SiteNotice" }, + { "name": "admin", "resource_type": "Badge" }, + { "name": "admin", "resource_type": "Role" }, + { "name": "batch_update", "resource_type": "Tag" }, + { "name": "moderator", "resource_type": "Topic" }, + { "name": "admin", "resource_type": "Advert" }, + { "name": "admin", "resource_type": "StaticPage" }, + { "name": "admin", "resource_type": "Image" } +] diff --git a/priv/repo/seeds/data/system_filters.json b/priv/repo/seeds/data/system_filters.json new file mode 100644 index 000000000..475806fab --- /dev/null +++ b/priv/repo/seeds/data/system_filters.json @@ -0,0 +1,14 @@ +[ + { + "name": "Default", + "description": "The site's default filter.", + "hidden": ["explicit", "grotesque"], + "spoilered": ["questionable"] + }, + { + "name": "Everything", + "description": "This filter won't filter out anything at all.", + "hidden": [], + "spoilered": [] + } +] diff --git a/priv/repo/seeds/data/users.json b/priv/repo/seeds/data/users.json new file mode 100644 index 000000000..066c52d0b --- /dev/null +++ b/priv/repo/seeds/data/users.json @@ -0,0 +1,8 @@ +[ + { + "name": "Administrator", + "email": "admin@example.com", + "password": "philomena123", + "role": "admin" + } +] diff --git a/assets/static/favicon.ico b/priv/repo/seeds/system_images/favicon.ico similarity index 100% rename from assets/static/favicon.ico rename to priv/repo/seeds/system_images/favicon.ico diff --git a/assets/static/favicon.svg b/priv/repo/seeds/system_images/favicon.svg similarity index 100% rename from assets/static/favicon.svg rename to priv/repo/seeds/system_images/favicon.svg diff --git a/assets/static/images/no_avatar.svg b/priv/repo/seeds/system_images/no_avatar.svg similarity index 100% rename from assets/static/images/no_avatar.svg rename to priv/repo/seeds/system_images/no_avatar.svg diff --git a/assets/static/images/tagblocked.svg b/priv/repo/seeds/system_images/tagblocked.svg similarity index 100% rename from assets/static/images/tagblocked.svg rename to priv/repo/seeds/system_images/tagblocked.svg diff --git a/priv/repo/seeds_development.exs b/priv/repo/seeds_development.exs index 1492e341d..633656f5b 100644 --- a/priv/repo/seeds_development.exs +++ b/priv/repo/seeds_development.exs @@ -20,13 +20,18 @@ alias Philomena.Tags {:ok, ip} = EctoNetwork.INET.cast({203, 0, 113, 0}) {:ok, _} = Application.ensure_all_started(:plug) -resources = - "priv/repo/seeds_development.json" - |> File.read!() - |> JSON.decode!() +defmodule Philomena.DevSeedLoader do + def load_resource(res) do + "priv/repo/seeds/data/development/#{res}.json" + |> File.read!() + |> JSON.decode!() + end +end + +alias Philomena.DevSeedLoader IO.puts "---- Generating users" -for user_def <- resources["users"] do +for user_def <- DevSeedLoader.load_resource("users") do {:ok, user} = Users.register_user(user_def) user @@ -45,7 +50,7 @@ request_attributes = [ ] IO.puts "---- Generating images" -for image_def <- resources["remote_images"] do +for image_def <- DevSeedLoader.load_resource("images") do file = Briefly.create!(extname: ".png") now = DateTime.utc_now() |> DateTime.to_unix(:microsecond) @@ -80,7 +85,7 @@ for image_def <- resources["remote_images"] do end IO.puts "---- Generating comments for image #1" -for comment_body <- resources["comments"] do +for comment_body <- DevSeedLoader.load_resource("comments") do image = Images.get_image!(1) Comments.create_comment( @@ -100,7 +105,7 @@ for comment_body <- resources["comments"] do end IO.puts "---- Generating forum posts" -for %{"forum" => forum_name, "topics" => topics} <- resources["forum_posts"] do +for %{"forum" => forum_name, "topics" => topics} <- DevSeedLoader.load_resource("forum_posts") do forum = Repo.get_by!(Forum, short_name: forum_name) for %{"title" => topic_name, "posts" => [first_post | posts]} <- topics do diff --git a/priv/repo/seeds_development.json b/priv/repo/seeds_development.json deleted file mode 100644 index 4788b0f5d..000000000 --- a/priv/repo/seeds_development.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "users": [ - { - "name": "Hot Pocket Consumer", - "email": "moderator@example.com", - "password": "philomena123", - "role": "moderator" - }, - { - "name": "Hoping For a Promotion", - "email": "assistant@example.com", - "password": "philomena123", - "role": "assistant" - }, - { - "name": "Pleb", - "email": "user@example.com", - "password": "philomena123", - "role": "user" - } - ], - "remote_images": [ - { - "url": "https://derpicdn.net/img/2015/9/26/988000/thumb.gif", - "sources": ["https://derpibooru.org/988000"], - "description": "Fairly large GIF (~23MB), use to test WebM stuff.", - "tag_input": "alicorn, angry, animated, art, artist:assasinmonkey, artist:equum_amici, badass, barrier, crying, dark, epic, female, fight, force field, glare, glow, good vs evil, lord tirek, low angle, magic, mare, messy mane, metal as fuck, perspective, plot, pony, raised hoof, safe, size difference, spread wings, stomping, twilight's kingdom, twilight sparkle, twilight sparkle (alicorn), twilight vs tirek, underhoof" - }, - { - "url": "https://derpicdn.net/img/2012/1/2/25/large.png", - "sources": ["https://derpibooru.org/25"], - "tag_input": "artist:moe, canterlot, castle, cliff, cloud, detailed background, fog, forest, grass, mountain, mountain range, nature, no pony, outdoors, path, river, safe, scenery, scenery porn, signature, source needed, sunset, technical advanced, town, tree, useless source url, water, waterfall, widescreen, wood" - }, - { - "url": "https://derpicdn.net/img/2018/6/28/1767886/full.webm", - "sources": ["http://hydrusbeta.deviantart.com/art/Gleaming-in-the-Sun-Our-Colors-Shine-in-Every-Hue-611497309"], - "tag_input": "3d, animated, architecture, artist:hydrusbeta, castle, cloud, crystal empire, crystal palace, flag, flag waving, no pony, no sound, safe, scenery, webm" - }, - { - "url": "https://derpicdn.net/img/view/2015/2/19/832750.jpg", - "sources": [ - "http://sovietrussianbrony.tumblr.com/post/111504505079/this-image-actually-took-me-ages-to-edit-the" - ], - "tag_input": "artist:rhads, artist:the sexy assistant, canterlot, cloud, cloudsdale, cloudy, edit, lens flare, no pony, ponyville, rainbow, river, safe, scenery, sweet apple acres" - }, - { - "url": "https://derpicdn.net/img/view/2016/3/17/1110529.jpg", - "sources": ["https://www.deviantart.com/devinian/art/Commission-Crystals-of-thy-heart-511134926"], - "tag_input": "artist:devinian, aurora crystialis, bridge, cloud, crepuscular rays, crystal empire, crystal palace, edit, flower, forest, grass, log, mountain, no pony, river, road, safe, scenery, scenery porn, source needed, stars, sunset, swing, tree, wallpaper" - }, - { - "url": "https://derpicdn.net/img/view/2019/6/16/2067468.svg", - "sources": ["https://derpibooru.org/2067468"], - "tag_input": "artist:cheezedoodle96, babs seed, bloom and gloom, cutie mark, cutie mark only, no pony, safe, scissors, simple background, svg, .svg available, transparent background, vector" - } - ], - "comments": [ - "bold is **bold**, italic is _italic_, spoiler is ||spoiler||, code is `code`, underline is __underline__, strike is ~~strike~~, sup is ^sup^, sub is %sub%.", - "inline embedded thumbnails (tsp): >>1t >>1s >>1p", - "embedded image inside a spoiler: ||who needs it anyway >>1s||", - "spoilers inside of a table\n\nHello | World\n--- | ---:\n`||cool beans!||` | ||cool beans!||" - ], - "forum_posts": [ - { - "forum": "dis", - "topics": [ - { - "title": "Example Topic", - "posts": ["example post", "yet another example post"] - }, - { - "title": "Second Example Topic", - "posts": ["post", "post 2"] - } - ] - }, - { - "forum": "art", - "topics": [ - { - "title": "Embedded Images", - "posts": [">>1t >>1s >>1p", ">>1", "non-existent: >>1000t >>1000s >>1000p >>1000"] - } - ] - } - ] -} diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 4314feb39..66f6abf63 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -2,7 +2,7 @@ -- PostgreSQL database dump -- -\restrict HaHFW4uoQTYEW2yFV8zxsi1boGlZaAq7wPC4PqUZeQq9dkvLFwHwwFxqqPeF0LZ +\restrict 9uxDdny3Bpcuj1Rkc9wlUPdpby5cuWuksIxgFIdKdkceoDa1fmrKWPlCr9UaBJ8 -- Dumped from database version 18.0 -- Dumped by pg_dump version 18.0 @@ -127,6 +127,126 @@ CREATE TABLE public.autocomplete ( ); +-- +-- Name: avatar_kinds; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.avatar_kinds ( + id bigint NOT NULL, + name character varying(255) NOT NULL +); + + +-- +-- Name: avatar_kinds_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.avatar_kinds_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: avatar_kinds_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.avatar_kinds_id_seq OWNED BY public.avatar_kinds.id; + + +-- +-- Name: avatar_parts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.avatar_parts ( + id bigint NOT NULL, + name character varying(255) NOT NULL, + priority integer DEFAULT 1 NOT NULL +); + + +-- +-- Name: avatar_parts_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.avatar_parts_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: avatar_parts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.avatar_parts_id_seq OWNED BY public.avatar_parts.id; + + +-- +-- Name: avatar_shape_kinds; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.avatar_shape_kinds ( + id bigint NOT NULL, + avatar_shape_id bigint NOT NULL, + avatar_kind_id bigint NOT NULL +); + + +-- +-- Name: avatar_shape_kinds_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.avatar_shape_kinds_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: avatar_shape_kinds_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.avatar_shape_kinds_id_seq OWNED BY public.avatar_shape_kinds.id; + + +-- +-- Name: avatar_shapes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.avatar_shapes ( + id bigint NOT NULL, + avatar_part_id bigint NOT NULL, + shape character varying(255) NOT NULL, + any_kind boolean DEFAULT false NOT NULL +); + + +-- +-- Name: avatar_shapes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.avatar_shapes_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: avatar_shapes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.avatar_shapes_id_seq OWNED BY public.avatar_shapes.id; + + -- -- Name: badge_awards; Type: TABLE; Schema: public; Owner: - -- @@ -379,6 +499,36 @@ CREATE SEQUENCE public.commissions_id_seq ALTER SEQUENCE public.commissions_id_seq OWNED BY public.commissions.id; +-- +-- Name: configs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.configs ( + id bigint NOT NULL, + key character varying(255) NOT NULL, + value character varying(255) NOT NULL +); + + +-- +-- Name: configs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.configs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: configs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.configs_id_seq OWNED BY public.configs.id; + + -- -- Name: conversations; Type: TABLE; Schema: public; Owner: - -- @@ -418,6 +568,37 @@ CREATE SEQUENCE public.conversations_id_seq ALTER SEQUENCE public.conversations_id_seq OWNED BY public.conversations.id; +-- +-- Name: default_quick_tags; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.default_quick_tags ( + id bigint NOT NULL, + quick_tag_tab_id bigint NOT NULL, + category character varying(255) NOT NULL, + tags character varying(255)[] NOT NULL +); + + +-- +-- Name: default_quick_tags_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.default_quick_tags_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: default_quick_tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.default_quick_tags_id_seq OWNED BY public.default_quick_tags.id; + + -- -- Name: dnp_entries; Type: TABLE; Schema: public; Owner: - -- @@ -609,6 +790,70 @@ CREATE SEQUENCE public.fingerprint_bans_id_seq ALTER SEQUENCE public.fingerprint_bans_id_seq OWNED BY public.fingerprint_bans.id; +-- +-- Name: footer_categories; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.footer_categories ( + id bigint NOT NULL, + title character varying(255) NOT NULL, + "position" integer NOT NULL +); + + +-- +-- Name: footer_categories_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.footer_categories_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: footer_categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.footer_categories_id_seq OWNED BY public.footer_categories.id; + + +-- +-- Name: footer_links; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.footer_links ( + id bigint NOT NULL, + footer_category_id bigint NOT NULL, + title character varying(255) NOT NULL, + url character varying(255) NOT NULL, + "position" integer NOT NULL, + bold boolean DEFAULT false NOT NULL, + new_tab boolean DEFAULT false NOT NULL +); + + +-- +-- Name: footer_links_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.footer_links_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: footer_links_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.footer_links_id_seq OWNED BY public.footer_links.id; + + -- -- Name: forum_post_notifications; Type: TABLE; Schema: public; Owner: - -- @@ -1331,6 +1576,36 @@ CREATE SEQUENCE public.posts_id_seq ALTER SEQUENCE public.posts_id_seq OWNED BY public.posts.id; +-- +-- Name: quick_tag_tabs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.quick_tag_tabs ( + id bigint NOT NULL, + title character varying(255) NOT NULL, + "position" integer NOT NULL +); + + +-- +-- Name: quick_tag_tabs_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.quick_tag_tabs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: quick_tag_tabs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.quick_tag_tabs_id_seq OWNED BY public.quick_tag_tabs.id; + + -- -- Name: reports; Type: TABLE; Schema: public; Owner: - -- @@ -1412,6 +1687,130 @@ CREATE TABLE public.schema_migrations ( ); +-- +-- Name: season_quick_tags; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.season_quick_tags ( + id bigint NOT NULL, + quick_tag_tab_id bigint NOT NULL, + episode integer NOT NULL, + tag character varying(255) NOT NULL +); + + +-- +-- Name: season_quick_tags_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.season_quick_tags_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: season_quick_tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.season_quick_tags_id_seq OWNED BY public.season_quick_tags.id; + + +-- +-- Name: shipping_quick_tags; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.shipping_quick_tags ( + id bigint NOT NULL, + quick_tag_tab_id bigint NOT NULL, + category character varying(255) NOT NULL, + implying character varying(255)[] DEFAULT ARRAY[]::character varying[], + not_implying character varying(255)[] DEFAULT ARRAY[]::character varying[] +); + + +-- +-- Name: shipping_quick_tags_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.shipping_quick_tags_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: shipping_quick_tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.shipping_quick_tags_id_seq OWNED BY public.shipping_quick_tags.id; + + +-- +-- Name: shorthand_quick_tag_categories; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.shorthand_quick_tag_categories ( + id bigint NOT NULL, + quick_tag_tab_id bigint NOT NULL, + category character varying(255) NOT NULL +); + + +-- +-- Name: shorthand_quick_tag_categories_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.shorthand_quick_tag_categories_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: shorthand_quick_tag_categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.shorthand_quick_tag_categories_id_seq OWNED BY public.shorthand_quick_tag_categories.id; + + +-- +-- Name: shorthand_quick_tags; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.shorthand_quick_tags ( + id bigint NOT NULL, + shorthand_quick_tag_category_id bigint NOT NULL, + shorthand character varying(255) NOT NULL, + tag character varying(255) NOT NULL +); + + +-- +-- Name: shorthand_quick_tags_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.shorthand_quick_tags_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: shorthand_quick_tags_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.shorthand_quick_tags_id_seq OWNED BY public.shorthand_quick_tags.id; + + -- -- Name: site_notices; Type: TABLE; Schema: public; Owner: - -- @@ -1592,6 +1991,35 @@ CREATE SEQUENCE public.subnet_bans_id_seq ALTER SEQUENCE public.subnet_bans_id_seq OWNED BY public.subnet_bans.id; +-- +-- Name: system_images; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.system_images ( + id bigint NOT NULL, + key character varying(255) NOT NULL +); + + +-- +-- Name: system_images_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.system_images_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: system_images_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.system_images_id_seq OWNED BY public.system_images.id; + + -- -- Name: tag_change_tags; Type: TABLE; Schema: public; Owner: - -- @@ -1695,7 +2123,8 @@ CREATE TABLE public.tags ( updated_at timestamp without time zone NOT NULL, category character varying, mod_notes character varying, - description character varying DEFAULT ''::character varying NOT NULL + description character varying DEFAULT ''::character varying NOT NULL, + invalid boolean DEFAULT false NOT NULL ); @@ -2221,6 +2650,34 @@ ALTER TABLE ONLY public.adverts ALTER COLUMN id SET DEFAULT nextval('public.adve ALTER TABLE ONLY public.artist_links ALTER COLUMN id SET DEFAULT nextval('public.artist_links_id_seq'::regclass); +-- +-- Name: avatar_kinds id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_kinds ALTER COLUMN id SET DEFAULT nextval('public.avatar_kinds_id_seq'::regclass); + + +-- +-- Name: avatar_parts id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_parts ALTER COLUMN id SET DEFAULT nextval('public.avatar_parts_id_seq'::regclass); + + +-- +-- Name: avatar_shape_kinds id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_shape_kinds ALTER COLUMN id SET DEFAULT nextval('public.avatar_shape_kinds_id_seq'::regclass); + + +-- +-- Name: avatar_shapes id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_shapes ALTER COLUMN id SET DEFAULT nextval('public.avatar_shapes_id_seq'::regclass); + + -- -- Name: badge_awards id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2263,6 +2720,13 @@ ALTER TABLE ONLY public.commission_items ALTER COLUMN id SET DEFAULT nextval('pu ALTER TABLE ONLY public.commissions ALTER COLUMN id SET DEFAULT nextval('public.commissions_id_seq'::regclass); +-- +-- Name: configs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.configs ALTER COLUMN id SET DEFAULT nextval('public.configs_id_seq'::regclass); + + -- -- Name: conversations id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2270,6 +2734,13 @@ ALTER TABLE ONLY public.commissions ALTER COLUMN id SET DEFAULT nextval('public. ALTER TABLE ONLY public.conversations ALTER COLUMN id SET DEFAULT nextval('public.conversations_id_seq'::regclass); +-- +-- Name: default_quick_tags id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.default_quick_tags ALTER COLUMN id SET DEFAULT nextval('public.default_quick_tags_id_seq'::regclass); + + -- -- Name: dnp_entries id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2305,6 +2776,20 @@ ALTER TABLE ONLY public.filters ALTER COLUMN id SET DEFAULT nextval('public.filt ALTER TABLE ONLY public.fingerprint_bans ALTER COLUMN id SET DEFAULT nextval('public.fingerprint_bans_id_seq'::regclass); +-- +-- Name: footer_categories id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.footer_categories ALTER COLUMN id SET DEFAULT nextval('public.footer_categories_id_seq'::regclass); + + +-- +-- Name: footer_links id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.footer_links ALTER COLUMN id SET DEFAULT nextval('public.footer_links_id_seq'::regclass); + + -- -- Name: forums id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2410,6 +2895,13 @@ ALTER TABLE ONLY public.polls ALTER COLUMN id SET DEFAULT nextval('public.polls_ ALTER TABLE ONLY public.posts ALTER COLUMN id SET DEFAULT nextval('public.posts_id_seq'::regclass); +-- +-- Name: quick_tag_tabs id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.quick_tag_tabs ALTER COLUMN id SET DEFAULT nextval('public.quick_tag_tabs_id_seq'::regclass); + + -- -- Name: reports id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2424,6 +2916,34 @@ ALTER TABLE ONLY public.reports ALTER COLUMN id SET DEFAULT nextval('public.repo ALTER TABLE ONLY public.roles ALTER COLUMN id SET DEFAULT nextval('public.roles_id_seq'::regclass); +-- +-- Name: season_quick_tags id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.season_quick_tags ALTER COLUMN id SET DEFAULT nextval('public.season_quick_tags_id_seq'::regclass); + + +-- +-- Name: shipping_quick_tags id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.shipping_quick_tags ALTER COLUMN id SET DEFAULT nextval('public.shipping_quick_tags_id_seq'::regclass); + + +-- +-- Name: shorthand_quick_tag_categories id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.shorthand_quick_tag_categories ALTER COLUMN id SET DEFAULT nextval('public.shorthand_quick_tag_categories_id_seq'::regclass); + + +-- +-- Name: shorthand_quick_tags id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.shorthand_quick_tags ALTER COLUMN id SET DEFAULT nextval('public.shorthand_quick_tags_id_seq'::regclass); + + -- -- Name: site_notices id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2459,6 +2979,13 @@ ALTER TABLE ONLY public.static_pages ALTER COLUMN id SET DEFAULT nextval('public ALTER TABLE ONLY public.subnet_bans ALTER COLUMN id SET DEFAULT nextval('public.subnet_bans_id_seq'::regclass); +-- +-- Name: system_images id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.system_images ALTER COLUMN id SET DEFAULT nextval('public.system_images_id_seq'::regclass); + + -- -- Name: tag_changes id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2573,6 +3100,38 @@ ALTER TABLE ONLY public.artist_links ADD CONSTRAINT artist_links_pkey PRIMARY KEY (id); +-- +-- Name: avatar_kinds avatar_kinds_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_kinds + ADD CONSTRAINT avatar_kinds_pkey PRIMARY KEY (id); + + +-- +-- Name: avatar_parts avatar_parts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_parts + ADD CONSTRAINT avatar_parts_pkey PRIMARY KEY (id); + + +-- +-- Name: avatar_shape_kinds avatar_shape_kinds_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_shape_kinds + ADD CONSTRAINT avatar_shape_kinds_pkey PRIMARY KEY (id); + + +-- +-- Name: avatar_shapes avatar_shapes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_shapes + ADD CONSTRAINT avatar_shapes_pkey PRIMARY KEY (id); + + -- -- Name: badge_awards badge_awards_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2621,6 +3180,14 @@ ALTER TABLE ONLY public.commissions ADD CONSTRAINT commissions_pkey PRIMARY KEY (id); +-- +-- Name: configs configs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.configs + ADD CONSTRAINT configs_pkey PRIMARY KEY (id); + + -- -- Name: conversations conversations_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2629,6 +3196,14 @@ ALTER TABLE ONLY public.conversations ADD CONSTRAINT conversations_pkey PRIMARY KEY (id); +-- +-- Name: default_quick_tags default_quick_tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.default_quick_tags + ADD CONSTRAINT default_quick_tags_pkey PRIMARY KEY (id); + + -- -- Name: dnp_entries dnp_entries_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2669,6 +3244,22 @@ ALTER TABLE ONLY public.fingerprint_bans ADD CONSTRAINT fingerprint_bans_pkey PRIMARY KEY (id); +-- +-- Name: footer_categories footer_categories_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.footer_categories + ADD CONSTRAINT footer_categories_pkey PRIMARY KEY (id); + + +-- +-- Name: footer_links footer_links_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.footer_links + ADD CONSTRAINT footer_links_pkey PRIMARY KEY (id); + + -- -- Name: forums forums_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2789,6 +3380,14 @@ ALTER TABLE ONLY public.posts ADD CONSTRAINT posts_pkey PRIMARY KEY (id); +-- +-- Name: quick_tag_tabs quick_tag_tabs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.quick_tag_tabs + ADD CONSTRAINT quick_tag_tabs_pkey PRIMARY KEY (id); + + -- -- Name: reports reports_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2813,6 +3412,38 @@ ALTER TABLE ONLY public.schema_migrations ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); +-- +-- Name: season_quick_tags season_quick_tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.season_quick_tags + ADD CONSTRAINT season_quick_tags_pkey PRIMARY KEY (id); + + +-- +-- Name: shipping_quick_tags shipping_quick_tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.shipping_quick_tags + ADD CONSTRAINT shipping_quick_tags_pkey PRIMARY KEY (id); + + +-- +-- Name: shorthand_quick_tag_categories shorthand_quick_tag_categories_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.shorthand_quick_tag_categories + ADD CONSTRAINT shorthand_quick_tag_categories_pkey PRIMARY KEY (id); + + +-- +-- Name: shorthand_quick_tags shorthand_quick_tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.shorthand_quick_tags + ADD CONSTRAINT shorthand_quick_tags_pkey PRIMARY KEY (id); + + -- -- Name: site_notices site_notices_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2853,6 +3484,14 @@ ALTER TABLE ONLY public.subnet_bans ADD CONSTRAINT subnet_bans_pkey PRIMARY KEY (id); +-- +-- Name: system_images system_images_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.system_images + ADD CONSTRAINT system_images_pkey PRIMARY KEY (id); + + -- -- Name: tag_changes_legacy tag_changes_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2993,6 +3632,20 @@ CREATE INDEX channel_live_notifications_user_id_read_index ON public.channel_liv CREATE INDEX channel_live_notifications_user_id_updated_at_desc_index ON public.channel_live_notifications USING btree (user_id, updated_at DESC); +-- +-- Name: configs_key_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX configs_key_index ON public.configs USING btree (key); + + +-- +-- Name: footer_categories_position_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX footer_categories_position_index ON public.footer_categories USING btree ("position"); + + -- -- Name: forum_post_notifications_post_id_index; Type: INDEX; Schema: public; Owner: - -- @@ -4386,6 +5039,13 @@ CREATE INDEX moderation_logs_user_id_index ON public.moderation_logs USING btree CREATE INDEX reports_system_index ON public.reports USING btree (system) WHERE (system = true); +-- +-- Name: system_images_key_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX system_images_key_index ON public.system_images USING btree (key); + + -- -- Name: tag_change_tags_tag_change_id_tag_id_index; Type: INDEX; Schema: public; Owner: - -- @@ -4442,6 +5102,30 @@ CREATE UNIQUE INDEX user_tokens_context_token_index ON public.user_tokens USING CREATE INDEX user_tokens_user_id_index ON public.user_tokens USING btree (user_id); +-- +-- Name: avatar_shape_kinds avatar_shape_kinds_avatar_kind_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_shape_kinds + ADD CONSTRAINT avatar_shape_kinds_avatar_kind_id_fkey FOREIGN KEY (avatar_kind_id) REFERENCES public.avatar_kinds(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: avatar_shape_kinds avatar_shape_kinds_avatar_shape_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_shape_kinds + ADD CONSTRAINT avatar_shape_kinds_avatar_shape_id_fkey FOREIGN KEY (avatar_shape_id) REFERENCES public.avatar_shapes(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: avatar_shapes avatar_shapes_avatar_part_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.avatar_shapes + ADD CONSTRAINT avatar_shapes_avatar_part_id_fkey FOREIGN KEY (avatar_part_id) REFERENCES public.avatar_parts(id) ON UPDATE CASCADE ON DELETE CASCADE; + + -- -- Name: channel_live_notifications channel_live_notifications_channel_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4458,6 +5142,14 @@ ALTER TABLE ONLY public.channel_live_notifications ADD CONSTRAINT channel_live_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; +-- +-- Name: default_quick_tags default_quick_tags_quick_tag_tab_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.default_quick_tags + ADD CONSTRAINT default_quick_tags_quick_tag_tab_id_fkey FOREIGN KEY (quick_tag_tab_id) REFERENCES public.quick_tag_tabs(id) ON UPDATE CASCADE ON DELETE CASCADE; + + -- -- Name: channels fk_rails_021c624081; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -5242,6 +5934,14 @@ ALTER TABLE ONLY public.gallery_subscriptions ADD CONSTRAINT fk_rails_fa77f3cebe FOREIGN KEY (gallery_id) REFERENCES public.galleries(id) ON UPDATE CASCADE ON DELETE CASCADE; +-- +-- Name: footer_links footer_links_footer_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.footer_links + ADD CONSTRAINT footer_links_footer_category_id_fkey FOREIGN KEY (footer_category_id) REFERENCES public.footer_categories(id) ON UPDATE CASCADE ON DELETE CASCADE; + + -- -- Name: forum_post_notifications forum_post_notifications_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -5378,6 +6078,38 @@ ALTER TABLE ONLY public.moderation_logs ADD CONSTRAINT moderation_logs_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; +-- +-- Name: season_quick_tags season_quick_tags_quick_tag_tab_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.season_quick_tags + ADD CONSTRAINT season_quick_tags_quick_tag_tab_id_fkey FOREIGN KEY (quick_tag_tab_id) REFERENCES public.quick_tag_tabs(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: shipping_quick_tags shipping_quick_tags_quick_tag_tab_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.shipping_quick_tags + ADD CONSTRAINT shipping_quick_tags_quick_tag_tab_id_fkey FOREIGN KEY (quick_tag_tab_id) REFERENCES public.quick_tag_tabs(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: shorthand_quick_tag_categories shorthand_quick_tag_categories_quick_tag_tab_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.shorthand_quick_tag_categories + ADD CONSTRAINT shorthand_quick_tag_categories_quick_tag_tab_id_fkey FOREIGN KEY (quick_tag_tab_id) REFERENCES public.quick_tag_tabs(id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: shorthand_quick_tags shorthand_quick_tags_shorthand_quick_tag_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.shorthand_quick_tags + ADD CONSTRAINT shorthand_quick_tags_shorthand_quick_tag_category_id_fkey FOREIGN KEY (shorthand_quick_tag_category_id) REFERENCES public.shorthand_quick_tag_categories(id) ON UPDATE CASCADE ON DELETE CASCADE; + + -- -- Name: source_changes source_changes_image_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -5446,7 +6178,7 @@ ALTER TABLE ONLY public.users -- PostgreSQL database dump complete -- -\unrestrict HaHFW4uoQTYEW2yFV8zxsi1boGlZaAq7wPC4PqUZeQq9dkvLFwHwwFxqqPeF0LZ +\unrestrict 9uxDdny3Bpcuj1Rkc9wlUPdpby5cuWuksIxgFIdKdkceoDa1fmrKWPlCr9UaBJ8 INSERT INTO public."schema_migrations" (version) VALUES (20200503002523); INSERT INTO public."schema_migrations" (version) VALUES (20200607000511); @@ -5476,3 +6208,8 @@ INSERT INTO public."schema_migrations" (version) VALUES (20250407021536); INSERT INTO public."schema_migrations" (version) VALUES (20250501174007); INSERT INTO public."schema_migrations" (version) VALUES (20250502110018); INSERT INTO public."schema_migrations" (version) VALUES (20250507183410); +INSERT INTO public."schema_migrations" (version) VALUES (20251014181016); +INSERT INTO public."schema_migrations" (version) VALUES (20251014181033); +INSERT INTO public."schema_migrations" (version) VALUES (20251014181203); +INSERT INTO public."schema_migrations" (version) VALUES (20251014191009); +INSERT INTO public."schema_migrations" (version) VALUES (20251018172400); diff --git a/test/philomena_web/controllers/reactivation_controller_test.exs b/test/philomena_web/controllers/reactivation_controller_test.exs index e7b2a63a1..b8effb5e2 100644 --- a/test/philomena_web/controllers/reactivation_controller_test.exs +++ b/test/philomena_web/controllers/reactivation_controller_test.exs @@ -12,7 +12,7 @@ defmodule PhilomenaWeb.ReactivationControllerTest do describe "GET /reactivations/:id" do test "renders the reactivate account page", %{conn: conn} do - conn = get(conn, ~p"/reactivations/pinkie-pie-is-best-pony") + conn = get(conn, ~p"/reactivations/new") response = html_response(conn, 200) assert response =~ "

Reactivate Your Account

" end