├── log └── .keep ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── view_fragment.rb │ ├── application_record.rb │ ├── post │ │ └── extension │ │ │ └── sage.rb │ ├── reply.rb │ ├── topic.rb │ ├── board.rb │ ├── image.rb │ └── post.rb ├── assets │ ├── images │ │ ├── .keep │ │ └── loading.svg │ ├── stylesheets │ │ ├── pagination.sass │ │ ├── _init.sass │ │ ├── topics.sass │ │ ├── reply.sass │ │ ├── application.css │ │ ├── general.sass │ │ └── posts.sass │ └── javascripts │ │ ├── fetch_omitted.coffee │ │ ├── image_load.coffee │ │ ├── topics.coffee │ │ ├── post_handle.coffee │ │ ├── application.js │ │ ├── image_upload.coffee │ │ ├── reply.coffee │ │ └── jquery.nested_attributes.coffee ├── controllers │ ├── concerns │ │ └── .keep │ ├── images_controller.rb │ ├── application_controller.rb │ ├── topics_controller.rb │ └── posts_controller.rb ├── views │ ├── partials │ │ └── _global │ │ │ └── .gitkeep │ ├── topics │ │ ├── omitted.html.erb │ │ ├── show.html.erb │ │ └── index.html.erb │ ├── posts │ │ └── new.html.erb │ └── layouts │ │ └── application.html.erb ├── cells │ ├── topic │ │ ├── omitted.erb │ │ └── show.erb │ ├── post │ │ ├── images.erb │ │ ├── image_upload_control.erb │ │ ├── form.erb │ │ └── show.erb │ ├── view_fragment_cell.rb │ ├── post_cell.rb │ └── topic_cell.rb ├── presenters │ ├── post_presenter.rb │ ├── board_presenter.rb │ ├── image_presenter.rb │ └── topic_presenter.rb ├── jobs │ ├── topic_callback_job.rb │ └── prune_job.rb ├── lib │ ├── pruner │ │ ├── destroy_remover.rb │ │ └── sort_selector.rb │ ├── board_config_serializer.rb │ ├── omit.rb │ └── raw_options_analyzer.rb ├── helpers │ └── application_helper.rb ├── services │ └── post_form.rb └── uploaders │ └── image_uploader.rb ├── lib ├── assets │ └── .keep ├── tasks │ └── .keep └── auto_html │ └── filters │ ├── newline_to_br.rb │ └── quote.rb ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── .rspec ├── config ├── initializers │ ├── sucker_punch.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── custom_path_helper.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ ├── secret_token.rb │ ├── inflections.rb │ └── content_security_policy.rb ├── spring.rb ├── environment.rb ├── cable.yml ├── boot.rb ├── setting.rb ├── sitemap.rb ├── routes.rb ├── database.yml.example ├── application.rb ├── app_config.yml.example ├── puma.rb ├── locales │ └── en.yml └── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── bin ├── rake ├── bundle ├── rails ├── rspec ├── yarn ├── spring ├── update └── setup ├── spec ├── factories │ ├── topics.rb │ ├── posts.rb │ └── boards.rb ├── lib │ ├── pruner │ │ ├── sort_selector_spec.rb │ │ └── destroy_remover_spec.rb │ ├── auto_html │ │ └── filters │ │ │ ├── newline_to_br_spec.rb │ │ │ └── quote_spec.rb │ ├── omit_spec.rb │ ├── raw_options_analyzer_spec.rb │ └── board_config_serializer_spec.rb ├── models │ ├── post │ │ └── extension │ │ │ └── sage_spec.rb │ ├── board_spec.rb │ └── post_spec.rb ├── jobs │ └── prune_job_spec.rb ├── rails_helper.rb ├── services │ └── post_form_spec.rb └── spec_helper.rb ├── config.ru ├── db ├── migrate │ ├── 20160319061350_allow_images_post_id_to_be_null.rb │ ├── 20150419153856_add_bumped_at_to_topics.rb │ ├── 20150512134821_add_config_to_boards.rb │ ├── 20150530134256_add_content_html_to_posts.rb │ ├── 20170523114957_add_file_attachable_to_topics.rb │ ├── 20160405155046_add_lock_to_topics.rb │ ├── 20160320083647_add_remote_url_to_images.rb │ ├── 20150331150050_create_images.rb │ ├── 20160217154121_create_replies.rb │ ├── 20150208155537_create_topics.rb │ ├── 20160327113357_add_width_and_height_to_images.rb │ ├── 20150208150147_create_boards.rb │ ├── 20150516062535_disallow_null_on_foreign_keys.rb │ ├── 20150208160146_create_posts.rb │ ├── 20150421152549_create_view_fragments.rb │ ├── 20151006130931_add_options_to_posts.rb │ └── 20150715151825_add_pos_to_posts.rb ├── seeds │ └── development │ │ └── common.seeds.rb ├── seeds.rb └── schema.rb ├── Rakefile ├── README.md ├── .gitignore ├── MIT-LICENSE ├── Guardfile ├── Gemfile └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/partials/_global/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | —-warnings 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /config/initializers/sucker_punch.rb: -------------------------------------------------------------------------------- 1 | require 'sucker_punch/async_syntax' 2 | -------------------------------------------------------------------------------- /app/cells/topic/omitted.erb: -------------------------------------------------------------------------------- 1 | <%= render_cell_collection :post, :show, @posts %> 2 | -------------------------------------------------------------------------------- /app/views/topics/omitted.html.erb: -------------------------------------------------------------------------------- 1 | <%= render_cell :topic, :omitted, @topic %> 2 | -------------------------------------------------------------------------------- /app/models/view_fragment.rb: -------------------------------------------------------------------------------- 1 | class ViewFragment < ApplicationRecord 2 | belongs_to :board 3 | end 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/factories/topics.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :topic do 3 | title "Lorem Ipsum" 4 | board 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/post/extension/sage.rb: -------------------------------------------------------------------------------- 1 | # Skip topic bump 2 | module Post::Extension::Sage 3 | def bump_topic 4 | # NOOP 5 | end 6 | end 7 | 8 | -------------------------------------------------------------------------------- /app/views/posts/new.html.erb: -------------------------------------------------------------------------------- 1 |
#{line.chomp}\n" 5 | else 6 | new_text << line 7 | end 8 | end.join 9 | end 10 | -------------------------------------------------------------------------------- /app/presenters/board_presenter.rb: -------------------------------------------------------------------------------- 1 | class BoardPresenter < LulalalaPresenter::Base 2 | def representative_image_url 3 | model.config.dig(:seo, :image) 4 | end 5 | 6 | def og_image_tag 7 | if image = representative_image_url 8 | h.tag(:meta, property:'og:image', content:h.image_url(image)) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150331150050_create_images.rb: -------------------------------------------------------------------------------- 1 | class CreateImages < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :images, comment:'image' do |t| 4 | t.references :post, index: true, foreign_key: true 5 | t.string :image, comment:'filename' 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/images_controller.rb: -------------------------------------------------------------------------------- 1 | class ImagesController < ApplicationController 2 | def create 3 | images = params[:images].map do |file| 4 | image = Image.create(image: file) 5 | {id: image.id} 6 | end 7 | 8 | respond_to do |format| 9 | format.json { render json: {images: images} } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20160217154121_create_replies.rb: -------------------------------------------------------------------------------- 1 | class CreateReplies < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :replies, comment:'replying relations between posts' do |t| 4 | t.integer :ancestor_id, comment:'post that is being replied to' 5 | t.integer :descendant_id, comment:'post that is the reply' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | 4 | before_action :set_board 5 | def set_board 6 | @board = Board.find_by(seo_name:params[:board]) 7 | default_url_options[:board] = params[:board] 8 | end 9 | 10 | after_action :prepare_unobtrusive_flash 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150208155537_create_topics.rb: -------------------------------------------------------------------------------- 1 | class CreateTopics < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :topics, comment:'topic of discussion, also called thread' do |t| 4 | t.string :title, comment:'title' 5 | t.references :board, index: true 6 | 7 | t.timestamps null: false 8 | end 9 | add_foreign_key :topics, :boards 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20160327113357_add_width_and_height_to_images.rb: -------------------------------------------------------------------------------- 1 | class AddWidthAndHeightToImages < ActiveRecord::Migration[4.2] 2 | def change 3 | change_table :images do |t| 4 | t.integer :width, after: :image 5 | t.integer :height, after: :width 6 | t.integer :thumb_width, after: :height 7 | t.integer :thumb_height, after: :thumb_width 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/fetch_omitted.coffee: -------------------------------------------------------------------------------- 1 | $(document).on 'click', '[data-behavior=view-omitted]', (e)-> 2 | e.preventDefault() 3 | 4 | $target = $(e.target) 5 | 6 | url = "#{$target.attr('href')}/omitted" 7 | request = $.ajax 8 | url:url, 9 | method:'GET' 10 | dataType:'html' 11 | 12 | request.done (msg)-> 13 | $target.parent().replaceWith(msg) 14 | PostHandle.apply() 15 | -------------------------------------------------------------------------------- /config/setting.rb: -------------------------------------------------------------------------------- 1 | require 'settei/loaders/simple_loader' 2 | require 'settei/base' 3 | require 'settei/extensions/host_url' 4 | 5 | if defined? Rails 6 | rails_env = Rails.env 7 | end 8 | 9 | loader= Settei::Loaders::SimpleLoader.new( 10 | dir: File.join(File.dirname(__FILE__), "environments") 11 | ) 12 | Setting = Settei::Base.new(loader.load(rails_env).as_hash) 13 | Setting.extend Settei::Extensions::HostUrl 14 | -------------------------------------------------------------------------------- /config/sitemap.rb: -------------------------------------------------------------------------------- 1 | SitemapGenerator::Sitemap.default_host = Setting.host() 2 | 3 | SitemapGenerator::Sitemap.create include_root:false do 4 | Board.find_each do |board| 5 | add board_path(board), lastmod:board.topics.maximum(:bumped_at), changefreq:'always' 6 | end 7 | 8 | Topic.find_each do |topic| 9 | add topic_path(board:topic.board.seo_name, id:topic), lastmod:topic.bumped_at, changefreq:'daily' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/lib/pruner/sort_selector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | describe Pruner::SortSelector do 3 | let(:board){ FactoryBot.create(:board) } 4 | 5 | subject { described_class.new(board, 3)} 6 | 7 | before do 8 | 5.times do 9 | FactoryBot.create(:topic, board: board) 10 | end 11 | end 12 | 13 | it "returns id" do 14 | subject.perform.should == Topic.order(id: :asc).limit(2).pluck(:id) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | scope '/:board' do 3 | get '/' => 'topics#index', as: :board 4 | resources :topics, only:[:show] do 5 | member do 6 | get :omitted 7 | end 8 | end 9 | resources :posts, only:[:create, :new] 10 | resources :images, only:[:create] 11 | end 12 | 13 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /db/migrate/20150208150147_create_boards.rb: -------------------------------------------------------------------------------- 1 | class CreateBoards < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :boards, comment:'board' do |t| 4 | t.string :seo_name, null:false, comment:'represent name in URL. Must be URL valid characters.' 5 | t.string :name, null:false, comment:'display name on top of page' 6 | 7 | t.timestamps null:false 8 | end 9 | add_index :boards, :seo_name, unique:true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/lib/auto_html/filters/newline_to_br_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'auto_html' 3 | require 'auto_html/filters/newline_to_br' 4 | 5 | describe 'auto_html newline_to_br filter' do 6 | include AutoHtml 7 | it "convert > into quotes" do 8 | source = <<-SOURCE 9 | foo 10 | bar 11 | SOURCE 12 | 13 | result = auto_html(source){ newline_to_br } 14 | result.should == <<-OUTPUT 15 | foo
> One18 |
>two19 | three 20 | four 21 | OUTPUT 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20150208160146_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :posts, comment:'text content posted. New post or reply comments are all posts.' do |t| 4 | t.text :content, comment:'text content' 5 | t.string :author, comment:'author name' 6 | t.string :email, comment:'email' 7 | t.references :topic, index: true 8 | 9 | t.timestamps null: false 10 | end 11 | add_foreign_key :posts, :topics 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20150421152549_create_view_fragments.rb: -------------------------------------------------------------------------------- 1 | class CreateViewFragments < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :view_fragments, comment:'Custom HTML fragments to be displayed' do |t| 4 | t.references :board, foreign_key: true 5 | t.string :name, null:false, comment:'name for referencing' 6 | t.text :content, comment:'html fragment' 7 | 8 | t.timestamps null: false 9 | end 10 | add_index :view_fragments, [:board_id, :name], unique:true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/assets/images/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Imageboard Mei 2 | ============== 3 | 4 | Mei is a Futaba-styled imageboard, allowing people to discuss and upload pictures. My aim is to improve the user interface to my liking, instead of mimicking the traditional Futaba's look & feel. 5 | 6 | **Visit the [wiki](https://github.com/lulalala/mei/wiki) to see more information! I try to minimize number of commits so documentations are all over there!** 7 | 8 | ### Highlights 9 | 10 | * Multi-image upload 11 | * Image upload using url 12 | * Better user interface 13 | * Multi-board support 14 | * Extensible -------------------------------------------------------------------------------- /app/presenters/image_presenter.rb: -------------------------------------------------------------------------------- 1 | class ImagePresenter < LulalalaPresenter::Base 2 | def linked_display 3 | h.link_to( 4 | h.image_tag(h.image_url(model.image.thumb.url), width:model.thumb_width, height:model.thumb_height), 5 | h.image_url(model.image.url), 6 | class:'img' 7 | ).html_safe 8 | end 9 | 10 | # Linked image in a container which clears floats, 11 | # so images will occupy entire row. 12 | def cleared_linked_display 13 | h.content_tag(:div, linked_display, class:'images single clear').html_safe 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /db/migrate/20151006130931_add_options_to_posts.rb: -------------------------------------------------------------------------------- 1 | class AddOptionsToPosts < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :posts, :options, :string, after: :author, comment:'array of options like sage' 4 | add_column :posts, :options_raw, :string, after: :options, comment:'user input for email and options' 5 | execute 'UPDATE posts SET options_raw = email, email = NULL' 6 | end 7 | 8 | def down 9 | execute 'UPDATE posts SET email = options_raw' 10 | remove_column :posts, :options 11 | remove_column :posts, :options_raw 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/cells/post_cell.rb: -------------------------------------------------------------------------------- 1 | class PostCell < Cell::Rails 2 | helper ApplicationHelper 3 | 4 | def show(post) 5 | @post = post 6 | @board = @post.topic.board 7 | render 8 | end 9 | 10 | def form(topic) 11 | return if topic.locked? 12 | 13 | @post_form = PostForm.new.from_topic(topic) 14 | render 15 | end 16 | 17 | def image_upload_control(form_field) 18 | @form_field = form_field 19 | render 20 | end 21 | 22 | def images(post) 23 | @images = post.images.where(remote_url:nil) 24 | render if @images.size > 0 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/lib/omit.rb: -------------------------------------------------------------------------------- 1 | module Omit 2 | # @param total [Integer] total number of replies 3 | # @param n_recent_only [Integer] number of replies to show if omit occurs. 4 | # @param avoid_only_n_hidden [Integer] if after omit the omitted replies is less than or equal to this number, then do not omit the replies 5 | # @return [Boolean] whether omit should occur 6 | def self.omit?(total, n_recent_only, avoid_only_n_hidden = 0) 7 | if total - n_recent_only <= avoid_only_n_hidden 8 | return false 9 | elsif total > n_recent_only 10 | true 11 | else 12 | false 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/models/post/extension/sage_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Post::Extension::Sage do 4 | let(:board) { FactoryBot.create(:board) } 5 | let(:topic) { FactoryBot.create(:topic, board:board).reload } 6 | let(:post) { FactoryBot.create(:post, topic:topic) } 7 | 8 | it 'does not bump' do 9 | old_bumped_at = topic.bumped_at 10 | 11 | Timecop.freeze(5.seconds.from_now) do 12 | new_post = topic.posts.build 13 | new_post.assign_attributes(content:'foo', options_raw:'sage') 14 | new_post.save 15 | 16 | topic.reload.bumped_at.should == old_bumped_at 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/views/topics/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :head do 2 | title = [@topic.presenter.title, @board.name].join(" - ") 3 | %> 4 |
If you are the application owner check the logs for more information.
64 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |