code:
node_modules/
backup.sh

#secrets
configs/*.json
configs/*.js

# ignore jschan tmp and static
tmp/
static/*
docker/static/*

# ignore some gulp resources
/gulp/res/css/codethemes
gulp/res/css/locals.css
gulp/res/css/custom.css
gulp/res/icons/*

# no compiledclients js files generated from .pug in gulpfile
gulp/res/js/locals.js
gulp/res/js/post.js
gulp/res/js/modal.js
gulp/res/js/captchaformsection.js
gulp/res/js/pugfilters.js
gulp/res/js/threadwatcher.js
gulp/res/js/watchedthread.js
gulp/res/js/pugruntime.js
gulp/res/js/uploaditem.js
gulp/res/js/socket.io.js
gulp/res/js/web3.js

# some ide and testing files, artefacts, etc
.idea/
coverage/
junit.xml

path: .gitignore


code:
image: node:latest

# Cache modules in between jobs
cache:
  key: $CI_COMMIT_REF_SLUG
  paths:
    - .npm/

before_script:
  - npm ci --cache .npm --prefer-offline

# Code linting
eslint:
  stage: .pre
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
  script:
    - 'npm i -g eslint-cli'
    - 'npm i eslint --save-dev'
    - 'node node_modules/eslint/bin/eslint.js ./'
  allow_failure: true

# Run tests
unit-tests:
  stage: test
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
  before_script:
    - apt-get update -y && apt-get install -y fonts-urw-base35 gsfonts fonts-dejavu
  script:
    - 'npm install'
    - 'npm install jest -g'
    - 'cp configs/secrets.js.example configs/secrets.js'
    - 'jest --ci --verbose --reporters=default --reporters=jest-junit --testPathIgnorePatterns=./test/ --collectCoverage --coverageReporters text --coverageReporters cobertura'
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
  artifacts:
    when: always
    reports:
      junit:
        - junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
  #TODO: stage: integration-tests

repo_name: Marzisoft/jschan

license: mit


code:
- [ ] I have searched the issues to make sure I am not opening a duplicate
- [ ] If this is a jschan installation/setup issue, I have followed the INSTALLATION.md
- [ ] I am asking a [SMART QUESTION](http://www.catb.org/~esr/faqs/smart-questions.html)
- [ ] Leave this box unchecked to prove you actually bothered to read

## Issue report

### Description
A clear and concise description of what the problem is.

### Steps to reproduce (if applicable)

1. Click on '...'
3. Scroll down to '....'
4. See error

### Expected result
What did you expect to happen?

### Actual result
What actually happened?

### Possible solution
Optional, if you have a suggestion.

### Additional information
For example, screenshots and log excerpts.

### Environment
Device:
Operating System and version:
Browser and version:


code:
### 1.7.3
  - Add "Hover expands media" option in frontend and global settings frontend script defaults.
    - Has a sub option to follow the cursor.
  - Fix an issue with Tegaki for browsers with anti-fingerprinting for canvas in e.g. firefox & forks by attempting to detect this.
    - If canvas is spoofed, the Tegaki applet won't open and a modal is displayed letting the user know.
    - Fixes an issue where even choosing "allow", the tegaki drawing have spoofing artifacts and not save properly, wasting users time.
  - Tegaki is now in a dragable, resizable, minimisable window instead of taking over the whole page.
    - To achieve this, some refactors of the Dragable class, and the addition of a Minimisable class were made, making adding and maintaining "windows" easier.
  - "/custompage/" URLs are now "/page/". To prevent immediately breaking links, custompage name will continue to work (redirect in nginx) but is deprecated and will be removed in a later version.
  - Updated Tegaki.js.
  - Regular file sha256 hashes are now added to filter strings so they can be blocked just like file names or fuzzy hashes.
  - Updated dependencies.
  - Updated unofficial tool repo links.
  - TODO

Some items are included in this tag/release but were already backported to 1.7.0 including:
  - Migration to update board staff with the proper bit for the USE_MARKDOWN_IMAGE permission (image embeds like github-style markdown).
  - Migration to fix a permissions issue from earlier 1.7.x installations, and fix the display and logic of _DEFAULT role permissions.
  - INSTALLATION readme instruction improvements/fixes.
  - Nginx config generation updated to handle an incompatibility with older versions that created a duplicate config.
Apologies for the delays on this release, I've been very focused on other projects but will be working on jschan more now and am committed 110%.

### 1.7.0
  - Important permission logic changes. Technical explanation:
    - If an account is a board owner or board staff, they will only get the permissions that are under the `_MANAGE_BOARD_BITS` if the "Board Owner" or "Board Staff" roles respectively have the permission ticked, AND its ticked in the board staff permissions.
    - Roles for "Board owner" and "Board staff" are now separate from "Board staff defaults" and "Board owner defaults". The defaults are the bits set by default on board owners/board staff, and the regular roles define the maximum permissions that board owners or staff can have. This allows more global control over what permissions BOs/staff can have and what BOs can delegate to their staff.
    - The "applyInheritance" function has been modified to not set all _MANAGE_BOARD_BITS for a BO unconditionally anymore, see the previous point.
    - This means admins can e.g. uncheck permissions on the "Board Owner"/"Board Staff" role globally to control what BOs and staff can access, or to prevent BOs delegating certain permissions to their staff, but still give it back on a per-account basis.
    - Accounts who are e.g. "Global Staff" who have those permissions on their role (happens before the board calculation), or have it set at an account level will still have those perms, so removing them from the role or being a board staff and a "Global Staff" or having "Board Owner" role at an account level doesn't break their permissions.
  - On global accounts management, add the ability to see who has 2FA enabled.
  - Add image embed support in posts (requires a permission), added to faq page "post styling" section.
  - When getting redirected for lack of authentication specifically to "/manage/thread/" paths, just redirect to the public version, rather than to /login.
    - Makes it easier for staff to share links to threads without having to remove "/manage" from the URL.
    - Logged out staff can just press "manage" in the top corner on the public page to get sent to /login with a ?goto query param for the thread anyway.
  - Make the appearance of the video play button more consistent across OS/devices and not display as an emoji on IOS.
  - Update the digi theme.
  - Improve global settings form validation to prevent enabling webring without some othre required fields being set.
  - Update docs and jschan-api-go with the "global clear" endpoint.
  - Update INSTALLATION.md and various other components to make install work reliably again.

### 1.6.2
  - Bugfix for "replace" filters not replacing all occurences of matches in some circumstances
  - Update dependencies & npm audit fix

### 1.6.1
  - Fixed a regression causing some video/audio file thumbnails to be blank or to be incorrectly rejected as corrupt.
  - Fixed a bug breaking the global bans page.
  - Translation updates thanks to @l29utp0 from ptchan.org.
    - Added Spanish locale.
    - Add new/missing translations to other locale files.
    - Fix some untranslated strings in the frontend templates.

### 1.6.0
  - Filters now have a "replace" mode, by @disco.
  - Global bans now show the board it originated from as "Global (<board>)".
  - Added a permission that allows board staff to see global bans in the board ban list if the ban was issued from a post on that board.
  - Modlogs now have private entries for staff auditing
    - So far covers creating boards, changing settings (board or global), and ban editing/unban/upgrade.
    - Modlog entries for e.g. board creation are global and not linked to the board and will not be deleted along with the board (duh).
    - Private modlogs for edited global bans will also be available from the originating board.
  - Global account management now has an option delete all boards owned by an account when deleting it.
  - Bugfix moving posts to non existing board not correctly returning an error sometimes.
  - Add "playsinline" attribute to expanded videos to prevent IOS defaulting them to fullscreen, thanks @l29utp0.
  - Styling tweaks to prevent showing a white background in the overscroll space on IOS, thanks to l29utp0.
  - Fixed a bug where in certain circumstances a thread would be sent to the last page when a reply is deleted.

### 1.5.0
  - Thanks very much to @disco and @some_random_guy for valuable contributions for this release while I have been busy and on vacation.
  - Remove spamhaus ZEN from the default included DNSBLs.
  - Improved handling of video thumbnailing with transparent vp8 and vp9 codecs.
    - Video codec now included in processedFile data.
  - Fix frontend bug of hovering quotes on some global/manage pages.
  - Added a tool to restore deleted posts from a backup.
  - Fix issue with scheduled file pruning.
  - Fix minor issue of text wrapping on board tables.
  - Update vendorised dependencies (gm, express-fileupload, etc).
  - Add a few more eslint rules to more strictly enforce code style.
  - No longer exlude the tools folder from linting.
  - Npm audit fix.

### 1.4.1
  - Update the INSTALLATION.md (install instructions).
  - Add a "Bypass DNSBL" account/role permission.
  - Add a "Bypass Anonymizer Restrictions" account/role permission.
  - Update dependencies.
  - Npm audit fix.

### 1.4.0
  - Add a new global management feature to clear all blockbypasses or all login sessions.
  - Make (You)s sync over multiple tabs when posting or clearing.
  - Update dependencies.
  - Npm audit fix.

### 1.3.3
  - Fix the "Board Defaults" section in globamanagement settings being inaccessible, and update the template to be slightly more maintainable (especially for forks).
  - Make the message box focused by js when a page is opened with #postform.
  - Fix an installation issue with a folder not getting created properly.

### 1.3.2
  - Global management board list links now link to boards in manage mode.
  - Bugfix live editing posts, when editing last post of a thread it no longer
    moves to the top of a thread.
  - pt-PT and pt-BR language improvements.
  - Styling tweaks to web3 signatures.
  - Update jschan-docs.
  - Npm audit.

### 1.3.1
  - Pt language fixes and updates.
  - Fix a bug in live posts not displaying web3 address/signatures.

### 1.3.0
  - Web3 integration with MetaMask, development generously funded by @0xBekket
    - Users with the MetaMask browser extension can now register/login to accounts, and sign their messages with MetaMask.
    - Signed post messages will display a signature and link to their wallet at the bottom of the post.
    - The link uses etherscan by default but can be changed to another url with %s substituting the address, like other URL settings.
    - A board-level and global option is available to disable web3 features.
    - `signature` and `address` are available as new properties on signed posts through the API.
    - No third party server communication or details are required for signing messages or authing.
    - This is a very exciting feature with a lot of possibilities for expanding in future. Stay tuned.
  - Client side filters can now be applied against flags or custom flags.
  - Fix order of Tegaki upload so files come before replays so catalog shows the thumbnail.
  - Video embeds now support youtube shorts and odysee.com.
  - New global option for URI decoding filenames, to display some characters such as quotation mark (") better in filenames.
  - Minor pt-PT language fixes.
  - Small color adjustments to Yotsuba theme to match 4chan colors more closely.
  - Update nginx config for new embeds in CSP, and some more file type support.
  - Npm audit.

### 1.2.2
  - Add a global setting to try and URI decode filenames
  - Minor pt-PT and pt-BR language fixes
  - Remove some unused dependencies
  - Npm audit

### 1.2.1
  - Bugfix ban upgrades not applying correctly due to bans format change.

### 1.2.0
  - Massive filter system overhaul, huge thanks to @disco.
    - Filters are no longer just a text box with some options that apply to all filters.
    - Filters can now be created/edited/deleted on separate global and board pages.
    - All previous filter options e.g. strict matching, ban/block mode, ban message, ban duration, and appealability are customisable per-filter.
    - The PR for this, #532 on gitgud is an excellent example of how to add features to jschan and fits into the design patterns perfectly.
    - This gives the flexibility to add a filter "replace" mode in a future release.
  - Added Italian locale (it-IT) courtesy of @zocram from niuchan.org
  - Improved Portuguese translations thanks to @l29utp0 from ptchan.org
  - Improved Russian translations thanks to Kusankot
  - Minor addition to INSTALLATION.md for those using Ubuntu rather than Debian.
  - Npm audit & dependencies updated.

### 1.1.1
  - Bugfix live post updates triggering the disconnect and removal of postforms on non-OP delete/move.
  - Improve text wrapping on long table data.
  - Npm audit.

### 1.1.0
  - Posts edits and "USER WAS BANNED FOR THIS POST" messages will now be updated live through the websocket.
  - Added Yandex Smart Captcha support, as an additional captcha type option.
    - NOTE: This is still a WIP because Yandex does not support registration from my country and I am unable to get an API key. If you are able to share an API key for development purposes, please reach out via contact detaiils in README.md
  - Fix pluralisation of thread stickying message.
  - Fix some language mixups in translations.

### 1.0.6
  - Bugfix in translation of blocked country message when posting.
  - Some Improvements to the Russian translation courtesy of an anon from bbs.quietplace.xyz. Handling of the complex plural rules in Russian will be improved in a future version.
  - Npm audit.

### 1.0.5
  - Add pt-BR language pack, by @thugking from Rchan.
  - Fix bypass not auto closing and submitting on non english locale.
  - Actually fix Tor hidden service country name not displaying correctly.
  - Make the error handling for "possible corrupt or unsupported file" catch more bad files and processing errors.
  - Make animated webp thumbnailing at least work and not give the above error (not supporting animated thumbs like .gif yet).
  - Add "ttf" extension to nginx snippets because some themes include fonts.
  - Reorganize <input> styling, use accent-color property on <input> elements, by @disco.
  - Add globalmanage setting for board default language.
  - Fix an issue with the referer because of translation, and prevent invalid settings for it from breaking the web panel.
  - Fix broken migration for board defaults causing language issues on newly created boards after updating from pre 1.0.0.
  - Npm audit.

### 1.0.4
  - Translation improvements.
  - Fix Tor hidden service country name not displaying correctly.
  - Make global disabling of anonymizer file posting apply to board banners, flags, custompages.

### 1.0.3
  - Translation improvements.
  - Add back missing "Lock+Unlist" mode for handling ianctive boards.
  - Npm audit.

### 1.0.2
  - Translation improvements.

### 1.0.1
  - Translation improvements.
  - Add a link to board in mod view e.g. /test/ on globalmanage recent posts to give some context.
  - Fix dockerignore for docker testing/development, was broken due to translations.

### 1.0.0
Version 1.0.0 is here. jschan is stable and mature enough and has been for a while. There's no need to go to version 0.9999.9999 and I am comfortable enough to call it "Version 1".
Of course, that doesn't mean jschan is perfect or finished by any means. I want to keep improving jschan and there are plenty of new features and changes planned for versions 2, 3, and beyond.
Thank you so much to all the jschan site admins, users & contributors for your help along the way. Your contributions have been invaluable to making this software great.
Special shoutout to l29utp0 & loynet (ptchan.org), Homicide (94chan.org) and some_random_guy (trashchan.xyz).

-Tom

Now, back to the program. Here are the changes for 1.0.0:
  - **Multiple language support.**

jschan now supports language packs. There is a global and board-level language setting which completely translates the interface to another language. Using server side rendering and logic, so it remains noscript compatible.
An effort has been made to translate everything, but given there is almost 4 years of code, some things may have slipped through the cracks. If something isn't translated, please report it on gitgud.

1.0.0 includes three* complete language packs:
  - English (en-GB)
  - Portuguese (pt-PT)
  - Russian (ru-RU) *Machine translated, may not be accurate.

Huge credit to the ptchan.org admins for providing the Portuguese translation.
Contributions for new language packs or improvements to existing ones are very welcome! **Reach out via email, IRC, or session (See README.md) to discuss imbursement for contributing language packs.**
Note: "global" pages e.g. overboards will adhere to global language setting. In a future iteration, these will be updated to (optionally, with global setting) support adhering to users browser language header for better UX going between a non-global language board and global pages.

  - Notify the user when making a playlist from a thread if there were no files, rather than just silently logging.
  - Improves the css and markup to only show the appropriate wording e.g "tap" or "click" in tooltips depending if you are on mobile/desktop.
  - Improve the installation process to fix a potential issue with the database connection settings.
  - Customflags will now show correctly when editing a post on a board with custom flags enabled.
  - Security improvement to the 2FA validation flow during login.
  - Log a few more errors related to hcaptcha/recaptcha, for debugging purposes. (already caught and returned in a friendly manner)
  - Remove showing language and relevance data when auto detecting highlighted code block language.
  - Add more themes (cybhub, digi, favela, ptchan, teletext).
  - More minor bugfixes to permissions pages displays.

### 0.11.4
  - Bugfix for the message stating how many banners were deleted when deleting banners.
  - Add an option to limit the total resolution of an image/video (width*height).

### 0.11.3
  - Fix max vs total upload count in controller for uploading board assets, flags and banners.
  - Move css theme assets to themes/assets instead of all lumped in one folder.
  - Adjust the letter spacing and line spacing of the 2FA QR code.

### 0.11.2
  - Convert the assets page form handling to the newer checkSchema code.
  - Don't show the "Edit" option in the post dropdowns for public pages.
  - No longer apply permissions inheritance after editing to prevent confusion.
  - Improve duplicate checking when editing roles to only explicitly match updated roles rather than applying inheritance first.
  - Bugfix "my permission" page not displaying correctly and board staff permission editing not applying.
  - Improve required parent permission display to show "requires X" in the tooltip of disabled checkboxes.

### 0.11.1
  - Bugfix non logged in users not able to use actions because of an issue in move post logic (thanks @some_random_guy).
  - Bugfix editing posts from the actions form (not the new dropdown "Edit" button) always redirecting to OP.

### 0.11.0
  - Improve the fallback behaviour of live posts to work on connect_error instead of only reconnect_failed, and also attempt to upgrade again to the socket.
  - The post form in modview now shows the full email field even with "sage only email", same as how the name field shows in modview with "force anon"
  - Globals or users who are staff on multiple boards can move posts between boards
  - IP/ban type display and suffixes improved to make IPv4/6/Bypass/Pruned IP clearer for more informed moderation.
  - Post editing is now available as a link in the post dropdown, and makes a GET for the page (like editcustompage/editnews)
  - Labelled link format markdown now supports mailto.
  - "Hide name" set by default when using public actions form.
  - Bugfix saveoverboard script so it works on catalog instead of only index, and improve the code to be more general and allow /catalog.html in navbar link.
  - Bugfix BYPASS_CAPTCHA permission not able to be applied to roles.
  - Add a small note on global account/role permission editing about the implication of setting "board management" permissions globally.
  - Update README with some new related projects and change demo/test instance to 94chan.
  - Globalmanage roles are now available in json format in the API.
  - Improve some of the templates and refactor some repetitive bits into includes/mixins.
  - Add and update some tests.
  - Npm audit & dependencies updated.
  - Buon Natale e buon anno 🎉

### 0.10.2
  - Bugfix to 2fa, code-reuse prevention was blocking all codes rather than only a recently used correct code.

### 0.10.1
  - Minor nginx configuration change for twofactor.html.

### 0.10.0
  - Add account two factor authenticaiton (TOTP).
  - Update dependencies.

### 0.9.4
  - Ability to add internal notes to bans. These are only seen by staff and not shown to the banned user.
  - Wordfilter auto bans will set an internal note with which wordfilter was triggered.
  - Fix zooming in on tabbed settings pages breaking when zoomed in on some browsers.
  - Npm audit fix.

### 0.9.3
  - Vastly improve installation instructions & automated nginx configuration.

### 0.9.2
  - Bugfix an issue with bypasses/captcha related to difference between renewing vs getting new bypass.

### 0.9.1
  - Allow customisable font for grid captcha.
  - Default grid captcha to a common system font instead of bundling an enormous 24MB font file.
  - Bugfix some tegaki issues.

### 0.9.0
  - Add board option to hide banners from header and link in board nav bar.
  - Add global option to remove reverse image search links from the overboard.
  - Tegaki now supports replay upload and playback.
  - Completing /bypass.html form will now renew your existing bypass if you had one.
  - Replaced "no subject" with #postId in several places.
  - New public API endpoints
    - Public board logs.
    - Board banners (previously undocumented).
    - Board custompages.
    - Select board settings and global settings (ones that are useful/necessary for 3rd party app integration).
  - Bugfix log pruning not removing old logs from mod view logs page (public log pages were unaffected and hid the old logs).
  - Bugfix deleting board assets/banners returning the wrong number in the success message.

### 0.8.2
  - Migrate to new socket.io redis adapter package.
  - Fix issue with pre checking boxes in ban modal conflicting with grid captcha.

### 0.8.1
  - Fix incorrectly displaying inactive accounts action settings in global settings.
  - Fix captcha gridv2 not appearing in board captcha slot with js enabled.

### 0.8.0
  - Settings pages are now more organised and use tabs, and still works without javascript.
  - Captcha improvements
    - New captcha image effects, and customisation for their value/strength.
    - Customisable font for text captcha.
    - Improved grid captcha, the character sets and question are now customisable.
    - Added new captcha type "grid v2" with a different challenge involving pointing arrows. Also customisable like grid v1.
  - Added a new account level permission to bypass captchas including blockbypass.
  - Anonymizers dummy bypass cookies given while blockbypass is disabled are no longer valid once blockbypass is enabled.
  - Add a new option for automatically forfeiting board staff position and/or deleting inactive accounts with customisable time.
  - Add a new option to automatically lock, lock+unlist, or delete boards that have no board owner.
  - Homepage hot threads have a new option for maximum thread age, and are no longer bound to a static 7 days.
  - Homepage hot threads score formula is now `score=replies in last 7 days*(1-(current thread age/max thread age))` to better represent more recently active threads, rather than simply sorting by reply count.
  - Add unofficial Typescript SDK + typings for jschan api to README, (thanks to Michell/ussaohelcim).
  - Add "playlist" button in OP dropdown to download thread audio/video as m3u playlist (thanks to Michell/ussaohelcim).
  - Switch youtube embeds to use youtube-nocookie domain.

### 0.7.3
  - Bugfix some captcha generations causing server error due non-integer argument to randomRange.

### 0.7.2
  - Add playlist generating bookmarklet script to README
  - Bugfix some captcha generations causing server error due to bad random number range.
  - Fix UTF8-filenames not being decoded corectly (busboy made this not the default in a recent update).

### 0.7.1
  - Show sticky level on hover (title property) of pin icon.
  - Reduce frontend script size by ~10KB. More improvement to come in future updates.
  - Change homepage hot threads to show creation date rather than last bumped.
  - Don't allow setting global limit for post password length below what the frontend automatically generates.
  - When strict mime validation is enabled, actually tell the user what the server thinks the mime is in the mismatch error message.
  - Make IP pruning schedule also apply to modlogs.
  - Fix incorrectly hiding "create board"/"register an account" on account page because of permission issue.
  - Fix the nginx configuration script putting commas in CSP if you say yes to recaptcha/hcaptcha.
  - Fix recaptcha/hcaptcha causing an issue for scrolling to new posts in some (chrom*) browsers.
  - Fix the generate-favicon gulp task not versioning correctly causing cache issues.
  - Small frontend and backend refactors.
  - Add screenshot image animated with light/dark theme in README.
  - Publish some dependencies to npmjs for faster installation.
  - Update dependencies.

### 0.7.0
  - ESLint has been added as a dev dependency. This does "linting" to enforce code style and find and fix some mistakes.
  - Updated CONTRIBUTING.md information.
  - Bugfixes.

### 0.6.6
  - Post editing bugfix.

### 0.6.5
  - Portrait mode support for tegaki, improves the experience particularly on mobile devices.
  - Small refactor of filtering.
  - Modlog pruning now only deletes log entries older than the specified date when new modlogs are added, as originally intended.
  - Add a few extra things to server logs when debugLogs is true.
  - Minor bugfixes.

### 0.6.4
  - Add "adaptive" theme that switches between "clear" and "tomorrow" based on the device system theme, using prefers-color-scheme media query.
  - Minor bugfixes.

### 0.6.3
  - Bugfix when setting empty board tags, and other multiline settings.

### 0.6.2
  - Hotfix webring.

### 0.6.1
  - +/- button to expand a thread and see the omitted posts from index pages.
  - Option for global filter bans to be not appealable.

### 0.6.0
  - Bans+Appeal form will now appear in the modal popup when js enabled, instead of a dodgy workaround which often caused posting bugs and broke over Tor.
  - Bans can now be "upgraded" retroactively to expand single IP bans to qrange/hrange bans.
  - Ban table now shows columns for "Type" (IP/Bypass/Pruned) and "Range" (Single/Narrow/Wide).
  - Global setting to show some popular threads from listed boards on the homepage, with adjustible limit (0=disabled) and threshold. (courtesy ptchan.org)
  - Big refactor of backend, the awfully named and disorganised "helpers" is now the more appropriately named and better organised "lib".
  - Some form and input handling code made more robust based on test feedback.
  - More tests.
  - Bugfixes.

### 0.5.3
  - Add testing framework. Allows to test individual code, simulated requests and check for regressions after changes. Will be made more comprehensive over time.
  - Make "Manage" navbar link go to the current page in modview, instead of defaulting to reports page.
  - Fix some obscure bugs with incorrect post ordering/pages not rebuilding after making a post, or submitting post actions.
  - Many bugfixes.

### 0.5.2
  - Fix regression in remarkup of posts during moving, deleting, cyclic threads, etc due to permissions.
  - Small internal change to how dicerolls are processed.

### 0.5.1
  - Small change to expanding spoilered files.
  - Bugfix outdated bans template mixin.

### 0.5.0
  - Add tegaki.js (oekaki). Has board toggle and settings option for default canvas size.

### 0.4.1
  - Fix not properly skipping dnsbl with block bypass when configured to do so, give the better frame popup, and skip if already solved board captcha, etc.
  - Fix sessions list page showing expiry dates far in the past
  - Minor bugfix to flag saving.

### 0.4.0
  - Hashed IPs now use an irc-style "cloaking".
    - Ranges are now combined in the IP instead of being only available in the JSON and not shown in management pages.
    - IPs will end with .IP, bypasses will end in .BP and pruned ips will end in .PRUNED
  - Users can now list/delete their active login sessions.
  - New permission system. No more "levels" shit.
    - Boards no longer have "moderators" but "staff".
    - New "staff" page in board management, to add/remove/edit staff.
    - The global "accounts" and board "staff" pages allow editing individual permissions.
    - There is now more fine permission/access control system for global and board-level stuff.
    - Users with permission to edit accounts can edit accounts on a global level and grant them permissions.
    - Users with permission to edit board staff can do the same on a per-board level.
  - Better continuity between pages like "news", "accounts", "custompages", "staff" and the associated editieng page. The manage/globalmaange navbar stays in place with the appropriate section highlighted.
  - More linking between moderation interfaces e.g. globals board list will now show direct link to view the owners account.
  - Made some board and page titles more consistent.
  - Custom flag selection is now saved like name and post password.
  - Bugfixes.

### 0.3.4
  - Minor bugfix, saving board settings with empty custom CSS was incorrectly returning an error if there were global filters for css.

### 0.3.3
  - Minor bugfix to filenames of expanded images being incorrectly truncated in some circumstances, when "image loading bars" is enabled.

### 0.3.2
  - Minor bugfix to post moving.

### 0.3.1
  - Small board list optimisation, no longer try to fetch webring site names if webring not enabled.
  - Make webring blacklisting also apply to endpoints after being fetched, in case the URL is different (alt domain, onions, etc).
  - Webring.json and site names cache is now removed when webring is disabled.
  - More reasonable webring timeout for fetching each site.
  - Webring bugfixes.

### 0.3.0
  - Ban durations are now editable. Staff can set a new ban duration (starting from the current date, not the original ban issue date).
  - Minor bugfixes.

### 0.2.0
  - From now on, versioning = major.minor.patch. significant changes = major, feature updates = minor, bugfixes/small stuff = patch.
  - Update instructions about nginx changes when upgrading.
  - Add an endpoint for getting the csrf token separately from html pages. See API docs for more details.
  - Add post "marking" so moved/deleted posts info is sent over websocket. Frontend will handle them. Deleted threads and moved OPs will now also disconnect the socket and remove the post form.
  - Block bypasses are now locked to where they were created (anonymizer or clearnet) to prevent some forms of 'smuggling'. This will be improved further in upcoming releases.
  - Code highlighting now supports all highlight.js languages when explicitly specified. The whitelist now only applies to auto-detection, as originally intended.
  - Quotes for post references in modlog now have the proper quote class, and will show when hovered like any other quote.
  - Bugfixes
  - [jschan-docs](https://fatchan.gitgud.site/jschan-docs/):
    - API docs improvements, now includes csrf token, posting, post actions (and mod variants), and more. It should be enough documentation for somebody to write a mobile app integration.
  - [globalafk](https://gitgud.io/fatchan/globalafk/) (my fork):
    - On android with termux, tapping the notification will open the post (in mod view) and the notification has new shortcut buttons to quickly delete, delete+ban or delete+global ban.

### 0.1.10
  - Add thread watcher/watchlist, so users can save list of threads to a little window and see unread count
  - There are now API docs available at https://fatchan.gitgud.site/jschan-docs/
  - Improved installation nginx script. Confirms overwriting and doesnt break when run more than once
  - Some visual tweaks (file "(u)" being on newline, "x" -> "×")
  - Bugfixes

### 0.1.9
  - Fix "improved" global settings changes not regenerating custom pages properly
  - Postmenu dropdown for filters/moderate added to catalog tiles
  - Notifications are no longer sent for posts when clicking "view full text"
  - Make handling files with no/incorrect extension better
  - Image count from OP is included in catalog tiles file count

### 0.1.8
  - Much improved nginx configuration for installation, script does most of the work
  - Fixed settings form asking to save password -.-
  - Multiple files & post flags now shown in catalog view
  - Faster, more efficient global settings changes
  - Add option for board owner to prevent OP deleting threads that are too old or have too many replies

### 0.1.7
  - Update inconsistent wording of custom overboard subtitle from 0.1.6
  - Dont show "local first" checkbox in boardlist when webring isn't even enabled
  - Bugfix to code markup, remove only leading blank lines, keeping leading whitespace on first non-empty line
  - Make overboard settings save somewhat in localstorage to help stupid zoomers who dont know what a BOOKMARK is

#### 0.1.6
  - Mongodb v5 and node driver v4 update
  - IP rangebans use more reasonable range for ipv6
  - Fix ip randomising schedule
  - Global announcements now show on overboard (both views)
  - Overboard upgrade, now users can set to add and remove any boards, global setting to enable/disable. Form is collapsible
  - Add an extra "new reply" button to the end of threads for convenience
  - Users with scripts will now see proper video thumbnails (depending on browser supported codecs) in the upload list
  - Make fuzzy image hash option and hashes visible to globals, for filtering
  - Add board option to enable reverse image search link next to filenames
  - Add board option to enable an external "archive" link with the other nav links
  - Add global option to configure the URL for archive/reverse image serach links
  - Improved API for some global moderation interfaces, to allow more custom integration to reports and more
  - Staff who hide their name from public logs will also be hidden in the ban when shown to banned user
  - Image previews now shown in notifications
  - Strict filtering improvements
  - Many bugfixes

#### 0.1.5
  - Try to fallback thumbnail generation for video with horribly broken encoding
  - Country blocklist now can actually fit all countries
  - Make "auth level" text box into "account type" dropdown in accounts page, easier to understand
  - Board owners can now edit custom pages
  - Board owners can now manage custom assets
  - Show a little message and disable reply form on full threads (hit reply limit)
  - Allow longer language names for code blocks
  - User settings import and export option

#### 0.1.4
  - Banner uploading bugfix

#### 0.1.3
  - Script optimizations, improves page load speed especially on longer threads
  - Extra (u) download link for no reason
  - favicon, webmanifest, browserconfig, etc for browsers made into gulp task
  - Webring now sends and checks for ppd stat
  - Board search improved (prefix matches)
  - Update code for form submission and data validation, faster and easier to maintain
  - Make it more obvious to users with a blacklisted IP if block bypass dnsbl is enabled
  - Add new themes
  - Improved README
  - Many bugfixes

#### 0.1.2
  - Merge webring and local board list and improve webring search and filter functionality
  - New stat section of homepage
  - Replaced and removed some fatchan-specific media
  - Fixed undefined hcaptcha site key bug
  - Updated README with info about nginx CSP for 3rd party captcha providers
  - Update socket-io 2.x to 4.x

#### 0.1.1
  - Added changelog
  - Version now changes with some kind of meaning
  - Animated gif thumbnails no longer generate static image for images < thumbnail dimensions
  - Boards management "Banners" page is now "Assets"
  - Boards can have custom flags


code:
# Contributing

Thanks for considering contributing.

Some links:
  * [Kanban board](https://gitgud.io/fatchan/jschan/-/boards/4780) already has issues in swim lanes.

## LICENSE?

See [LICENSE](https://gitgud.io/fatchan/jschan/-/blob/master/LICENSE)

## Making changes

  * Bug? Open an issue.
  * Request? Open an issue and ask. Saves wasting your time making changes that will be denied for being bad or unwanted.

When contributing, make a merge request with a clear list of what you've done.

Write clear commit messages. One line is fine for smaller changes, or multiple lines for bigger changes. Be clear and concise.

    $ git commit -m "A brief summary of the commit
    >
    > A paragraph describing what changed and its impact."

## Coding style

  - Tab indentation
    - Switch cases indented 1 level
    - Member expressions indented 1 level
    - Comment indentation ignored
  - Unix linebreaks
  - Single quotes
  - Always include semicolon

ESLint will enforce this (and pick up some other minor code issues). Run ESLint:

```bash
#whole project
eslint ./

#specific directory/file
eslint /path/to/whatever
```

## Running tests

Make sure these still pass after your changes, or adjust them to meet the new expected results.

There is a "jschan-test" service in the `docker-compose.yml` file that will run all the tests in a jschan instance using the docker instance. See the advanced section of installation for some instruction on how to use this.

You can also Run them locally if you have an instance setup (or for quickly running unit tests):

```bash
#unit tests
npm run test
# OR npm run test:unit

#integration tests
TEST_ADMIN_PASSWORD=<password from jschan-reset docker> npm run test:integration

#all tests
npm run test:all

#specific test(s)
npm run test:all <filename|regex>
```

Linting, code coverage and test results are reported in merge requests. Improvements to add tests and/or improve test coverage are welcomed.

Thanks,
Tom


code:
## Installation
##### These instructions are not for the uninitiated and assume you have a brain. Follow them carefully and you will have a nice working imageboard by the end.

##### Requirements
- Linux - Debian used in this example
- Node.js - the application runtime
- MongoDB >= 4.4 - the database
- Redis - session store, task queue, locks, caching, websocket message arbiter
- Nginx - webserver/proxy, serve static files, handle https, GeoIP lookup
- Certbot/letsencrypt - to get a free https certificate
- Graphicsmagick+Imagemagick - identify and thumbnail images, generate captchas
- Ffmpeg - identify and thumbnail audio, video and gifs

**0. Read the LICENSE**

**1. Setup server with some basics**

- Separate, unpriveliged user to run the application.
- Basic security:
  - Sshd root login disabled, key login only, listen only on desired interface.
  - Firewall (ufw works) to deny all incoming on ports besides http/s and sshd.
  - Setup unattended-upgrades for security patches.
- Set the timezone to UTC.
- Clone the repo somewhere. The homedir for the user you setup or /var/www should work.

**2. Install dependencies.**

```bash
sudo apt update -y
sudo apt install curl wget libgeoip-dev gnupg ffmpeg imagemagick graphicsmagick fontconfig fonts-dejavu certbot -y
```

**3. Install MongoDB**

NOTE: As per the MongoDB Documentation, since MongoDB 5.0, the AVX instruction set is required.

> MongoDB 5.0 requires use of the AVX instruction set, available on [select Intel and AMD processors](https://en.wikipedia.org/wiki/Advanced_Vector_Extensions#CPUs_with_AVX).

Please ensure your hardware is supported before reporting issues. The complete platform support matrix is available [here](https://www.mongodb.com/docs/manual/administration/production-notes/#platform-support-matrix).

[MongoDB Installation](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-debian/#install-mongodb-community-edition-on-debian):
```bash
wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add -
echo "deb http://repo.mongodb.org/apt/debian $(lsb_release -sc)/mongodb-org/7.0 main" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt update
sudo apt install -y mongodb-org
sudo systemctl enable --now mongod
```

NOTE: If at this point, mongod doesn't start or has an error, fix the permissions ([stackoverflow](https://stackoverflow.com/questions/64608581/mongodb-code-exited-status-14-failed-but-not-any-clear-errors/66107451#66107451)):
```bash
sudo mkdir -p /var/lib/mongodb
sudo mkdir -p /var/log/mongodb
sudo chown -R mongodb:mongodb /var/lib/mongodb
sudo chown -R mongodb:mongodb /var/log/mongodb
sudo chown mongodb:mongodb /tmp/mongodb-27017.sock
sudo service mongod restart
```

[Enable authentication](https://www.mongodb.com/features/mongodb-authentication):
```bash
#NOTE: change "CHANGE-ME-YOUR-SECURE-MONGODB-PASSWORD" to something secure.
mongosh admin --eval "db.getSiblingDB('jschan').createUser({user: 'jschan', pwd: 'CHANGE-ME-YOUR-SECURE-MONGODB-PASSWORD', roles: [{role:'readWrite', db:'jschan'}]})"
sudo sh -c "cat > /etc/mongod.conf" <<'EOF'
storage:
  dbPath: /var/lib/mongodb
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log
net:
  port: 27017
  bindIp: 127.0.0.1
processManagement:
  timeZoneInfo: /usr/share/zoneinfo
security:
  authorization: "enabled"
EOF

sudo systemctl restart mongod
#NOTE: to access to DB directly in future:
#mongosh "mongodb://jschan:CHANGE-ME-YOUR-SECURE-MONGODB-PASSWORD@127.0.0.1:27017/jschan"
```

**4. Install Redis**

[Redis Installation](https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-debian-10):
```bash
sudo apt update -y
sudo apt install redis-server -y
sudo sed -i -e 's/supervised no/supervised systemd/' -e '$!b' -e '/# supervised auto/!b' -e 's/# supervised auto/supervised auto/' -e '$!s/$/\nsupervised systemd/' /etc/redis/redis.conf
sudo systemctl enable --now redis-server
```

Enable authentication:
```
echo "requirepass CHANGE-ME-YOUR-SECURE-REDIS-PASSWORD" | sudo tee -a /etc/redis/redis.conf
sudo systemctl restart redis-server
```

**5. Install Node.js**

For easy installation, use [node version manager](https://github.com/nvm-sh/nvm#installing-and-updating) "nvm":
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
```

Then run the following command to get the LTS version of nodejs.
```bash
nvm install --lts
```

Installing Node.js yourself without nvm is possible, but unsupported by this guide.

**6. (Optional) If you want a .onion address (Tor) and/or .loki address (Lokinet)**

Install Tor, setup a hidden service, and output your .onion address:
```bash
sudo apt install tor -y
sudo systemctl enable --now tor
sudo sh -c "cat > /etc/tor/torrc" <<'EOF'
HiddenServiceDir /var/lib/tor/jschan/
HiddenServiceVersion 3
HiddenServicePort 80 unix:/var/run/nginx-tor.sock
EOF

sudo systemctl restart tor
until [ -f /var/lib/tor/jschan/hostname ]
do
     sleep 1
done
sudo cat /var/lib/tor/jschan/hostname
```

Install Lokinet, setup a SNApp, and find your .loki address:
```bash
sudo curl -so /etc/apt/trusted.gpg.d/oxen.gpg https://deb.oxen.io/pub.gpg
echo "deb https://deb.oxen.io $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/oxen.list
sudo apt update -y
sudo apt install lokinet -y
sudo systemctl enable --now lokinet
sudo sh -c "cat > /var/lib/lokinet/lokinet.ini" <<'EOF'
[router]
[network]
keyfile=/var/lib/lokinet/snappkey.private
[paths]
[dns]
[bind]
[api]
[bootstrap]
[logging]
EOF

sudo systemctl restart lokinet
nslookup -type=cname localhost.loki # Your loki address is the "canonical name".
```

For Lokinet, make sure to firewall all ports except 80 on `lokitun0` interface. If you use `ufw` for example, you could do:
```bash
sudo ufw deny in on lokitun0 to any
sudo ufw allow in on lokitun0 to any port 80 proto tcp
```

Note down the .loki and .onion address for the next step.

**7. Setup nginx**

Become root:
```bash
sudo su -
```

Once you are root:
```
wget https://raw.githubusercontent.com/fatchan/nginx-autoinstall/master/nginx-autoinstall.sh
chmod +x nginx-autoinstall.sh
HEADLESS=y OPTION=1 NGINX_VER=STABLE SUBFILTER=y RTMP=y ./nginx-autoinstall.sh
echo "You can safely ignore that error about restarting nginx ^"
rm nginx-autoinstall.sh
sudo mkdir -p /etc/nginx/snippets
exit # this exits being root
```

Back as your non-root sudo user, from within your jschan folder:
```
sudo bash configs/nginx/nginx.sh
```

For non-standard installations like using a CDN, see [configs/nginx/README.md](configs/nginx/README.md) and DIY.

**8. Get the backend setup & running**

1. Copy the example secrets file and edit it with your mongodb+redis credentials, cookie secrets, etc:
```bash
cp configs/secrets.js.example configs/secrets.js && editor configs/secrets.js
```

2. Edit the rules and FAQ page if desired:
```bash
editor views/custompages/faq.pug views/custompages/rules.pug
```

3. Install nodejs dependencies:
```bash
npm install
```

4. Replace `gulp/res/icons/master.png` with your desired favicon image.

5. Run the setup npm run-script (runs some gulp tasks).
```bash
npm run-script setup
```

6. Run gulp reset to initialize the database and folder structure, and create the admin account. **Default admin account with random password will be printed to the command line.** NOTE: dont run gulp reset again unless you want to completely irreversibly wipe everything.
```bash
npx gulp reset
```

7. Install pm2 completions for your terminal, and install a system service and load on system startup. **NOTE: This will also output some additional commands you need to run to complete the process. Read the command output carefully.**
```
npx pm2 completion install
npx pm2 startup
```

8. Start all the backend processes.
```bash
npm run-script start
npx gulp
npx pm2 save
```

Some commands you may need to use in future/find helpful:
- `pm2 list` - Lists running pm2 processes.
- `pm2 logs` - Start tailing logs.
- `pm2 reload ecosystem.config.js` - Reload all backend processes.
- `gulp password` - Reset the admin account password if you forgot it.
- `gulp` - Run the default gulp task.

More information and commands for customisation or advanced use is in the ADVANCED section.

**9. (Optional) if you plan to use the webring and want to make requests with a proxy to mask your origin server IP**

EITHER:

- Use the socks proxy provided by a non-docker tor daemon, which is probably already setup on port 9050 if you have tor installed for a hidden service.
- Install docker and run torproxy in a container: https://github.com/dperson/torproxy (of course, audit the docker image yourself).
- Use your own socks proxy

Either of the first two options will allow you to follow .onions in your webring follow list.

To enable the proxy, tick "Use Socks Proxy" in global management settings and set the appropriate proxy address, e.g. `socks5h://127.0.0.1:9050`, then save settings.

**10. All done!**

## Updating

0. `cd` to your jschan installation folder.

1. Stop the running backend:
```bash
npx pm2 stop ecosystem.config.js
```

2. (Optional) Update node.js to the latest LTS node version, latest npm, and reinstall global packages. NOTE: Only works if you installed node.js "nvm" as per the recommendation in the installation instructions.
```bash
nvm install --lts --reinstall-packages-from=$(node --version) --latest-npm
```

3. Pull the latest changes from git:
```bash
git pull
```

4. Install nodejs dependencies again in case any have updated or changed
```bash
npm install
```

5. Check if the nginx config needs updating, comparing the version you updated from with the current version:
```bash
git diff v0.9.2 v0.8.0 configs/nginx
```
If the output was blank, goto step 6.

If the output showed changes and you used `configs/nginx/nginx.sh` to setup nginx, run it again to reconfigure nginx and overwrite your old configuration.

If you have a non-standard nginx config, update your nginx config yourself.

6. Run the gulp migrate task. This will run "migrations" such as updating the database schema or file structure:
```bash
npx gulp migrate && npx gulp
```

7. Start the backend:
```bash
npx pm2 restart ecosystem.config.js --env production
```

At this point, your installation is updated. If something is broken, check and read the logs, they will help figure out what went wrong:
```bash
npx pm2 logs
```

## Help! Something didn't work!!!1!!1

If you are sure you did everything correctly and you still can't get it working, you can ask for help in the IRC (linked in [README](README.md)).

Be polite, be patient, ask [smart questions](http://www.catb.org/~esr/faqs/smart-questions.html), and keep in mind nobody is obliged to help you.

Paid support is available at a rate of $50USD/hr, payable in cryptocurrency (BTC/XMR) only. Email the address on my [Gitgud profile](https://gitgud.io/fatchan) to inquire.

## Advanced

This is an optional section for people who know what they are doing.

Here are some additional "advanced" things you can do with your jschan installation:

<details><summary>Advanced</summary>

#### Performance tweaks

**Using unix sockets for nginx, tor, redis, and mongodb:**

If you run an instance with local redis, mongodb, tor, nginx, etc then you can (usually) net some performance improvement by making them communicate with unix sockets rather than TCP. No more localhost. The biggest disadvantage is making sure each component that communicates using a given socket has the correct permissions set. Generally, creating a socket that is owned by a group to which you add both components is good enough.

This example is for nginx <-> tor daemon communication. You should be able to work out redis and mongodb yourself based on this.

In the nginx `server {}` block:

```nginx
listen unix:/var/run/nginx-tor.sock;
allow "unix:";
deny all;
```

In your torrc:

```
HiddenServicePort 80 unix:/var/run/nginx-tor.sock
```

sudo systemctl restart tor && sudo systemctl restart nginx

**Don't use pm2/nodejs clustering:**

By default, the "chan" pm2 process (the main web serving component of jschan backend) will be "clustered". In nodejs this means that you can have multiple processes e.g. sharing the same port, whereby the "master" will accept connections and pass the handlers over to each process in a round-robin fashion. The pm2 master process handles all this and allows for nice things like zero downtime reloads. The downside is that there is a slight overhead associated with having that single thread accept connections and delegate them to each worker. Instead, you can run each chan process on a sequential port, starting from the port in configs/secrets.js. Then you can configure multiple ip:ports in nginx to connect to each nodejs process independently.

**Note: pm2 reload will no longer give zero downtime restarts this way.**

1. Change the `exec_mode` in ecosystem.config.js and some code in server.js for listening to the ports:

```diff
diff --git a/ecosystem.config.js b/ecosystem.config.js
index fc000094..f2b53c1b 100644
--- a/ecosystem.config.js
+++ b/ecosystem.config.js
@@ -30,0 +31 @@ module.exports = {
+               exec_mode: 'fork',
diff --git a/server.js b/server.js
index f9e19dbc..1630dce0 100644
--- a/server.js
+++ b/server.js
@@ -152 +152 @@ const express = require('express')
-       server.listen(port, '127.0.0.1', () => {
+       server.listen(port+parseInt(process.env.NODE_APP_INSTANCE), '127.0.0.1', () => {
```

2. Change the `upstream` block in your jschan sites-available nginx config to have a backend for each chan process, like so:

```nginx
upstream chan {
	server 127.0.0.1:7000;
	server 127.0.0.1:7001;
	server 127.0.0.1:7002;
	server 127.0.0.1:7003;
}
```

You will need to match the number of servers to however many chan processes you are running, which by default is (number of cpu threads/2)

3. pm2 restart ecosystem.config.js --env production && sudo systemctl restart nginx

#### Customisation

Customisation is pretty easy. As long as you format whatever you are customising properly e.g. pug templates, css, etc and have correct syntax for javascript files, the build system will handle the rest for you. Here are some things you can do:

**Custom pages**

To add additional custom pages which will be at the root of your site, add a .pug file to views/custompages/. See rules.pug or faq.pug as an example. These will get added to the root of your site just like /rules.html and /faq.html.

Pug template language reference: https://pugjs.org/api/getting-started.html

To build all custompages, run `npx gulp custompages`.

**Custom CSS & themes**

All css files in gulp/res/css/ will get combined and minified into the main style.css for the site. If you want to edit the css, it is advised to not edit gulp/res/css/style.css directly as this can change between updates. Instead, add a "custom.css" (example name only) to the same folder with what ever css you want. This will be included after style.css so rules will take precedence.

Theme files in gulp/res/css/themes/ can also be edited, if desired. You can also create new themes copying their general format of including variables inside `:root{}`.

To build all css files, run `npx gulp css`. For some situations, such as adding or removing themes, you should run `npx gulp` and `pm2 restart all` because scripts and templates containing the theme selector dropdowns and server-side checks for valid theme names will need to be updated.

#### Handy nginx stuff

For detecting and automatically updating Tor exit node lists, see [tools/update_tor_exits.sh](tools/update_tor_exits.sh)

For updating the GeoIP database for nginx, see [tools/update_geoip.sh](tools/update_geoip.sh)

#### Docker

Experimental, strictly for development only.

Basically:

```bash
docker-compose up -d mongodb redis

#on the first run, or to "npx gulp reset" later:
docker-compose up jschan-reset

docker-compose up -d jschan

docker-compose up -d nginx
```

</details>


code:
# jschan

Anonymous imageboard software that doesn't suck.

Repo Mirror(s):
 - https://gitgud.io/fatchan/jschan
 - https://git.basedflare.com/fatchan/jschan

Contact the author:
 - [Session](https://getsession.org/): `051b2ff270769d20764fa1b8e6bc3240b0a3c28ffb3242e7cce60db479b23ef427`
 - Telegram: [t.me/basedflare](https://t.me/basedflare)
 - Email: see my gitgud profile

## Live instances (Unofficial)
 - 🇵🇹/🇧🇷 [ptchan](https://ptchan.org)
 - 🇺🇸 [zzzchan](https://zzzchan.xyz)
 - 🇺🇸/🇰🇷 [heolkek](https://heolkek.cafe)
 - 🇺🇸 [sportschan](https://sportschan.org)
 - 🇺🇸 [trashchan](https://trashchan.xyz/index.html)
 - 🇧🇷 [27chan](https://27chan.org)
 - 🇺🇸 [jaksoy.party](https://jaksoy.party)
 - 🇮🇹 [nuichan](https://niuchan.org)
 - And many more...

## Features
 - [x] Multiple language support (🇬🇧 🇵🇹 🇧🇷 🇷🇺 🇮🇹 🇪🇸)
 - [x] Optional user created boards
 - [x] Multiple files per post
 - [x] Antispam/Anti-flood & DNSBL
 - [x] 3 customisable inbuilt captchas + 3 third party captchas (hcaptcha, recaptcha, yandex smartcaptcha)
 - [x] Two factor authentication (TOTP) for accounts
 - [x] Manage everything from the web panel
 - [x] Granular account permissions
 - [x] Works properly with anonymizer networks (Tor, Lokinet, etc)
 - [x] Web3 integration - register, login, and sign posts with [MetaMask](https://metamask.io)
 - [x] [Tegaki](https://github.com/desuwa/tegaki) applet with drawing and replays
 - [x] [API documentation](https://fatchan.gitgud.site/jschan-docs/)
 - [x] Built-in webring (compatible w/ [lynxchan](https://gitlab.com/alogware/LynxChanAddon-Webring) & [infinity](https://gitlab.com/Tenicu/infinityaddon-webring))
 - [x] Beautiful bundled frontend with lots of themes and options, see below:

![screenshots](collage.gif "screenshots")

## License
GNU AGPLv3, see [LICENSE](LICENSE).

## Installation & Upgrading
See [INSTALLATION.md](INSTALLATION.md) for instructions on setting up a jschan instance or upgrading to a newer version.

## Changelog
See [CHANGELOG.md](CHANGELOG.md) for changes between versions.

## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.

## Related Projects

Official:
 - [fatchan/jschan-docs](https://gitgud.io/fatchan/jschan-docs/) - API Documentation for jschan ([gitgud pages](https://fatchan.gitgud.site/jschan-docs/#introduction))
 - [fatchan/jschan-api-go](https://gitgud.io/fatchan/jschan-api-go) - WIP Golang API Client for jschan ([gitgud pages](https://fatchan.gitgud.site/jschan-api-go/pkg/jschan/))
 - [jschan-antispam group](https://gitgud.io/jschan-antispam/) - Multiple projects that are compatible with jschan and implement more sophisticated antispam capabilities.

Unofficial: **Not guaranteed to work or be safe, use at your own risk.**
 - [globalafk](https://github.com/loynet/automod) - "A simple python script that sends ugly notifications when something happens on a jschan imageboard that you moderate."
 - [reporter](https://github.com/loynet/reporter) - news bot that fetches news from a provider and posts a snippet of it on a configurable board of a configurable jschan imageboard
 - [yacam](https://github.com/loynet/yacam) - a bot that tries to detect (dumb) spam on jschan imageboards and does something about it

## For generous people

Bitcoin (BTC): [`bc1q4elrlz5puak4m9xy3hfvmpempnpqpu95v8s9m6`](bitcoin:bc1q4elrlz5puak4m9xy3hfvmpempnpqpu95v8s9m6)

Monero (XMR): [`89J9DXPLUBr5HjNDNZTEo4WYMFTouSsGjUjBnUCCUxJGUirthnii4naZ8JafdnmhPe4NP1nkWsgcK82Uga7X515nNR1isuh`](monero:89J9DXPLUBr5HjNDNZTEo4WYMFTouSsGjUjBnUCCUxJGUirthnii4naZ8JafdnmhPe4NP1nkWsgcK82Uga7X515nNR1isuh)


code:
`sites-available.example` is a sample /etc/nginx/sites-available/example.com file
`snippets/*` are sample snippets for /etc/nginx/snippets/

For standard installs, run nginx.sh for easy configuration with prompts.

For non-standard installations, DIY.

If you use cloudflare, please read [these](https://support.cloudflare.com/hc/en-us/articles/200170786-Restoring-original-visitor-IPs-Logging-visitor-IP-addresses-with-mod-cloudflare-) [articles](https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation) to setup proper IP forwarding and geolocation headers. Similar steps would apply to other CDNs/reverse proxies.


code:
#!/bin/bash
#sets up nginx config

#are you root?
[[ "$EUID" -ne 0 ]] && echo "Please run as root" && exit;

echo "[jschan nginx configuration helper]"
read -p "Enter the directory you cloned jschan, no trailing slash. (blank=$(pwd)): " JSCHAN_DIRECTORY
JSCHAN_DIRECTORY=${JSCHAN_DIRECTORY:-$(pwd)}
read -p "Enter your clearnet domain name e.g. example.com (blank=no clearnet domain): " CLEARNET_DOMAIN
SITES_AVAILABLE_NAME=${CLEARNET_DOMAIN:-jschan} #not sure on a good default, used for sites-available config name
read -p "Enter tor .onion address (blank=no .onion address): " ONION_DOMAIN
read -p "Enter lokinet .loki address (blank=no .loki address): " LOKI_DOMAIN
read -p "Would you like to add a www. subdomain? (y/n): " ADD_WWW_SUBDOMAIN
if [ "$CLEARNET_DOMAIN" != "" ]; then
	read -p "Run certbot and automatically configure a certificate for https on clearnet? (y/n): " CERTBOT
	if [ "$CERTBOT" == "n" ]; then
		read -p "Generate a self-signed certificate instead? (y/n): " SELFSIGNED
	fi
	if [ "$SELFSIGNED" == "n" ]; then
		read -p "Warning: no https certificate chosen for clearnet. Continue without https? (y/n): " NOHTTPS
		[[ "$NOHTTPS" == "n" ]] && echo "Exiting..." && exit;
	fi
fi
read -p "Should robots.txt disallow compliant crawlers? (y/n): " ROBOTS_TXT_DISALLOW
read -p "Allow google captcha in content-security policy? (y/n): " GOOGLE_CAPTCHA
read -p "Allow Hcaptcha in content-security policy? (y/n): " H_CAPTCHA
read -p "Allow Yandex SmartCaptcha in content-security policy? (y/n): " Y_CAPTCHA
read -p "Download and setup geoip for post flags? (y/n): " GEOIP

#looks good?
read -p "Is this correct?
jschan directory: $JSCHAN_DIRECTORY
clearnet domain: $CLEARNET_DOMAIN
.onion address: $ONION_DOMAIN
.loki address: $LOKI_DOMAIN
www subdomains: $ADD_WWW_SUBDOMAIN
certbot https cert: $CERTBOT
self-signed https cert: $SELFSIGNED
no https cert: $NOHTTPS
robots.txt disallow all: $ROBOTS_TXT_DISALLOW
google captcha: $GOOGLE_CAPTCHA
hcaptcha: $H_CAPTCHA
yandex captcha: $Y_CAPTCHA
geoip: $GEOIP
(y/n): " CORRECT
#not saying no = yes, just like real life
[[ "$CORRECT" == "n" ]] && echo "Exiting..." && exit;

#ask to overwrite if already exists
if [[ -f /etc/nginx/sites-available/$SITES_AVAILABLE_NAME.conf ]]; then
	read -p "/etc/nginx/sites-available/$SITES_AVAILABLE_NAME.conf already exists. Continue and overwrite existing configuration? (y/n)" OVERWRITE
	[[ "$OVERWRITE" == "n" ]] && echo "Exiting..." && exit;
	rm /etc/nginx/sites-available/$SITES_AVAILABLE_NAME.conf
	rm /etc/nginx/sites-enabled/$SITES_AVAILABLE_NAME.conf
fi

if [[ -f /etc/nginx/sites-available/$SITES_AVAILABLE_NAME ]]; then
	read -p "A configuration for previous versions /etc/nginx/sites-available/$SITES_AVAILABLE_NAME already exists. Back this up to current dir ($pwd) before creating new configuration? (y/n)" MV_OLD_CONFIG
	[[ "$MV_OLD_CONFIG" == "n" ]] && echo "Exiting..." && exit;
	cp /etc/nginx/sites-available/$SITES_AVAILABLE_NAME "$(pwd)/"
	echo "Copied old config /etc/nginx/sites-available/$SITES_AVAILABLE_NAME to $(pwd)/$SITES_AVAILABLE_NAME"
	rm /etc/nginx/sites-available/$SITES_AVAILABLE_NAME
	rm /etc/nginx/sites-enabled/$SITES_AVAILABLE_NAME
fi

echo "Stopping nginx..."
sudo systemctl stop nginx

if [ "$CERTBOT" == "y" ]; then
	#run certbot for certificate
	if [ "$ADD_WWW_SUBDOMAIN" == "y" ]; then
		echo "Running certbot to setup SSL cert for $CLEARNET_DOMAIN and www.$CLEARNET_COMAIN..."
		sudo sudo certbot certonly --standalone --register-unsafely-without-email -d $CLEARNET_DOMAIN -d www.$CLEARNET_DOMAIN
	else
		echo "Running certbot to setup SSL cert for $CLEARNET_DOMAIN..."
		sudo sudo certbot certonly --standalone --register-unsafely-without-email -d $CLEARNET_DOMAIN
	fi
fi

echo "Copying snippets to nginx folder & replacing paths..."
#copy the snippets and replace install path, they aren't templated
sudo cp $JSCHAN_DIRECTORY/configs/nginx/snippets/* /etc/nginx/snippets
sudo sed -i "s|/path/to/jschan|$JSCHAN_DIRECTORY|g" /etc/nginx/snippets/*

#declare teplate start
JSCHAN_CONFIG="upstream chan {
	server 127.0.0.1:7000;
}"

#Use some variabels to make the templating easier later, depending on if they want www. or not
CLEARNET_SERVER_NAME="$CLEARNET_DOMAIN"
LOKI_SERVER_NAME="$LOKI_DOMAIN"
ONION_SERVER_NAME="$ONION_DOMAIN"
if [ "$ADD_WWW_SUBDOMAIN" == "y" ]; then
	CLEARNET_SERVER_NAME="$CLEARNET_DOMAIN www.$CLEARNET_DOMAIN"
	LOKI_SERVER_NAME="$LOKI_DOMAIN www.$LOKI_DOMAIN"
	ONION_SERVER_NAME="$ONION_DOMAIN www.$ONION_DOMAIN"
fi

if [ "$CLEARNET_DOMAIN" != "" ]; then

	sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096
		cat > /etc/nginx/snippets/ssl-params.conf <<EOF
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA";
EOF

	HTTPS_MIDSECTION=""
	HTTPS_CERT_SECTION=""
	if [ "$CERTBOT" == "y" ]; then
		HTTPS_CERT_SECTION="
	ssl_certificate /etc/letsencrypt/live/$CLEARNET_DOMAIN/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/$CLEARNET_DOMAIN/privkey.pem;
	include /etc/nginx/snippets/ssl-params.conf;
	ssl_dhparam /etc/nginx/dhparam.pem;
	"
	elif [ "$SELFSIGNED" == "y" ]; then
		echo "Generating self-signed SSL cert..."
		mkdir /etc/ssl/private/
		mkdir /etc/ssl/certs/
		sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt
		echo "Generating dh group. This may take a while..."
		HTTPS_CERT_SECTION="
	ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
	ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
	ssl_dhparam /etc/nginx/dhparam.pem;
	include /etc/nginx/snippets/ssl-params.conf;
	"
	fi

	if [ "$NOHTTPS" != "y" ]; then
			HTTPS_MIDSECTION="
	listen [::]:443 ssl ipv6only=on;
	listen 443 ssl;
	$HTTPS_CERT_SECTION

}

server {
	if (\$host = www.$CLEARNET_DOMAIN) {
		return 301 https://\$host\$request_uri;
	}

	if (\$host = $CLEARNET_DOMAIN) {
		return 301 https://\$host\$request_uri;
	}

	server_name $CLEARNET_SERVER_NAME;
	return 444;
"
	fi

	#onion_location redirect header
	ONION_LOCATION=""
	if [ "$ONION_DOMAIN" != "" ]; then
		ONION_LOCATION="add_header onion-location 'http://$ONION_DOMAIN\$request_uri';"
	fi

	#concat clearnet server{} block
	JSCHAN_CONFIG="${JSCHAN_CONFIG}

server {
	server_name $CLEARNET_SERVER_NAME;
	client_max_body_size 0;

	$ONION_LOCATION

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_clearnet_routes.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;

$HTTPS_MIDSECTION

	listen 80;
	listen [::]:80;
}"

	#replace clearnet domain in snippets
	echo "Replacing clearnet domain in snippets..."
	sudo sed -i "s/example.com/$CLEARNET_DOMAIN/g" /etc/nginx/snippets/*

fi

if [ "$ONION_DOMAIN" != "" ]; then

	#concat onion server{} block
	JSCHAN_CONFIG="${JSCHAN_CONFIG}

server {
	server_name www.$ONION_DOMAIN $ONION_DOMAIN;
	client_max_body_size 0;

	listen unix:/var/run/nginx-tor.sock;
	allow 'unix:';
	deny all;

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
	include /etc/nginx/snippets/jschan_tor_routes.conf;
}"

	#replace onion domain in snippets
	echo "Replacing onion domain in snippets..."
	sudo sed -i "s/example.onion/$ONION_DOMAIN/g" /etc/nginx/snippets/*

else
	#no onion, remove it from CSP
	echo "No onion, removing example.onion from CSP..."
	sudo sed -i 's/ wss:\/\/www.example.onion\/ wss:\/\/example.onion\///g' /etc/nginx/snippets/security_headers*
fi

if [ "$LOKI_DOMAIN" != "" ]; then

	#concat lokinet server{} block
	JSCHAN_CONFIG="${JSCHAN_CONFIG}

server {
	server_name www.$LOKI_DOMAIN $LOKI_DOMAIN;
	client_max_body_size 0;

	#address may vary if this address is already used by something other than lokinet
	listen 172.16.0.1:80;

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
	include /etc/nginx/snippets/jschan_loki_routes.conf;
}"

	#replace lokinet domain in snippets
	echo "Replacing loki domain in snippets..."
	sudo sed -i "s/example.loki/$LOKI_DOMAIN/g" /etc/nginx/snippets/*

else
	#no lokinet, remove it from csp
	echo "No lokinet, removing example.loki from CSP..."
	sudo sed -i 's/ wss:\/\/www.example.loki\/ wss:\/\/example.loki\///g' /etc/nginx/snippets/security_headers*
fi

#write the config to file and syymlink to sites-available
echo "Writing main jschan vhost config..."
printf "$JSCHAN_CONFIG" > /etc/nginx/sites-available/$SITES_AVAILABLE_NAME.conf
sudo ln -s -f /etc/nginx/sites-available/$SITES_AVAILABLE_NAME.conf /etc/nginx/sites-enabled/$SITES_AVAILABLE_NAME.conf

if [ "$NOHTTPS" == "y" ]; then
	echo "Adjusting config snippets to support NOHTTPS mode..."
	sudo sed -i "s/Forwarded-Proto https/Forwarded-Proto http/g" /etc/nginx/snippets/jschan_clearnet_routes.conf
fi

if [ "$GOOGLE_CAPTCHA" == "y" ]; then
	echo "Allowing recaptcha in CSP..."
	#add google captcha CSP exceptions
	sudo sed -i "s|script-src|script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ |g" /etc/nginx/snippets/*
	sudo sed -i "s|frame-src|frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/ |g" /etc/nginx/snippets/*
fi

if [ "$H_CAPTCHA" == "y" ]; then
	echo "Allowing hcaptcha in CSP..."
	#add hcaptcha CSP exceptions
	sudo sed -i "s|script-src|script-src https://hcaptcha.com https://*.hcaptcha.com |g" /etc/nginx/snippets/*
	sudo sed -i "s|frame-src|frame-src https://hcaptcha.com https://*.hcaptcha.com |g" /etc/nginx/snippets/*
	sudo sed -i "s|style-src|style-src https://hcaptcha.com https://*.hcaptcha.com |g" /etc/nginx/snippets/*
	sudo sed -i "s|connect-src|connect-src https://hcaptcha.com https://*.hcaptcha.com |g" /etc/nginx/snippets/*
fi

if [ "$Y_CAPTCHA" == "y" ]; then
	echo "Allowing Yandex SmartCaptcha in CSP..."
	#add yandex captcha CSP exceptions
	sudo sed -i "s|script-src|script-src https://smartcaptcha.yandexcloud.net/captcha.js |g" /etc/nginx/snippets/*
	sudo sed -i "s|frame-src|frame-src https://smartcaptcha.yandexcloud.net/ |g" /etc/nginx/snippets/*
	sudo sed -i "s|font-src|font-src yastatic.net |g" /etc/nginx/snippets/*
fi

if [ "$ROBOTS_TXT_DISALLOW" == "y" ]; then
	echo "Setting robots.txt to disallow all..."
	#add path / (all) to disallow to make robots.txt block all robots instead of allowing
	sudo sed -i "s|Disallow:|Disallow: /|g" /etc/nginx/snippets/jschan_common_routes.conf
fi

if [ "$GEOIP" == "y" ]; then
	echo "Downloading and installing geoip database for nginx..."
	#download geoip data
	cd /usr/share/GeoIP
	wget --retry-connrefused -qO- "https://download.db-ip.com/free/dbip-country-lite-`date +%Y-%m`.mmdb.gz"  | tee "/usr/share/GeoIP/dbip.mmdb.gz" >/dev/null
	gunzip "/usr/share/GeoIP/dbip.mmdb.gz"
	#add goeip_country to /etc/nginx/nginx.conf, only if not already exists
	grep -qF "geoip_country" /etc/nginx/nginx.conf
	if [ $? -eq 1 ]; then
		sudo sed -i '/http {/a \
geoip_country /usr/share/GeoIP/dbip.mmdb;' /etc/nginx/nginx.conf
	fi
else
	echo "Geoip not installed, removing directives..."
	sudo sed '/geoip_country/d' /etc/nginx/nginx.conf
	sudo sed '/geoip_country_code/d' /etc/nginx/snippets/jschan_clearnet_routes.conf
fi

echo "Restarting nginx..."
#and restart nginx
sudo systemctl restart nginx


code:
upstream chan {
	server 127.0.0.1:7000;
}

server {
	server_name www.example.loki example.loki;
	client_max_body_size 0;

	listen 172.16.0.1:80;

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
	include /etc/nginx/snippets/jschan_loki_routes.conf;
}

server {
	server_name www.example.onion example.onion;
	client_max_body_size 0;

	listen unix:/var/run/nginx-tor.sock;
	allow "unix:";
	deny all;

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
	include /etc/nginx/snippets/jschan_tor_routes.conf;
}

server {
	server_name www.example.com example.com;
	client_max_body_size 0;

	add_header onion-location 'http://example.onion$request_uri';

	listen [::]:443 ssl ipv6only=on; # managed by Certbot
	listen 443 ssl; # managed by Certbot
	ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
	ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
	include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

	include /etc/nginx/snippets/security_headers.conf;
	include /etc/nginx/snippets/error_pages.conf;
	include /etc/nginx/snippets/jschan_clearnet_routes.conf;
	include /etc/nginx/snippets/jschan_common_routes.conf;
}

server {
	if ($host = www.example.com) {
		return 301 https://$host$request_uri;
	} # managed by Certbot

	if ($host = example.com) {
		return 301 https://$host$request_uri;
	} # managed by Certbot

	server_name www.example.com example.com;

	listen 80;
	listen [::]:80;
	return 444; # managed by Certbot
}


code:
error_page 404 /404.html;
error_page 500 /500.html;
error_page 502 /502.html;
error_page 503 /503.html;
error_page 504 /504.html;
location = /404.html {
	root /path/to/jschan/static/html;
	internal;
}
location = /500.html {
	root /path/to/jschan/static/html;
	internal;
}
location = /502.html {
	root /path/to/jschan/static/html;
	internal;
}
location = /503.html {
	root /path/to/jschan/static/html;
	internal;
}
location = /504.html {
	root /path/to/jschan/static/html;
	internal;
}


code:
location / {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;

	proxy_set_header Host $host;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection 'upgrade';
	proxy_cache_bypass $http_upgrade;

	proxy_set_header X-Forwarded-Proto https;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code $geoip_country_code;
}

location @backend {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto https;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code $geoip_country_code;
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}

location @backend-private {
	include /etc/nginx/snippets/security_headers_nocache.conf;
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto https;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code $geoip_country_code;
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}


code:
if ($request_uri ~ ^/(?!captcha|randombanner|forms|logout|socket\.io)) {
	rewrite ^([^.\?]*[^/])$ $1/ redirect;
	rewrite ^(.+)/$ $1/index.html redirect;
}

location = /robots.txt {
	access_log off;
	add_header Content-Type text/plain;
	return 200 "User-agent: *\nDisallow:\n";
}

location = /favicon.ico {
	access_log off;
	expires max;
	root /path/to/jschan/static/file;
	try_files $uri =404;
}

location = / {
	return 302 $scheme://$host/index.html;
}

location /captcha {
	root /path/to/jschan/static/captcha;
	if ($cookie_captchaid) {
		return 302 $scheme://$host/captcha/$cookie_captchaid.jpg;
	}
	try_files /$cookie_captchaid.jpg @backend;
}

# authed, no cache pages
location ~* ^/((\w+/manage/.*|globalmanage/(reports|bans|recent|boards|filters|globallogs|custompages|news|editnews/.*|accounts|editaccount/.*|roles|editrole/.*|settings))|sessions|mypermissions|account|create|twofactor|csrf)\.(html|json)$ {
	expires 0;
	try_files /dev/null @backend-private;
}

# TODO: remove, redirect from legacy custompage paths
location ~* ^/(\w+)/custompage/(.*\.html)$ {
        return 302 /$1/page/$2;
}

# public html
location ~* \.html$ {
	expires 0;
	root /path/to/jschan/static/html;
	try_files $uri @backend;
}

# public json
location ~* \.json$ {
	expires 0;
	root /path/to/jschan/static/json;
	try_files $uri @backend;
}

# CSS
location ~* \.css$ {
	access_log off;
	expires 1w;
	root /path/to/jschan/static;
	try_files $uri =404;
}

# Scripts
location ~* \.js$ {
	expires 1w;
	access_log off;
	root /path/to/jschan/static;
	try_files $uri =404;
}

# Files (image, video, audio, other)
location ~* \.(png|jpg|jpeg|webmanifest|xml|ico|apng|bmp|webp|pjpeg|jfif|gif|mp4|webm|mov|mkv|svg|flac|mp3|m4a|ogg|wav|opus|ttf|woff2)$ {
	access_log off;
	expires max;
	root /path/to/jschan/static;
	try_files $uri =404;
}

# inline in browser so even HTML filetypes can be offered and will present a "save" dialog box
location ~* \.(txt|pdf|tgkr)$ {
	access_log off;
	expires max;
	add_header Cache-Control "public";
	add_header X-Content-Type-Options "nosniff" always;
	add_header Content-Disposition "attachment";
	root /path/to/jschan/static;
	try_files $uri =404;
}


code:
location / {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;

	proxy_set_header Host $host;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection 'upgrade';
	proxy_cache_bypass $http_upgrade;

	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'LOKI';
}

location @backend {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'LOKI';
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}

location @backend-private {
	include /etc/nginx/snippets/security_headers_nocache.conf;
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'LOKI';
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}


code:
location / {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header Host $host;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection 'upgrade';
	proxy_cache_bypass $http_upgrade;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'TOR';
}

location @backend {
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'TOR';
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}

location @backend-private {
	include /etc/nginx/snippets/security_headers_nocache.conf;
	proxy_buffering off;
	proxy_request_buffering off;
	proxy_pass http://chan$request_uri;
	proxy_http_version 1.1;
	proxy_set_header X-Forwarded-Proto http;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Country-Code 'TOR';
	proxy_set_header Connection '';
	proxy_set_header Host $host;
}


code:
include /etc/nginx/snippets/security_headers_nocache.conf;
add_header Cache-Control "public";


code:
add_header Content-Security-Policy "default-src 'self'; media-src 'self' blob:; img-src 'self' blob:; object-src 'self' blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' https://www.youtube-nocookie.com/embed/ https://www.bitchute.com/embed/ https://odysee.com/%24/embed/; connect-src 'self' wss://example.com/ wss://www.example.com/ wss://www.example.onion/ wss://example.onion/ wss://www.example.loki/ wss://example.loki/; font-src 'self'" always;
add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin" always;
add_header X-Frame-Options "sameorigin" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;


code:
module.exports = {

	//mongodb connection string
	dbURL: 'mongodb://jschan:CHANGE-ME-YOUR-SECURE-MONGODB-PASSWORD@127.0.0.1:27017/jschan',

	//database name
	dbName: 'jschan',

	//redis connection info
	redis: {
		host: '127.0.0.1',
		port: '6379',
		password: 'CHANGE-ME-YOUR-SECURE-REDIS-PASSWORD'
	},

	//backend webserver port
	port: 7000,

	//secrets/salts for various things
	cookieSecret: 'changeme',
	tripcodeSecret: 'changeme',
	ipHashSecret: 'changeme',
	postPasswordSecret: 'changeme',

	//keys for google recaptcha
	google: {
		siteKey: 'changeme',
		secretKey: 'changeme'
	},

	//keys for hcaptcha
	hcaptcha: {
		siteKey: '10000000-ffff-ffff-ffff-000000000001',
		secretKey: '0x0000000000000000000000000000000000000000'
	},

	//keys for yandex smartcaptcha
	yandex: {
		siteKey: 'changeme',
		secretKey: 'changeme'
	},

	//enable debug logging
	debugLogs: true,

};


code:
module.exports = {

	globalAnnouncement: {
		markdown: '',
		raw: '',
	},

	secureCookies: true,

	//check referrer to prevent some CSRF attack
	refererCheck: false,

	//list of allowed host for checking referrer
	allowedHosts: [],

	//header for country codes
	countryCodeHeader: 'x-country-code',
	//header for visitor IP
	ipHeader: 'x-real-ip',

	//data used in opengraph meta tags. used to generate link previews in e.g. discord, twitter, etc
	meta: {
		siteName: '',
		url: ''
	},

	language: 'en-GB',

	//settings for captchas
	captchaOptions: {
		type: 'text',
		generateLimit: 250,
		font: 'default',
		text: {
			line: true,
			wave: 0,
			paint: 2,
			noise: 0,
		},
		grid: {
			falses: ['○','□','♘','♢','▽','△','♖','✧','♔','♘','♕','♗','♙','♧'],
			trues: ['●','■','♞','♦','▼','▲','♜','✦','♚','♞','♛','♝','♟','♣'],
			question: 'Select the solid/filled icons',
			size: 4,
			imageSize: 120,
			iconYOffset: 15,
			edge: 25,
			noise: 0,
		},
		numDistorts: {
			min: 2,
			max: 3
		},
		distortion: 7,
	},

	/* dnsbl, will add a small delay for uncached requests. You could also install some
		kind of dns cache e.g. unbound to improve performance. DNSBL only checked for posting */
	dnsbl: {
		enabled: false,
		blacklists: ['tor.dan.me.uk'],
		cacheTime: 3600 //in seconds, idk whats a good value
	},

	//TODO: default these true and update instructions for how to disable
	forceAccountTwofactor: false,
	forceActionTwofactor: false,

	//disable file posting over anonymizers globally, overrides any board setting.
	disableAnonymizerFilePosting: false,

	//count anon posters (e.g. loki, tor .onion) as "users" in stats. if set to false, all .onion is counted as a single user. doesnt affect pph stat.
	statsCountAnonymizers: true,

	floodTimers: { //basic delays to stop flooding, in ms. 0 to disable
		sameContentSameIp: 120000, //same message or any file from same ip
		sameContentAnyIp: 30000, //same message or file from any ip
		anyContentSameIp: 5000, //any post from the same ip
	},

	//block bypasses
	blockBypass: {
		enabled: false,
		forceAnonymizers: true,
		expireAfterUses: 50, //however many (attempted) posts per block bypass captcha
		expireAfterTime: 86400000, //expiry in ms regardless if the limit was reached, default 1 day
		bypassDnsbl: false,
	},

	/* delete files immediately rather than pruning later. usually disabled to prevent re-thumbnailing and processing commonly
		uploaded files, but deleting immediately is better if you are concerned about "deleted" content not being immediately removed */
	pruneImmediately: true,

	//do not use
	hashImages: false,

	rateLimitCost: { //Cost out of 100 per minute e.g. cost of 25 means 4 per minute. Cost is separate for each.
		captcha: 10,
		boardSettings: 30,
		editPost: 30,
	},

	//how many threads to show on overboard index view
	overboardLimit: 20,
	//how many threads to show on overboard catalog view
	overboardCatalogLimit: 100,
	//allow custom overboard (user pick what boards to show)
	allowCustomOverboard: true,
	//whether to show reverse links on overboard
	overboardReverseLinks: true,

	//how many hot threads show on homepage. 0 disabled
	hotThreadsLimit: 5,
	//how many replies the thread needs to be "hot"
	hotThreadsThreshold: 10,
	//max age of a hot thread, with a reducing score multiplier from 0-1 bias towards this date
	hotThreadsMaxAge: 2629800000,

	//default url format/links for archive, reverse image search, and ethereum links, %s will be replaced by the url
	archiveLinksURL: 'https://archive.today/?run=1&url=%s',
	reverseImageLinksURL: 'https://tineye.com/search?url=%s',
	ethereumLinksURL: 'https://etherscan.io/address/%s',

	//cache templates in memory. disable only if editing templates and doing dev work
	cacheTemplates: true,

	//max wait time in ms for obtaining locks for saving files
	lockWait: 3000,

	//optionally prune modlog entries older than x days
	pruneModlogs: 30,

	//option to prune ips on posts older than x days
	pruneIps: 0,

	//dont store raw ips at all (in jschan), not counting webservers or whatever else.
	dontStoreRawIps: false,

	//enable the webring (also copy configs/webring.json.example -> configs/webring.json and edit)
	enableWebring: false,

	//web3
	enableWeb3: false,
	ethereumNode: '',

	//extension for thumbnails
	thumbExtension: '.webp',
	//whether to animate gif thumbnails
	animatedGifThumbnails: false,
	//generate waveform thumbnails for audio
	audioThumbnails: true,
	/* if animatedGifThumbnails is true, use ffmpeg for better animated gif thumbnailing. Recommended.
		requires ffmpeg >=4.3.x. You can install from debian testing repo, or compile from source. */
	ffmpegGifThumbnails: true,

	//max thumb dimensions (square) in px
	thumbSize: 250,

	//0-100 percent through a video to try taking video thumbnails from, to remedy black first frames or fade-ins
	videoThumbPercentage: 5,

	/* extra mime types for files to be uploaded as attachments (no thumbnails) e.g. text files/archives
		NOTE: appropriate extensions will need to be added to nginx configuration, and uncommend the provided
		"other files" section which includes an example configuration for .txt files to match this default config.
		mime types and file extention does not always correspond exactly this cant be done automatically. */
	otherMimeTypes: [
		'text/plain',
		'application/pdf',
		'tegaki/replay',
	],

	//check the real mime type of uploaded files
	checkRealMimeTypes: false,
	//if checking real mime types, and the real type is unknown, allow it anyway
	allowMimeNoMatch: false,

	//default ban duration in ms if ban duration field is left blank (default value is 1 year)
	defaultBanDuration: 31536000000,

	//max number of quotes that will be linked in a post. 0 for unlimited (not recommended)
	quoteLimit: 25,

	//how many replies to show on index pages under each OP
	previewReplies: 5,
	stickyPreviewReplies: 5, //choose a different amount for sticky posts if desired

	/* the fraction of threadLimit beyond which early404 prunes posts
		e.g. 3 means after the first third, so if you had 6 pages,
		anything after page 2 with less than early404Replies gets pruned */
	early404Fraction: 3,
	//how many replies a thread needs to not get removed by early404
	early404Replies: 5,

	//how many of the most recent newsposts to show on the homepage
	maxRecentNews: 5,

	//time for an account to be considered inactive
	inactiveAccountTime: 7889400000,
	//action for inactive accounts: nothing, forfeit staff positions, delete account
	inactiveAccountAction: 0,

	//action for handling abandoned boards (no board owner): nothing, lock, lock + unlist, delete board
	abandonedBoardAction: 0,

	/* filter filenames on posts and banners
		false=no filtering
		true=allow only A-Za-z0-9_-
		regex=custom regex of what to replace e.g. /[^\w\s-]+/g */
	filterFileNames: false,
	/* replace spaces and multiple spaces with some character default _ for better filenames
		(spaces dont belong in filenames) */
	spaceFileNameReplacement: '_',

	uriDecodeFileNames: false,

	//options for code block highlighting in posts
	highlightOptions: {

		//subset of languages to allow.
		languageSubset: [
			'javascript',
			'typescript',
			'perl',
			'js',
			'c++',
			'c',
			'java',
			'kotlin',
			'php',
			'h',
			'csharp',
			'bash',
			'sh',
			'zsh',
			'python',
			'ruby',
			'css',
			'html',
			'json',
			'golang',
			'rust',
			'aa',
		],

		//threshold below which auto language is ignored
		threshold: 5

	},

	//uses names of css in gulp/res/css. if blank, all themes are enabled.
	themes: [],
	codeThemes: [],

	//global limits for board settings
	globalLimits:  {
		threadLimit: { //number of threads, 10 per page
			min: 10,
			max: 200
		},
		replyLimit: { //number of replies to a thread, thread is locked after this limit is reached
			min: 10,
			max: 1000
		},
		bumpLimit: { //number of replies to a thread before it wont get bumped anymore
			min: 10,
			max: 1000
		},
		postFiles: { //number of files in a post
			max: 5,
		},
		postFilesSize: {
			max: 10485760, //in bytes, 10MB default
			//width*height max number, 10000*10000 default, since square vs very wids vs very tall doesnt matter,
			//just the raw number of pixels e.g. 4K 3840x2160=77414400, so we give PLENTY of headroom
			imageResolution: 100000000,
			videoResolution: 77414400,
		},
		bannerFiles: {
			width: 300, //banner image max width in px
			height: 100, //banner image max height in px
			forceAspectRatio: true, //enforce 3:1 aspect ratio
			max: 10, //number of banners uploadable in one request
			total: 100, //max number of banners for a board
		},
		flagFiles: {
			max: 10, //number of banners uploadable in one request
			total: 100, //max number of flags for a board
		},
		assetFiles: {
			max: 10, //number of assets uploadable in one request
			total: 50, //max number of assets for a board
		},
		bannerFilesSize: { //in bytes, 10MB default
			max: 10485760
		},
		assetFilesSize: { //in bytes, 10MB default
			max: 10485760
		},
		flagFilesSize: { //in bytes, 1MB default
			max: 1048576
		},
		/* NOTE: xFilesSize in bytes the amount of total data in form submission including
			other fields like message, name, etc. Therefore a very long message would reduce the space left for files very slightly.
			To counteract this, consider increasing them beyond your desired max filesize by a small margin */
		fieldLength: { //max length of fields in some forms
			//post form
			name: 100,
			email: 100,
			subject: 100,
			postpassword: 100,
			message: 20000,
			//reports/post actions
			report_reason: 100,
			ban_reason: 100,
			log_message: 100,
			//board creation
			uri: 50,
			boardname: 50,
			description: 100,
		},
		multiInputs: {
			//limits on certain requests that have multi-select. set any to 0 for unlimited
			posts: { //post actions (report, delete, spoiler, ban, etc).
				anon: 20, //separate for non-staff to prevent performance issues or abuse e.g. user mass self-deleting or reporting posts with single captcha
				staff: 100, //but to still allow mass moderation for staff
			},
			//note: room for more limits here if they are needed for other checkbox forms in future
		},
		customCss: {
			enabled: true, //allow custom css by board owners
			max: 10000, //max number of characters to allow
			strict: true, //enables filters to block certain strings in custom css
			filters: [
				'@',
				'url(',
			]
		},
		customPages: {
			max: 10, //max number of custom pages per board
			maxLength: 10000, //max number of characters to allow per page
		},
		filters: {
			max: 50,
		}
	},

	//default frontend script settings
	frontendScriptDefault: {
		embedsEnabled: true, //show [embed] toggle next to embeddable video links
		heightUnlimit: false, //allow images taller than window height
		hideRecursive: true, //hide replies to hidden OPs
		crispImages: false, //use crisp/pixelated image rendering
		hideThumbnails: false, //hide thumbnails
		nonColorIds: false, //dont color ids
		alwaysShowSpoilers: false , //always reveal spoiler text
		hidePostStubs: false, //hide hidden posts completely, dont even show it minimized
		smoothScrolling: true, //smooth scrolling. annoying for chrome users on long threads
		hoverExpandsMedia: false, //hover over video/image/audio thumbs to show full file in a fixed popup
		hoverExpandsFollowCursor: true, //when hover is on, follow the mouse rather than putting in top-right corner
		threadWatcher: true, //make threadwatcher visible
		defaultVolume: 100, //default video/audio volume
		loop: true, //looping video/audio
		imageLoadingBars: true, //show loading bar for images, changes behaviour of right click save/open
		live: true, //update threads automatically
		scrollToPosts: false, //scroll to new posts
		localTime: true, //show local time for posts
		hour24Time: false, //show 24 hour time for posts
		relativeTime: true, //show relative time i.e. "5 minutes ago" for posts
		//note: relative time overrides other time options, they show on hover in the tooltip/"title" property
		notificationsEnabled: false, //show notifications for new posts in a thread
		notificationsYousOnly: true, //only show notifications for posts that quote you
		showYous: true, //show (You) next to name on your posts, and quotes linking to your post
		hideDeletedPostContent: false, //hide the message and files from marked deleted posts from websocket
		tegakiWidth: 500, //default canvas size for tegaki drawings
		tegakiHeight: 500,
	},

	//default board settings when a board is created
	boardDefaults: {
		language: 'en-GB',
		theme: 'yotsuba-b',
		codeTheme: 'ir-black',
		sfw: false, //safe for work board
		lockMode: 0, //board lock mode
		fileR9KMode: 0, //enfore unique files, 0=off, 1=per thread, 2=whole board
		messageR9KMode: 0, //enforce unique message, 0=off, 1=per thread, 2=whole board
		unlistedLocal: false, //board hidden from on-site board list and frontpage
		unlistedWebring: false, //board hidden from webring
		captchaMode: 0, //0=disabled, 1=for threads, 2=for all posts
		tphTrigger: 10, //numebr of threads in an hour before trigger action is activated
		pphTrigger: 50, //number of posts in an hour before ^
		//0=none, 1=captcha enable for threads, 2=captcha enable for all posts, 3=lock thread creation, 4=lock board
		tphTriggerAction: 1,
		pphTriggerAction: 2,
		//0=dont change, 1=unlock board/disable captcha, 2=lock thread creation/enable captcha for thread creation
		captchaReset: 0,
		lockReset: 0,
		forceAnon: false, //disable name and subject, only allow sage email
		sageOnlyEmail: false, //only allow sage email
		early404: true, //delete threads beyond the first 1/3 of pages with less than 5 replies
		ids: false, //show per thread poster ID based on ip
		geoFlags: false, //show geo flags, requires nginx setup
		customFlags: false, //show custom flags
		enableTegaki: true,
		enableWeb3: false,
		userPostDelete: true, //allow users to delete their posts
		userPostSpoiler: true, //allow user to spoiler their post files
		userPostUnlink: true, //alow user to unlink files fomr their post
		reverseImageSearchLinks: false, //show imgops or similar
		archiveLinks: false, //show external archive link
		threadLimit: 200,
		replyLimit: 1000,
		bumpLimit: 500,
		maxFiles: 5,
		forceReplyMessage: false,
		forceReplyFile: false,
		forceThreadMessage: false,
		forceThreadFile: false,
		forceThreadSubject: false,
		disableReplySubject: false,
		hideBanners: false,
		minThreadMessageLength: 0,
		minReplyMessageLength: 0,
		maxThreadMessageLength: 20000,
		maxReplyMessageLength: 20000,
		defaultName: 'Anon',
		customCSS: null,
		blockedCountries: [], //2 char ISO country codes to block
		disableAnonymizerFilePosting: false,
		deleteProtectionAge: 0, //prevent non-staff OP from deleting their thread if it older than this age in ms
		deleteProtectionCount: 0, //prevent non-staff op deleting their thread if it has more than this many replies
		announcement: {
			raw: null,
			markdown: null
		},
		allowedFileTypes: { //enable different types of files to be posted
			animatedImage: true,
			image: true,
			video: true,
			audio: true,
			other: false
		}
	},

	following: [],
	blacklist: [],
	logo: [],
	proxy: {
		enabled: false,
		address: ''
	}

};