├── .ruby-version ├── public ├── cache │ └── .gitkeep ├── apple-touch-icon.png ├── apple-touch-icon-144.png ├── .well-known │ └── security.txt ├── ads.txt ├── favicon.ico ├── touch-icon.png ├── touch-icon-144.png ├── touch-icon-192.png ├── 422.html ├── 504.html ├── 500.html ├── 502.html └── opensearch.xml ├── vendor ├── javascript │ └── .keep └── assets │ └── stylesheets │ └── .gitkeep ├── .rspec ├── hatchbox ├── transport ├── virtual_aliases ├── 20auto-upgrades ├── Caddyfile.post ├── opendkim.m4 ├── post-deploy ├── root-deploy.path ├── logrotate.conf ├── aliases ├── root-deploy.service ├── opendkim.service ├── bash_aliases └── Caddyfile ├── .git-blame-ignore-revs ├── .github ├── workflows │ └── check │ │ ├── master.key │ │ └── credentials.yml.enc ├── pull_request_template.md └── ISSUE_TEMPLATE │ └── bug-or-feature-request.md ├── Procfile.dev ├── app ├── helpers │ ├── suggestions_helper.rb │ ├── cabinet_helper.rb │ └── stories_helper.rb ├── views │ ├── tags │ │ ├── new.html.erb │ │ ├── edit.html.erb │ │ └── _multi_tag_tip.html.erb │ ├── categories │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── _multi_category_tip.html.erb │ │ └── _form.html.erb │ ├── users │ │ ├── _dev_flag_warning.html.erb │ │ ├── not_found.html.erb │ │ ├── list.html.erb │ │ └── _flag_warning.html.erb │ ├── home │ │ ├── _active.html.erb │ │ ├── _recent.html.erb │ │ ├── _newest_by_user.html.erb │ │ ├── _multi_tag.html.erb │ │ ├── _top.html.erb │ │ ├── _category.html.erb │ │ ├── _for_origin.html.erb │ │ ├── _single_tag.html.erb │ │ └── _for_domain.html.erb │ ├── email_message_mailer │ │ └── notify.text.erb │ ├── stories │ │ ├── _subnav.html.erb │ │ ├── _missing.html.erb │ │ └── new.html.erb │ ├── about │ │ ├── 404.html.erb │ │ ├── _subnav.html.erb │ │ └── privacy.html.erb │ ├── email_reply_mailer │ │ ├── mention.text.erb │ │ └── reply.text.erb │ ├── comments │ │ ├── _preview.html.erb │ │ ├── user_threads.html.erb │ │ ├── _postedreply.html.erb │ │ ├── _too_deep.html.erb │ │ └── index.rss.builder │ ├── ban_notification_mailer │ │ └── notify.text.erb │ ├── mod │ │ ├── index.html.erb │ │ ├── flagged_stories.html.erb │ │ ├── flagged_comments.html.erb │ │ ├── _subnav.html.erb │ │ └── reparents │ │ │ └── new.html.erb │ ├── saved │ │ └── _subnav.html.erb │ ├── helpers │ │ └── _link_post.html.erb │ ├── signup │ │ ├── invite.html.erb │ │ └── index.html.erb │ ├── password_reset_mailer │ │ └── password_reset_link.text.erb │ ├── invitation_mailer │ │ └── invitation.text.erb │ ├── mod_notes │ │ ├── index.html.erb │ │ └── _table.html.erb │ ├── login │ │ ├── forgot_password.html.erb │ │ ├── twofa.html.erb │ │ └── set_new_password.html.erb │ ├── hats │ │ ├── edit.html.erb │ │ └── doff.html.erb │ ├── origins │ │ └── for_domain.html.erb │ ├── suggestions │ │ └── new.html.erb │ ├── invitation_request_mailer │ │ └── invitation_request.text.erb │ ├── messages │ │ └── _subnav.html.erb │ ├── notifications │ │ └── _message.html.erb │ └── settings │ │ ├── twofa_verify.html.erb │ │ ├── mastodon_authentication.html.erb │ │ ├── twofa.html.erb │ │ └── twofa_enroll.html.erb ├── assets │ ├── images │ │ ├── select2.png │ │ └── merge.svg │ └── stylesheets │ │ ├── dark-system.css │ │ ├── light-system.css │ │ ├── system-high.css │ │ ├── system-normal.css │ │ └── system-system.css ├── controllers │ ├── cabinet_controller.rb │ ├── mod │ │ └── moderator_controller.rb │ ├── stats_controller.rb │ ├── jobs_mod_controller.rb │ ├── inbox_controller.rb │ ├── story_urls_controller.rb │ ├── concerns │ │ └── story_finder.rb │ ├── csp_controller.rb │ └── domains_ban_controller.rb ├── models │ ├── tag_filter.rb │ ├── suggested_tagging.rb │ ├── tagging.rb │ ├── suggested_title.rb │ ├── application_record.rb │ ├── saved_story.rb │ ├── concerns │ │ └── token.rb │ ├── story_repository.rb │ ├── hidden_story.rb │ ├── replying_comment.rb │ ├── inactive_user.rb │ ├── invitation.rb │ ├── short_id.rb │ ├── story_text.rb │ └── category.rb ├── mailboxes │ ├── backstop_mailbox.rb │ └── application_mailbox.rb ├── mailers │ ├── application_mailer.rb │ ├── password_reset_mailer.rb │ ├── invitation_mailer.rb │ ├── email_message_mailer.rb │ ├── invitation_request_mailer.rb │ └── ban_notification_mailer.rb └── jobs │ ├── restic_job.rb │ ├── application_job.rb │ └── stats_graphs_job.rb ├── docs ├── docker_ps.jpg ├── vim_result.jpg ├── server_output.jpg ├── credentials_error.jpg ├── credentials_error.png └── foreign_key_error.jpg ├── test └── run_specs_test.rb ├── bin ├── rake ├── importmap ├── jobs ├── rails ├── dev ├── faker ├── puma ├── racc ├── rotp ├── thor ├── byebug ├── ldiff ├── listen ├── oauth ├── pumactl ├── rackup ├── rspec ├── ascii85 ├── rubocop ├── htmldiff ├── nokogiri ├── pdf_text ├── pdf_object ├── ruby-parse ├── stackprof ├── standardrb ├── ruby-rewrite ├── commonmarker ├── pdf_callbacks ├── ruby-memory-profiler ├── stackprof-gprof2dot.py ├── stackprof-flamegraph.pl ├── safe_yaml └── update ├── script ├── expire_page_cache ├── lobsters-ingress └── lobsters-cron ├── spec ├── factories │ ├── hidden_story.rb │ ├── notification.rb │ ├── vote.rb │ ├── category.rb │ ├── origin.rb │ ├── invitation.rb │ ├── comment.rb │ ├── tag.rb │ ├── invitation_request.rb │ ├── hat.rb │ ├── hat_request.rb │ ├── message.rb │ ├── story.rb │ └── domain.rb ├── fixtures │ ├── story_pages │ │ └── titled.pdf │ └── inbound_emails │ │ ├── 1.eml │ │ ├── 3.eml │ │ └── 4.eml ├── support │ ├── factory_bot.rb │ ├── vcr.rb │ └── authentication_helper.rb ├── slow │ ├── README.md │ └── fake_data_spec.rb ├── mailers │ └── previews │ │ └── email_reply_preview.rb ├── models │ ├── comment_stat_spec.rb │ ├── story_text_spec.rb │ ├── hat_request_spec.rb │ ├── hat_spec.rb │ ├── mastodon_app_spec.rb │ └── invitation_spec.rb ├── requests │ ├── cabinet_spec.rb │ ├── signup_spec.rb │ ├── authenticatable_spec.rb │ └── mod │ │ └── reparents_spec.rb ├── extras │ ├── html_encoder_spec.rb │ └── routes_spec.rb ├── routing │ ├── origin_spec.rb │ └── domain_spec.rb ├── helpers │ ├── cabinet_helper_spec.rb │ └── interval_helper_spec.rb ├── controllers │ ├── messages_controller_spec.rb │ └── comments_controller_spec.rb ├── jobs │ └── notify_message_job_spec.rb └── features │ ├── doff_hat_spec.rb │ └── hats_spec.rb ├── config ├── initializers │ ├── mini_profiler.rb │ ├── console_methods.rb │ ├── query_log_tags.rb │ ├── mime_types.rb │ ├── journaldutrader.io.rb │ ├── cookies_serializer.rb │ ├── application_controller_renderer.rb │ ├── email.rb │ ├── backtrace_silencers.rb │ ├── session_store.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ ├── scenic.rb │ ├── 00_zeitwerk.rb │ └── filter_parameter_logging.rb ├── boot.rb ├── spring.rb ├── environment.rb ├── storage.yml ├── cable.yml ├── cache.yml ├── queue.yml ├── importmap.rb ├── credentials.yml.enc.sample └── locales │ └── en.yml ├── db └── migrate │ ├── 20140121063641_pushover_sound.rb │ ├── 20170607153224_add_stories_user_index.rb │ ├── 20140221164400_inactive_tags.rb │ ├── 20150224035617_hotness_mod_to_float.rb │ ├── 20170522144135_remove_comments_dragon.rb │ ├── 20140113153413_add_account_deletion.rb │ ├── 20170119192703_add_dragons.rb │ ├── 20180503150810_add_hat_to_message.rb │ ├── 20140825225915_add_tag_hotness_mod.rb │ ├── 20150106195555_add_story_unavailable.rb │ ├── 20230923195457_index_story_titles.rb │ ├── 20120701181319_invitation_memo.rb │ ├── 20140701153554_add_user_weblog_url.rb │ ├── 20140901013149_drop_weblogs.rb │ ├── 20200811022248_unique_category_names.rb │ ├── 20140106205200_remove_tag_filtered_by_default.rb │ ├── 20150313040930_add_user_avatar_pref.rb │ ├── 20171018034551_drop_default_tag_name.rb │ ├── 20220207033514_stories_expired_to_deleted.rb │ ├── 20120816203248_keystore_bigint.rb │ ├── 20150730215915_add_submitter_is_author.rb │ ├── 20180411131217_tag_moderation_log.rb │ ├── 20151015143959_moderations_from_group.rb │ ├── 20191010172004_add_index_for_moderations_associated_model.rb │ ├── 20200210155624_limit_new_user_tags.rb │ ├── 20240813153242_add_hidden_story_created_at.rb │ ├── 20150730225352_add_user_setting_show_preview.rb │ ├── 20140408160306_add_story_merging.rb │ ├── 20231023155620_add_user_setting_show_email.rb │ ├── 20120906183346_default_filtered_tags.rb │ ├── 20150127180326_add_story_twitter_id.rb │ ├── 20171025072230_modlog_hat_use.rb │ ├── 20200820170829_add_category_id_to_moderations.rb │ ├── 20240815145726_add_last_read_line_to_newest.rb │ ├── 20131228175805_add_comment_by_email_flag.rb │ ├── 20170413161450_add_indexes.rb │ ├── 20180131170318_update_replying_comments_to_version_2.rb │ ├── 20180131203555_fix_doffed_at.rb │ ├── 20180201024840_update_replying_comments_to_version_3.rb │ ├── 20180201041055_update_replying_comments_to_version_4.rb │ ├── 20180201172618_update_replying_comments_to_version_5.rb │ ├── 20180201184612_update_replying_comments_to_version_6.rb │ ├── 20180830114325_update_replying_comments_to_version_7.rb │ ├── 20190529133507_update_replying_comments_to_version_8.rb │ ├── 20240812221847_update_replying_comments_for_hiding.rb │ ├── 20151207180050_add_user_setting_post_threads.rb │ ├── 20180705143850_add_author_downvote_index.rb │ ├── 20120918152116_private_rss_feed.rb │ ├── 20121004153529_add_story_text.rb │ ├── 20200519000845_index_users_on_email.rb │ ├── 20180124143340_add_doffed_at_to_hat.rb │ ├── 20240608224626_add_indexes_on_foreign_keys.rb │ ├── 20140112192936_add_ban_reason.rb │ ├── 20140219183804_change_mailing_list_enabled.rb │ ├── 20250605141223_remove_keybase.rb │ ├── 20151015005101_add_suggested_taggings.rb │ ├── 20240207151341_add_story_mastodon_id.rb │ ├── 20120704025956_user_filter.rb │ ├── 20160704022756_change_story_columns.rb │ ├── 20250516035743_expand_ban_reason.rb │ ├── 20120712174445_add_comment_deleted.rb │ ├── 20151015011231_add_suggested_titles.rb │ ├── 20240315143804_coalesce_vote_reason.rb │ ├── 20250302102016_add_edit_last_read_newest.rb │ ├── 20191227232955_create_domains.rb │ ├── 20160406160019_add_hat_requests.rb │ ├── 20241016194903_add_index_banned_by_user_id_foreign_key_in_origins.rb │ ├── 20120706221602_more_indexes.rb │ ├── 20181202194809_add_follow_story_for_submitter.rb │ ├── 20170713195446_add_saved_stories.rb │ ├── 20250206172739_fix_prefers_contrast.rb │ ├── 20250630123615_moderation_enlarge_action.rb │ ├── 20160515162433_add_disabled_invites_to_users.rb │ ├── 20141114184921_add_hats.rb │ ├── 20120704004020_fix_up_messages.rb │ ├── 20170607170604_add_fulltext_indicies.rb │ ├── 20180501031849_add_used_at_to_invitation.rb │ ├── 20200607192351_story_index_for_comments.rb │ ├── 20121112165212_add_tag_media_types.rb │ ├── 20250210214901_fix_last_x_at_timestamps.rb │ ├── 20240822174547_add_short_id_hats.rb │ ├── 20120701160006_reply_notifications.rb │ ├── 20120919195401_private_tags.rb │ ├── 20130526164230_change_tables_to_utf8mb4.rb │ ├── 20230424222719_create_mastodon_apps.rb │ ├── 20120910172514_mention_notification_options.rb │ ├── 20250203223624_count_merged_stories.rb │ ├── 20120704013019_pm_notification_options.rb │ ├── 20240208024935_remove_twitter.rb │ ├── 20130622021035_add_user_list_id.rb │ ├── 20180130235553_add_read_notification_support.rb │ ├── 20220806200248_set_is_tracker_as_banned_to_domains.rb │ ├── 20150115172138_drop_extra_pushover_fields.rb │ ├── 20180711123439_create_mod_notes.rb │ ├── 20191029021735_fix_database_consistency_with_validators.rb │ ├── 20200210125425_add_domain_banning.rb │ ├── 20131018201413_add_invitation_requests.rb │ ├── 20220331165136_message_author_nullable.rb │ ├── 20200828015742_fix_comment_flags_default.rb │ ├── 20120902143549_add_moderation_log.rb │ ├── 20240717155412_hydrate_links.rb │ ├── 20170225201811_delete_old_settings.rb │ ├── 20240907171330_fix_links_in_invite_notes.rb │ ├── 20150211170052_move_hidden_votes_to_hidden_story.rb │ ├── 20240717144412_create_links.rb │ ├── 20180506045709_timestamp_votes.rb │ ├── 20250428193204_typeid_fix_token_name.rb │ ├── 20241104215430_add_comment_stats.rb │ ├── 20230823150648_materialize_confidence_order.rb │ ├── 20120701154453_create_invitations.rb │ ├── 20120703184957_integrated_story_hotness.rb │ ├── 20140101202252_add_karma_to_users.rb │ ├── 20120705145520_move_markdown_into_sql.rb │ ├── 20140109034338_move_comment_counts_to_story.rb │ ├── 20140804005415_add_weblogs.rb │ ├── 20241106160424_tidy_duplicate_origins.rb │ ├── 20250628172500_create_notifications.rb │ ├── 20240920115957_create_action_mailbox_tables.action_mailbox.rb │ ├── 20250124063340_add_last_comment_at_to_stories.rb │ ├── 20250424165830_add_typeid.rb │ ├── 20220128133226_fix_story_mod_notes.rb │ ├── 20240909220644_recalculate_comment_confidences.rb │ └── 20200812024241_create_story_texts.rb ├── config.ru ├── Makefile ├── Rakefile ├── extras ├── html_encoder.rb └── pushover.rb ├── .custom_cops.yml ├── SECURITY.md ├── docker-compose.yaml ├── lib ├── time_series.rb ├── tasks │ └── privacy_wipe.rake └── time_ago_in_words.rb └── Dockerfile.dev /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.3 2 | -------------------------------------------------------------------------------- /public/cache/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/javascript/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | touch-icon.png -------------------------------------------------------------------------------- /hatchbox/transport: -------------------------------------------------------------------------------- 1 | lobste.rs lobsters-ingress: 2 | -------------------------------------------------------------------------------- /public/apple-touch-icon-144.png: -------------------------------------------------------------------------------- 1 | touch-icon-144.png -------------------------------------------------------------------------------- /hatchbox/virtual_aliases: -------------------------------------------------------------------------------- 1 | @lobste.rs deploy@lobste.rs 2 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | c3f8625788245129903f31622bb452957223fc44 2 | -------------------------------------------------------------------------------- /.github/workflows/check/master.key: -------------------------------------------------------------------------------- 1 | 2649985e7283928a3b8a5c230e327bc6 -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bundle exec rails server 2 | job: bin/jobs 3 | -------------------------------------------------------------------------------- /public/.well-known/security.txt: -------------------------------------------------------------------------------- 1 | Contact: mailto:peter@push.cx 2 | -------------------------------------------------------------------------------- /app/helpers/suggestions_helper.rb: -------------------------------------------------------------------------------- 1 | module SuggestionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/tags/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: 'form', locals: { tag: @tag } %> 2 | -------------------------------------------------------------------------------- /docs/docker_ps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/docs/docker_ps.jpg -------------------------------------------------------------------------------- /docs/vim_result.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/docs/vim_result.jpg -------------------------------------------------------------------------------- /public/ads.txt: -------------------------------------------------------------------------------- 1 | We don't have ads. 2 | 3 | I created this file to stop getting 404s for it. 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/public/favicon.ico -------------------------------------------------------------------------------- /app/views/categories/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: 'form', locals: { category: @category } %> 2 | -------------------------------------------------------------------------------- /app/views/categories/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: 'form', locals: { category: @category } %> 2 | -------------------------------------------------------------------------------- /docs/server_output.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/docs/server_output.jpg -------------------------------------------------------------------------------- /public/touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/public/touch-icon.png -------------------------------------------------------------------------------- /public/touch-icon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/public/touch-icon-144.png -------------------------------------------------------------------------------- /public/touch-icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/public/touch-icon-192.png -------------------------------------------------------------------------------- /docs/credentials_error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/docs/credentials_error.jpg -------------------------------------------------------------------------------- /docs/credentials_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/docs/credentials_error.png -------------------------------------------------------------------------------- /docs/foreign_key_error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/docs/foreign_key_error.jpg -------------------------------------------------------------------------------- /hatchbox/20auto-upgrades: -------------------------------------------------------------------------------- 1 | APT::Periodic::Update-Package-Lists "1"; 2 | APT::Periodic::Unattended-Upgrade "1"; 3 | -------------------------------------------------------------------------------- /test/run_specs_test.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | require "rspec/core" 4 | RSpec::Core::Runner.run(["spec"]) 5 | -------------------------------------------------------------------------------- /app/assets/images/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/app/assets/images/select2.png -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /hatchbox/Caddyfile.post: -------------------------------------------------------------------------------- 1 | log disk-log { 2 | output file /home/deploy/lobsters/shared/log/caddy.log 3 | format json 4 | } 5 | -------------------------------------------------------------------------------- /script/expire_page_cache: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | find /home/deploy/lobsters/current/public/cache/ -type f -not -mmin 2 -delete 4 | -------------------------------------------------------------------------------- /spec/factories/hidden_story.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | FactoryBot.define do 4 | factory :hidden_story do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/notification.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | FactoryBot.define do 4 | factory :notification do 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/story_pages/titled.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/lobsters/master/spec/fixtures/story_pages/titled.pdf -------------------------------------------------------------------------------- /app/controllers/cabinet_controller.rb: -------------------------------------------------------------------------------- 1 | class CabinetController < ApplicationController 2 | def index 3 | render 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/tag_filter.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class TagFilter < ApplicationRecord 4 | belongs_to :tag 5 | belongs_to :user 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | RSpec.configure do |config| 4 | config.include FactoryBot::Syntax::Methods 5 | end 6 | -------------------------------------------------------------------------------- /hatchbox/opendkim.m4: -------------------------------------------------------------------------------- 1 | INPUT_MAIL_FILTER(`opendkim', 2 | `S=local:/var/spool/postfix/opendkim/opendkim.socket, F=, T=S:4m;R:4m;E:10m')dnl 3 | -------------------------------------------------------------------------------- /bin/jobs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/environment" 4 | require "solid_queue/cli" 5 | 6 | SolidQueue::Cli.start(ARGV) 7 | -------------------------------------------------------------------------------- /app/mailboxes/backstop_mailbox.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class BackstopMailbox < ApplicationMailbox 4 | def process 5 | bounced! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/vote.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | FactoryBot.define do 4 | factory :vote do 5 | association(:user) 6 | vote { 1 } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/slow/README.md: -------------------------------------------------------------------------------- 1 | Tests in this directory do not run as part of the regular test suite with 2 | `bundle exec rspec`. They are excluded by config.exclude 3 | -------------------------------------------------------------------------------- /app/controllers/mod/moderator_controller.rb: -------------------------------------------------------------------------------- 1 | class Mod::ModeratorController < ApplicationController 2 | before_action :require_logged_in_moderator 3 | end 4 | -------------------------------------------------------------------------------- /app/views/tags/edit.html.erb: -------------------------------------------------------------------------------- 1 |
<%= link_to "Filter By Tag #{@tag.tag}", tag_path(@tag) %>
2 | 3 | <%= render partial: 'form', locals: { tag: @tag } %> 4 | -------------------------------------------------------------------------------- /app/views/users/_dev_flag_warning.html.erb: -------------------------------------------------------------------------------- 1 |
3 | Tip: read stories across multiple tags with /t/tag1,tag2
4 |
5 | Stories with active discussions. 6 |
7 | -------------------------------------------------------------------------------- /config/initializers/mini_profiler.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | Rack::MiniProfiler.config.disable_caching = false 4 | Rack::MiniProfiler.config.position = "bottom-left" 5 | -------------------------------------------------------------------------------- /spec/factories/category.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | FactoryBot.define do 4 | factory :category do 5 | sequence(:category) { |n| "category-#{n}" } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/home/_recent.html.erb: -------------------------------------------------------------------------------- 1 | <%# locals: () -%> 2 |3 | The newest stories that have not yet reached the front page. 4 |
5 | 6 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 4 | 5 | require "bundler/setup" # Set up gems listed in the Gemfile. 6 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | %w[ 4 | .ruby-version 5 | .rbenv-vars 6 | tmp/restart.txt 7 | tmp/caching-dev.txt 8 | ].each { |path| Spring.watch(path) } 9 | -------------------------------------------------------------------------------- /spec/support/vcr.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | VCR.configure do |config| 4 | config.cassette_library_dir = "fixtures/vcr_cassettes" 5 | config.hook_into :webmock 6 | end 7 | -------------------------------------------------------------------------------- /app/models/suggested_tagging.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class SuggestedTagging < ApplicationRecord 4 | belongs_to :story 5 | belongs_to :tag 6 | belongs_to :user 7 | end 8 | -------------------------------------------------------------------------------- /app/models/tagging.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class Tagging < ApplicationRecord 4 | belongs_to :tag, inverse_of: :taggings 5 | belongs_to :story, inverse_of: :taggings 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20140121063641_pushover_sound.rb: -------------------------------------------------------------------------------- 1 | class PushoverSound < ActiveRecord::Migration 2 | def change 3 | add_column :users, :pushover_sound, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170607153224_add_stories_user_index.rb: -------------------------------------------------------------------------------- 1 | class AddStoriesUserIndex < ActiveRecord::Migration 2 | def change 3 | add_index "stories", ["user_id"] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/email_message_mailer/notify.text.erb: -------------------------------------------------------------------------------- 1 | <%= word_wrap(@message.plaintext_body, :line_width => 72).gsub(/\n/, "\n ") %> 2 | 3 | Reply at <%= Routes.message_url @message %> 4 | -------------------------------------------------------------------------------- /db/migrate/20140221164400_inactive_tags.rb: -------------------------------------------------------------------------------- 1 | class InactiveTags < ActiveRecord::Migration 2 | def change 3 | add_column :tags, :inactive, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150224035617_hotness_mod_to_float.rb: -------------------------------------------------------------------------------- 1 | class HotnessModToFloat < ActiveRecord::Migration 2 | def change 3 | change_column :tags, :hotness_mod, :float 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170522144135_remove_comments_dragon.rb: -------------------------------------------------------------------------------- 1 | class RemoveCommentsDragon < ActiveRecord::Migration 2 | def change 3 | remove_column :comments, :is_dragon 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /hatchbox/post-deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | find /home/deploy/lobsters/current/public/cache/ -type f -delete 4 | 5 | # see hatchbox/root-deploy 6 | touch /home/deploy/lobsters/current 7 | -------------------------------------------------------------------------------- /app/views/categories/_multi_category_tip.html.erb: -------------------------------------------------------------------------------- 1 | <%# locals: () -%> 2 |
3 | Tip: read stories across multiple categories with /categories/foo,bar
4 |
5 | Newest stories submitted by 6 | <%= link_to newest_by_user.username, user_path(newest_by_user) %>. 7 |
8 | -------------------------------------------------------------------------------- /db/migrate/20151207180050_add_user_setting_post_threads.rb: -------------------------------------------------------------------------------- 1 | class AddUserSettingPostThreads < ActiveRecord::Migration 2 | def change 3 | add_column "users", "show_submitted_story_threads", :boolean, 4 | default: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180705143850_add_author_downvote_index.rb: -------------------------------------------------------------------------------- 1 | class AddAuthorDownvoteIndex < ActiveRecord::Migration[5.2] 2 | def change 3 | add_index :comments, [:user_id, :story_id, :downvotes, :created_at], name: "downvote_index" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/mailboxes/application_mailbox.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class ApplicationMailbox < ActionMailbox::Base 4 | # routing /something/i => :somewhere 5 | routing(/^#{Rails.application.shortname}-/ => :inbox) 6 | 7 | routing all: :backstop 8 | end 9 | -------------------------------------------------------------------------------- /app/views/about/404.html.erb: -------------------------------------------------------------------------------- 1 |6 | The resource you requested was not found, or the story has been deleted. 7 |
8 |3 | If this user used to exist, they changed their username. 4 | Check the moderation log. 5 |
6 |The request you made was invalid.
9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/factories/comment.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | FactoryBot.define do 4 | factory :comment do 5 | association(:user) 6 | association(:story) 7 | sequence(:comment) { |n| "comment text #{n}" } 8 | created_at { Time.current } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/jobs_mod_controller.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Rails/ApplicationController 2 | class JobsModController < ActionController::Base 3 | include Authenticatable 4 | 5 | before_action :require_logged_in_moderator 6 | end 7 | # rubocop:enable Rails/ApplicationController 8 | -------------------------------------------------------------------------------- /app/jobs/restic_job.rb: -------------------------------------------------------------------------------- 1 | class ResticJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform(*args) 5 | shared = "/home/deploy/lobsters/shared" 6 | system("source #{shared}/etc/restic-env ; restic backup --no-scan #{shared}/etc #{shared}/log") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/email_reply_mailer/mention.text.erb: -------------------------------------------------------------------------------- 1 | <%= @comment.user.username %> mentioned you in a comment: 2 | 3 | <%= word_wrap(@comment.plaintext_comment, :line_width => 72).gsub(/\n/, "\n ") %> 4 | 5 | Reply to this email or visit <%= Routes.comment_short_id_url @comment %> 6 | -------------------------------------------------------------------------------- /spec/factories/tag.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | FactoryBot.define do 4 | factory :tag do 5 | association(:category) 6 | sequence(:tag) { |n| "tag-#{n}" } 7 | sequence(:description) { |n| "tag #{n}" } 8 | permit_by_new_users { true } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint test 2 | 3 | test: 4 | bundle exec rspec 5 | brakeman -q 6 | 7 | lint: 8 | bundle exec standardrb --fix-unsafely 9 | 10 | docker-serve: 11 | export RUBY_VERSION=`cat .ruby-version` && \ 12 | docker compose up --build 13 | 14 | all: lint test 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path("../config/application", __FILE__) 5 | 6 | Lobsters::Application.load_tasks 7 | -------------------------------------------------------------------------------- /app/views/home/_multi_tag.html.erb: -------------------------------------------------------------------------------- 1 | <%# locals: (tags:) -%> 2 |4 | You can edit the URL to change the time period like /5d or /2y; 5 | known units are: <%= IntervalHelper::TIME_INTERVALS.map { |k,v| "#{k}#{v[1..].downcase}" }.join(", ").html_safe %>. 6 |
7 | -------------------------------------------------------------------------------- /db/migrate/20151015005101_add_suggested_taggings.rb: -------------------------------------------------------------------------------- 1 | class AddSuggestedTaggings < ActiveRecord::Migration 2 | def change 3 | create_table :suggested_taggings do |t| 4 | t.integer :story_id 5 | t.integer :tag_id 6 | t.integer :user_id 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20240207151341_add_story_mastodon_id.rb: -------------------------------------------------------------------------------- 1 | class AddStoryMastodonId < ActiveRecord::Migration[7.1] 2 | def change 3 | change_table :stories, bulk: true do |t| 4 | t.column :mastodon_id, :string, limit: 25, null: true 5 | t.index [:mastodon_id] 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20120704025956_user_filter.rb: -------------------------------------------------------------------------------- 1 | class UserFilter < ActiveRecord::Migration 2 | def up 3 | create_table :tag_filters do |t| 4 | t.timestamps null: false 5 | t.integer :user_id 6 | t.integer :tag_id 7 | end 8 | end 9 | 10 | def down 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20160704022756_change_story_columns.rb: -------------------------------------------------------------------------------- 1 | class ChangeStoryColumns < ActiveRecord::Migration 2 | def change 3 | change_column :stories, :is_moderated, :boolean, default: false, null: false 4 | change_column :stories, :is_expired, :boolean, default: false, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20250516035743_expand_ban_reason.rb: -------------------------------------------------------------------------------- 1 | class ExpandBanReason < ActiveRecord::Migration[8.0] 2 | def up 3 | change_column :users, :banned_reason, :string, limit: 256 4 | end 5 | 6 | def down 7 | change_column :users, :banned_reason, :string, limit: 200 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/journaldutrader.io.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | if Rails.application.domain == "journaldutrader.io" || Rails.application.name == "Sitename" 4 | raise "Journal du Trader, take my email address out of /etc/aliases, I'm sick of getting bounces from your misconfigured mail server." 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/invitation_request.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | FactoryBot.define do 4 | factory :invitation_request do 5 | name { "pete smith" } 6 | sequence(:email) { |n| "user-#{n}@example.com" } 7 | memo { "https://memo.example" } 8 | ip_address { "1.1.1.1" } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | 6 | # https://stackoverflow.com/questions/50026344/composing-activerecord-scopes-with-selects 7 | scope :select_fix, -> { select(arel_table.project(Arel.star)) } 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20120712174445_add_comment_deleted.rb: -------------------------------------------------------------------------------- 1 | class AddCommentDeleted < ActiveRecord::Migration 2 | def up 3 | add_column "comments", "is_deleted", :boolean, default: false 4 | add_column "comments", "is_moderated", :boolean, default: false 5 | end 6 | 7 | def down 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20151015011231_add_suggested_titles.rb: -------------------------------------------------------------------------------- 1 | class AddSuggestedTitles < ActiveRecord::Migration 2 | def change 3 | create_table "suggested_titles" do |t| 4 | t.integer :story_id 5 | t.integer :user_id 6 | t.string :title, limit: 150, null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20240315143804_coalesce_vote_reason.rb: -------------------------------------------------------------------------------- 1 | class CoalesceVoteReason < ActiveRecord::Migration[7.1] 2 | def up 3 | Vote.where(reason: nil).update_all(reason: "") 4 | change_column :votes, :reason, :string, limit: 1, null: false, default: "" 5 | end 6 | 7 | def down 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20250302102016_add_edit_last_read_newest.rb: -------------------------------------------------------------------------------- 1 | class AddEditLastReadNewest < ActiveRecord::Migration[8.0] 2 | def change 3 | rename_column :users, :last_read_newest, :last_read_newest_story 4 | add_column :users, :last_read_newest_comment, :datetime, null: true, default: nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/hat.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | FactoryBot.define do 4 | factory :hat do 5 | association(:user) 6 | sequence(:hat) { |n| Faker::Lorem.sentence(word_count: 2)[..-2] } 7 | association(:granted_by_user, factory: :user) 8 | link { "http://example.com" } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/about/_subnav.html.erb: -------------------------------------------------------------------------------- 1 | <%# locals: () -%> 2 | <% content_for :subnav do %> 3 | <%= link_to_different_page 'About', about_path %> 4 | <%= link_to_different_page 'Chat', chat_path %> 5 | <%= link_to_different_page 'Stats', stats_path %> 6 | <%= link_to_different_page 'Hats', hats_path %> 7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/comments/_preview.html.erb: -------------------------------------------------------------------------------- 1 | <%# locals: (comment:, show_tree_lines: false) -%> 2 |3 | This is the Internet, you have no privacy. 4 |
5 | 6 |7 | (Unless you are under 13, in which case you are not allowed here.) 8 |
9 |3 | <%= @title %> (<%= @user_count %>) 4 |
5 | 6 |Also: jobs dashboard.
10 |2 | Now that you are a member you can invite people you think would be a good fit. 3 | This is available anytime from your settings page. 4 |
5 | 6 | <%= render :partial => "users/invitationform", 7 | :locals => { :return_home => true } %> 8 | 9 |or
10 | 11 |12 | <%= button_to "Skip", "/", :method => :get %> 13 |
14 | -------------------------------------------------------------------------------- /db/migrate/20130526164230_change_tables_to_utf8mb4.rb: -------------------------------------------------------------------------------- 1 | class ChangeTablesToUtf8mb4 < ActiveRecord::Migration 2 | def up 3 | return if !/Mysql/.match?(connection.adapter_name) 4 | 5 | ["comments", "invitations", "messages", "moderations", "stories", "users"].each do |t| 6 | execute("alter table #{t} convert to character set utf8mb4") 7 | end 8 | end 9 | 10 | def down 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20230424222719_create_mastodon_apps.rb: -------------------------------------------------------------------------------- 1 | class CreateMastodonApps < ActiveRecord::Migration[7.1] 2 | def change 3 | create_table :mastodon_apps do |t| 4 | t.string :name, null: false 5 | t.string :client_id, null: false 6 | t.string :client_secret, null: false 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :mastodon_apps, :name, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/inbox_controller.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class InboxController < ApplicationController 4 | before_action :require_logged_in_user 5 | 6 | def index 7 | if @user.unread_replies_count > 0 8 | redirect_to replies_unread_path 9 | elsif @user.unread_message_count > 0 10 | redirect_to messages_path 11 | else 12 | redirect_to replies_path 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/password_reset_mailer/password_reset_link.text.erb: -------------------------------------------------------------------------------- 1 | Hello <%= @user.email %>, 2 | 3 | Someone at <%= @ip %> requested to reset your account password 4 | on <%= Rails.application.name %>. If you submitted this request, visit the link below to 5 | set a new password. If not, you can disregard this e-mail. 6 | 7 | <%= set_new_password_url username: @user.username, password_reset_token: @user.password_reset_token %> 8 | -------------------------------------------------------------------------------- /db/migrate/20120910172514_mention_notification_options.rb: -------------------------------------------------------------------------------- 1 | class MentionNotificationOptions < ActiveRecord::Migration 2 | def up 3 | add_column :users, :email_mentions, :boolean, default: false 4 | add_column :users, :pushover_mentions, :boolean, default: false 5 | end 6 | 7 | def down 8 | remove_column :users, :pushover_mentions 9 | remove_column :users, :email_mentions 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/system-system.css: -------------------------------------------------------------------------------- 1 | /* system color scheme (system contrast) */ 2 | 3 | @import url("light-normal.css"); /* default */ 4 | @import url("light-high.css") (not (prefers-color-scheme: dark)) and (prefers-contrast: more); 5 | @import url("dark-normal.css") (prefers-color-scheme: dark) and (not (prefers-contrast: more)); 6 | @import url("dark-high.css") (prefers-color-scheme: dark) and (prefers-contrast: more); 7 | -------------------------------------------------------------------------------- /db/migrate/20250203223624_count_merged_stories.rb: -------------------------------------------------------------------------------- 1 | class CountMergedStories < ActiveRecord::Migration[8.0] 2 | def up 3 | add_column :stories, :stories_count, :integer, null: false, default: 0 4 | Story.update_all("stories_count = (select count(*) from stories as s_inner where s_inner.merged_story_id = stories.id)") 5 | end 6 | 7 | def down 8 | remove_column :stories, :stories_count 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20120704013019_pm_notification_options.rb: -------------------------------------------------------------------------------- 1 | class PmNotificationOptions < ActiveRecord::Migration 2 | def up 3 | change_table :messages do |t| 4 | t.change :has_been_read, :boolean, default: false 5 | end 6 | 7 | add_column :users, :email_messages, :boolean, default: true 8 | add_column :users, :pushover_messages, :boolean, default: true 9 | end 10 | 11 | def down 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | This repo exists to public the source to https://lobste.rs 6 | 7 | While we occasionally answer questions, we don't have a formal support 8 | policy for sites using the codebase. 9 | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please email peter@push.cx. I'll try to respond promptly. 14 | If you've found an issue, I'll help ensure you get credit for it. 15 | -------------------------------------------------------------------------------- /app/mailers/invitation_request_mailer.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class InvitationRequestMailer < ApplicationMailer 4 | def invitation_request(invitation_request) 5 | @invitation_request = invitation_request 6 | 7 | mail( 8 | to: invitation_request.email, 9 | subject: "[#{Rails.application.name}] Confirm your invitation " \ 10 | "request to " << Rails.application.name 11 | ) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/categories/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%# locals: (category:) -%> 2 | <%= form_with model: category, url: category.persisted? ? update_category_path : categories_path, method: :post do |f| %> 3 |2 | If you've forgotten your password, enter your e-mail address or username 3 | below and instructions will be e-mailed to you. 4 |
5 | 6 | <%= form_with url: reset_password_path do |f| %> 7 |9 | The puma worker timed out responding to your request. 10 |
11 |12 | If this persists, please come to the 13 | IRC channel 14 | (webchat). 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't 6 | # wish to see in your backtraces. 7 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 8 | 9 | # You can also remove all the silencers if you're trying to debug a problem 10 | # that might stem from framework code. 11 | # Rails.backtrace_cleaner.remove_silencers! 12 | -------------------------------------------------------------------------------- /db/migrate/20200828015742_fix_comment_flags_default.rb: -------------------------------------------------------------------------------- 1 | class FixCommentFlagsDefault < ActiveRecord::Migration[6.0] 2 | def change 3 | change_column :comments, :flags, :integer, default: 0, null: false, unsigned: true 4 | ActiveRecord::Base.connection.execute <<~SQL 5 | update comments 6 | set flags = coalesce(( 7 | select count(vote) 8 | from votes 9 | where comment_id = comments.id 10 | and vote = -1 11 | ), 0) 12 | SQL 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /hatchbox/opendkim.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OpenDKIM Milter 3 | Documentation=man:opendkim(8) man:opendkim.conf(5) man:opendkim-lua(3) man:opendkim-genkey(8) man:opendkim-genzone(8) man:opendkim-testkey(8) http://www.opendkim.org/docs.html 4 | After=network.target nss-lookup.target 5 | 6 | [Service] 7 | User=opendkim 8 | Group=opendkim 9 | Type=forking 10 | ExecStart=/usr/sbin/opendkim 11 | ExecReload=/bin/kill -USR1 $MAINPID 12 | Restart=on-failure 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /db/migrate/20120902143549_add_moderation_log.rb: -------------------------------------------------------------------------------- 1 | class AddModerationLog < ActiveRecord::Migration 2 | def up 3 | add_column "users", "is_moderator", :boolean, default: false 4 | 5 | create_table "moderations" do |t| 6 | t.timestamps null: false 7 | t.integer "moderator_user_id" 8 | t.integer "story_id" 9 | t.integer "comment_id" 10 | t.integer "user_id" 11 | t.text "action" 12 | t.text "reason" 13 | end 14 | end 15 | 16 | def down 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /hatchbox/bash_aliases: -------------------------------------------------------------------------------- 1 | export EDITOR=vim 2 | export HISTFILESIZE=$HISTSIZE 3 | export HISTIGNORE=[fb]g 4 | export HISTSIZE=100000 5 | export PAGER="less -R" 6 | 7 | alias be="bundle exec" 8 | alias c="curl" 9 | alias cl="clear" 10 | alias con="cd ~deploy/lobsters/current; bundle exec rails console" 11 | alias db="cd ~deploy/lobsters/current; bundle exec rails db -p" 12 | alias e="exa" 13 | alias et="exa -snew" 14 | alias fd="fdfind" 15 | alias gerp="grep" 16 | alias g="git" 17 | alias vi="vim" 18 | 19 | set -o vi 20 | -------------------------------------------------------------------------------- /spec/routing/origin_spec.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | require "rails_helper" 4 | 5 | describe "origin routing" do 6 | it "routes a single origin" do 7 | assert_recognizes({controller: "home", action: "for_origin", identifier: "github.com/alice"}, "/origins/github.com/alice") 8 | end 9 | 10 | it "redirects attempts to load an identifier from a domain url", type: :request do 11 | get "/domains/github.com/alice" 12 | expect(response).to redirect_to("/origins/github.com/alice") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # match this in caddy config for bypassing the file cache 6 | Lobsters::Application.config.session_store :cookie_store, 7 | key: "lobster_trap", 8 | expire_after: 1.month, 9 | httponly: true, 10 | same_site: :strict 11 | # :secure commented out because it's redundant with config.force_ssl 12 | # https://api.rubyonrails.org/v5.2.8.1/classes/ActionDispatch/SSL.html 13 | # secure: true 14 | -------------------------------------------------------------------------------- /db/migrate/20240717155412_hydrate_links.rb: -------------------------------------------------------------------------------- 1 | class HydrateLinks < ActiveRecord::Migration[7.1] 2 | def up 3 | Comment.where("comment like '%https://%' or comment like '%http://%'").find_each do |c| 4 | Link.recreate_from_comment! c 5 | end 6 | Story.where("id >= 40621").where("url is not null or (description like '%https://%' or description like '%http://%')").find_each do |s| 7 | Link.recreate_from_story! s 8 | end 9 | end 10 | 11 | def down 12 | Link.delete_all 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/helpers/stories_helper.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | module StoriesHelper 4 | def show_guidelines?(user) 5 | if !user 6 | return true 7 | end 8 | 9 | if user.stories_submitted_count <= 5 10 | return true 11 | end 12 | 13 | if Moderation.joins(:story) 14 | .where( 15 | "stories.user_id = ? AND moderations.created_at > ?", 16 | user.id, 17 | 5.days.ago 18 | ).exists? 19 | return true 20 | end 21 | 22 | false 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/views/login/twofa.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with url: twofa_login_url do |f| %> 2 |3 | Enter the current TOTP code from your TOTP application: 4 |
5 | 6 |The page you requested cannot be displayed.
9 |The site operators have been notified of this error. You're welcome to and encouraged to also report it in the IRC channel (also accessible via webchat).
10 | 11 | 12 | -------------------------------------------------------------------------------- /app/views/hats/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_with model: @hat, url: update_in_place_hat_path(@hat), method: :post do |f| %> 2 |2 | Domain <%= link_to @domain.domain, @domain %> has <%= "#{number_with_delimiter @origins.count} origin".pluralize(@origins.count) %> 3 |
4 | 5 |4 | Not a user yet? Sign up here. 5 |
6 | <% else %> 7 |8 | Signup is currently by invitation only to combat spam and increase 9 | accountability. If you know <%= link_to 'a current user', users_tree_path %> 10 | of the site, ask them for an invitation, or 11 | <% if Rails.application.allow_invitation_requests? %> 12 | request one publicly. 13 | <% else %> 14 | request one in chat. 15 | <% end %> 16 |
17 | <% end %> 18 || Mod → User/When | 5 |Note | 6 |
|---|---|
|
10 | <% if note.moderator != InactiveUser.inactive_user %>
11 | <%= note.moderator.username %>
12 | →
13 | <% end %>
14 | <%= link_to note.user.username, user_path(note.user) %>
15 | 16 | <%= raw note.created_at.strftime("%Y-%m-%d %H:%M %z") %> 17 | |
18 | <%= raw note.markeddown_note %> | 19 |
9 | nginx tried to pass your request to puma but failed. 10 | This probably means we're in the middle of a deploy. 11 | If this persists for more than 120 seconds, please come hassle us in the 12 | IRC channel 13 | (webchat). 14 |
15 | 16 |17 | It is safe to reload until the page loads and resubmit a POST request, 18 | you won't double-post a comment or anything. 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /spec/fixtures/inbound_emails/3.eml: -------------------------------------------------------------------------------- 1 | Date: Tue, 16 Jul 2013 10:37:11 -0500 2 | From: joshua stein3 | <% if !@user %> 4 | <% 5 | # Logged-out visitors don't get details so we don't make a convenient 6 | # public monument to content that was so bad or bizarre we removed it. 7 | %> 8 | Story was removed. 9 | <% elsif story.is_moderated? %> 10 | <% if moderation %> 11 | Story removed by moderator: <%= moderation.reason %> 12 | <% else %> 13 | Story removed by moderator but no log message was found. 14 | Please report this bug. 15 | <% end %> 16 | <% else %> 17 | Story was removed by submitter<% if @user %> <%= link_to story.user.username, user_path(story.user) %><% end %>. 18 | <% end %> 19 |
3 | <%= origin.identifier %> (part of <%= link_to origin.domain.domain, origin.domain %>): 4 | <%= "#{number_with_delimiter origin.stories_count} story".pluralize(origin.stories_count) %> from 5 | <%= "#{number_with_delimiter origin.n_submitters} submitter".pluralize(origin.n_submitters) %> 6 | 7 | <%= link_to 'Edit', edit_origin_path(origin) if @user&.is_moderator? %> 8 |
9 | 10 | 11 | 12 | <% if origin.banned? %> 13 |14 | Banned 15 | <%= how_long_ago_label(origin.banned_at) %> 16 | <% if origin.banned_by_user %> 17 | by <%= link_to origin.banned_by_user.try(:username), origin.banned_by_user %>: 18 | <%= origin.banned_reason %> 19 | <% end %> 20 |
21 | <% end %> 22 | 23 | -------------------------------------------------------------------------------- /db/migrate/20240920115957_create_action_mailbox_tables.action_mailbox.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from action_mailbox (originally 20180917164000) 2 | class CreateActionMailboxTables < ActiveRecord::Migration[6.0] 3 | def change 4 | create_table :action_mailbox_inbound_emails, id: primary_key_type do |t| 5 | t.integer :status, default: 0, null: false 6 | t.string :message_id, null: false 7 | t.string :message_checksum, null: false 8 | 9 | t.timestamps 10 | 11 | t.index [:message_id, :message_checksum], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true 12 | end 13 | end 14 | 15 | private 16 | 17 | def primary_key_type 18 | config = Rails.configuration.generators 19 | config.options[config.orm][:primary_key_type] || :primary_key 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /config/initializers/scenic.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | scenic_multidb_adapter = Class.new do 4 | def initialize 5 | @mysql = Scenic::Adapters::MySQL.new 6 | @sqlite = Scenic::Adapters::Sqlite.new 7 | end 8 | 9 | def method_missing(...) 10 | adapter.__send__(...) 11 | end 12 | 13 | def respond_to_missing? name, include_private 14 | adapter.respond_to? name 15 | end 16 | 17 | private 18 | 19 | def adapter 20 | case ActiveRecord::Base.connection 21 | when ActiveRecord::ConnectionAdapters::TrilogyAdapter 22 | @mysql 23 | when ActiveRecord::ConnectionAdapters::SQLite3Adapter 24 | @sqlite 25 | else 26 | raise "Unsupported adapter" 27 | end 28 | end 29 | end 30 | 31 | Scenic.configure do |config| 32 | config.database = scenic_multidb_adapter.new 33 | end 34 | -------------------------------------------------------------------------------- /spec/models/hat_spec.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | require "rails_helper" 4 | 5 | describe Hat do 6 | it "has a hat field" do 7 | hat = Hat.new(hat: nil) 8 | expect(hat).to_not be_valid 9 | end 10 | 11 | it "has a limit on the hat field" do 12 | hat = build(:hat, hat: "a" * 256) 13 | expect(hat).to_not be_valid 14 | end 15 | 16 | it "has a limit on the link field" do 17 | hat = build(:hat, link: "a" * 256) 18 | expect(hat).to_not be_valid 19 | end 20 | 21 | it "santizes email addresses" do 22 | hat = build(:hat, link: "foo@bar.com") 23 | expect(hat.sanitized_link).to eq("bar.com") 24 | end 25 | 26 | it "doesn't sanitize links that aren't email addressees" do 27 | hat = build(:hat, link: "google.com") 28 | expect(hat.sanitized_link).to eq("google.com") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/views/home/_single_tag.html.erb: -------------------------------------------------------------------------------- 1 | <%# locals: (tag:, related:) -%> 2 |
4 | Stories tagged as
5 | <%= tag_link(tag) %>
6 | <%= tag.description %>
7 |
8 | <% if tag.hotness_mod != 0.0 %>
9 | Hotness modifier <%= tag.hotness_mod %>
10 | (<%= tag.hotness_mod > 0 ? 'raises' : 'lowers' %> a story's rank).
11 | <% end %>
12 | <% if tag.privileged %>
13 | Only mods can use this tag.
14 | <% elsif !tag.permit_by_new_users %>
15 | Tag is not permitted on stories submitted by new users.
16 | <% end %>
17 |
21 | Most often also tagged with 22 | <% related.each do |tag| %> 23 | <%= tag_link(tag) %> 24 | <% end %> 25 |
26 |8 | To enable Two-Factor Authentication on your account using your new TOTP 9 | secret, enter the six-digit code from your TOTP application: 10 |
11 | 12 |24 | <%= f.submit "Verify and Enable" %> 25 |
26 | <% end %> 27 |3 | <%= domain.domain %>: 4 | <%= "#{number_with_delimiter domain.stories_count} story".pluralize(domain.stories_count) %> from 5 | <%= "#{number_with_delimiter domain.n_submitters} submitter".pluralize(domain.n_submitters) %> 6 | <% if domain.origins.count > 0 %> 7 | with <%= link_to "#{number_with_delimiter domain.origins.count} origins", domain_origins_path(domain) %> 8 | <% end %> 9 | 10 | <%= link_to 'Edit', edit_domain_path(domain) if @user&.is_moderator? %> 11 |
12 | 13 | <% if domain.banned? %> 14 |15 | Banned 16 | <%= how_long_ago_label(domain.banned_at) %> 17 | <% if domain.banned_by_user %> 18 | by <%= link_to domain.banned_by_user.try(:username), domain.banned_by_user %>: 19 | <%= domain.banned_reason %> 20 | <% end %> 21 |
22 | <% end %> 23 | 24 | -------------------------------------------------------------------------------- /bin/ruby-memory-profiler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'ruby-memory-profiler' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("memory_profiler", "ruby-memory-profiler") 28 | -------------------------------------------------------------------------------- /bin/stackprof-gprof2dot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'stackprof-gprof2dot.py' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("stackprof", "stackprof-gprof2dot.py") 28 | -------------------------------------------------------------------------------- /bin/stackprof-flamegraph.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'stackprof-flamegraph.pl' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("stackprof", "stackprof-flamegraph.pl") 28 | -------------------------------------------------------------------------------- /.github/workflows/check/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | zcS5WjQmGyGNQx3HkAKuLBq5IA8Fvn4WUBNxDIs5H+WldnNvnknuFny1BhBOmqwxPm0cepASiWKlAOyqCtSLXHyVVfxKn7QolWc7XakXh5jneSZR5lENRfdv518vSH81zIBMjeodsjvO7qu219yLjK5C5bXJNTfZDJl5P4LUYUgQadMRUnSIyLuWao5Plh4nN/iavcOcPKsxIXDIdbDtVX/yMJ00HtkK08GbUS6R5i7tMyi2Q6l+g4UuuCh3GJYtRbAxGKuNW0HvxXUZc72vALLxI+JHeNC2ldP4xb/r4HtjJWjVdDWXMyxZfGvh3TN4kUNWmV5UKqPXfuTSdCCCjxdID0Efh0+gaIejlFAsaXO9r3ydpGzFmeZCg85UpIKzgJ7TEDmdEPVUaYhKnkIhmPD3HKaaKinalBcSvyot5ig7csCJCA0DNtLT3XwBMlqCUebbWZNFWhVUqsynDXAd7QbChYKNL03rao8ahMvYHwf7mC+GJ0Uz/1bBT/NmpJP8RdcFan+xngD6hX6CGYTwho+gD01k1af24T0UF0MsEOi5ewZAREU3lZ6XSQbTUpTt822Zmq35pULi88MMa08xs7pBkvwIq0MDBJZufwm2APNzz/6Ura6aZlGNu6TNRVLqLCL7tBrzXgN9nPwNvrWTbgVQWSSaXWBTwOsd2aI/iRBAy96w7MLn27l+FfRj0LE7NvfC8Zq909sIATzrAJIxppXmig0ExnAv/6Pfy2Af5j/9QEQebcEhSN+xwcjt33uX0pMpgDupS/aQSpan1SHu621p0ZJjfPRvY5OUL5rGm3B17sC22YHkvSFw8lt847o1sUP310lKwwyVcfZp9FECN79rLZc=--nHG/SUA/akl2b6Hz--xYdN8CMc0C3RumXq093utg== -------------------------------------------------------------------------------- /config/credentials.yml.enc.sample: -------------------------------------------------------------------------------- 1 | # See README.md for instructions. 2 | 3 | # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. 4 | secret_key_base: "secret" 5 | 6 | diffbot: 7 | api_key: null 8 | 9 | github: 10 | client_id: null 11 | client_secret: null 12 | 13 | mastodon: 14 | # mastodon bot posting setup (not needed for account linking) 15 | # 1. visit instance to register your desired username 16 | # 2. run `rails credentials:edit` to enter instance name and username 17 | instance_name: "mastodon.social" 18 | bot_name: "lobsters" 19 | # 3. run Mastodon.get_bot_credentials! in the prod rails console for these three values 20 | client_id: null 21 | client_secret: null 22 | token: null 23 | # 4. create a list in the web ui, see the ID in the URL when you edit 24 | list_id: null 25 | 26 | pushover: 27 | api_token: null 28 | subscription_code: null 29 | -------------------------------------------------------------------------------- /spec/models/mastodon_app_spec.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | require "rails_helper" 4 | 5 | RSpec.describe MastodonApp, type: :model do 6 | describe "#sanitized_instance_name" do 7 | it "accepts an instance name" do 8 | expect(MastodonApp.sanitized_instance_name("example.com")).to eq("example.com") 9 | end 10 | 11 | it "accepts urls" do 12 | expect(MastodonApp.sanitized_instance_name("https://example.com")).to eq("example.com") 13 | expect(MastodonApp.sanitized_instance_name("https://example.com/")).to eq("example.com") 14 | expect(MastodonApp.sanitized_instance_name("https://example.com/@user")).to eq("example.com") 15 | end 16 | 17 | it "accepts a user id" do 18 | expect(MastodonApp.sanitized_instance_name("user@example.com")).to eq("example.com") 19 | expect(MastodonApp.sanitized_instance_name("@user@example.com")).to eq("example.com") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/helpers/interval_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | require "rails_helper" 4 | 5 | describe IntervalHelper do 6 | describe "#time_interval" do 7 | let(:placeholder) { IntervalHelper::PLACEHOLDER } 8 | 9 | it "replaces empty input with placeholder" do 10 | expect(helper.time_interval("")).to eq(placeholder) 11 | expect(helper.time_interval(nil)).to eq(placeholder) 12 | end 13 | 14 | # concerned with xss and sql injection 15 | it "replaces invalid input with placeholder" do 16 | expect(helper.time_interval("0h")).to eq(placeholder) 17 | expect(helper.time_interval("1'h")).to eq(placeholder) 18 | expect(helper.time_interval("1h'")).to eq(placeholder) 19 | expect(helper.time_interval("-1w")).to eq(placeholder) 20 | expect(helper.time_interval("2")).to eq(placeholder) 21 | expect(helper.time_interval("m")).to eq(placeholder) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /bin/safe_yaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby2.7 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'safe_yaml' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("safe_yaml", "safe_yaml") 30 | -------------------------------------------------------------------------------- /app/models/invitation.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class Invitation < ApplicationRecord 4 | belongs_to :user 5 | belongs_to :new_user, class_name: "User", inverse_of: nil, optional: true 6 | 7 | scope :used, -> { where.not(used_at: nil) } 8 | scope :unused, -> { where(used_at: nil) } 9 | 10 | include Token 11 | 12 | validate do 13 | unless /\A[^@ ]+@[^ @]+\.[^ @]+\z/.match?(email.to_s) 14 | errors.add(:email, "is not valid") 15 | end 16 | end 17 | 18 | validates :code, :email, length: {maximum: 255} 19 | validates :memo, length: {maximum: 375} 20 | 21 | before_validation :create_code, on: :create 22 | 23 | def create_code 24 | 10.times do 25 | self.code = Utils.random_str(15) 26 | return unless Invitation.exists?(code: code) 27 | end 28 | raise "too many hash collisions" 29 | end 30 | 31 | def send_email 32 | InvitationMailer.invitation(self).deliver_now 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/models/invitation_spec.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | require "rails_helper" 4 | 5 | describe Invitation do 6 | it "has a valid factory" do 7 | invitation = build(:invitation) 8 | expect(invitation).to be_valid 9 | end 10 | 11 | it "has a limit on the email field" do 12 | invitation = build(:invitation, email: "a" * 256 + "@b.b") 13 | invitation.valid? 14 | expect(invitation.errors[:email]).to eq(["is too long (maximum is 255 characters)"]) 15 | end 16 | 17 | it "creates a code before validation" do 18 | invitation = build(:invitation) 19 | invitation.code = "my code" 20 | invitation.valid? 21 | expect(invitation.code).to_not eq("my_code") 22 | end 23 | 24 | it "has a limit on the memo field" do 25 | invitation = build(:invitation, memo: "a" * 376) 26 | invitation.valid? 27 | expect(invitation.errors[:memo]).to eq(["is too long (maximum is 375 characters)"]) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/models/short_id.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | class ShortId 4 | attr_accessor :klass, :generation_attempts 5 | 6 | def initialize(klass) 7 | self.klass = klass 8 | self.generation_attempts = 0 9 | end 10 | 11 | def generate 12 | until (generated_id = candidate_id) && generated_id.valid? 13 | self.generation_attempts += 1 14 | raise "too many hash collisions" if generation_attempts == 10 15 | end 16 | generated_id.to_s 17 | end 18 | 19 | def candidate_id 20 | CandidateId.new(klass) 21 | end 22 | 23 | class CandidateId 24 | attr_accessor :klass, :id 25 | 26 | def initialize(klass) 27 | self.klass = klass 28 | self.id = generate_id 29 | end 30 | 31 | def to_s 32 | id 33 | end 34 | 35 | def generate_id 36 | Utils.random_str(6).downcase 37 | end 38 | 39 | def valid? 40 | !klass.exists?(short_id: id) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/fixtures/inbound_emails/4.eml: -------------------------------------------------------------------------------- 1 | Date: Tue, 16 Jul 2013 10:37:11 -0500 2 | From: joshua stein8 | Enter the name or URL of your Mastodon instance (for example, mastodon.social): 9 |
10 | 11 |17 | <%= f.submit "Continue" %> 18 |
19 | 20 |21 | We request the 22 | read:accounts scope to confirm your username, 23 | which includes basic profile info. 24 |
25 | 26 | <% end %> 27 |8 | <% if @user.has_2fa? %> 9 | To turn off two-factor authentication for your account, enter your 10 | current password: 11 | <% else %> 12 | To begin the two-factor authentication enrollment for your account, 13 | enter your current password: 14 | <% end %> 15 |
16 | 17 |23 | <% if @user.has_2fa? %> 24 | <%= f.submit "Disable Two-Factor Authentication" %> 25 | <% else %> 26 | <%= f.submit "Continue" %> 27 | <% end %> 28 |
29 | <% end %> 30 |5 | This will reparent <%= @reparent_user.username %> to you in the invite tree, create a mod note on the user with their old inviter, and put an entry in the moderation log with the given reason. 6 |
7 | 8 |9 | This should be used rarely because it makes it harder to investigate sockpuppets and voting rings. 10 | Currently the only known purpose is if this user was abused off-site by their inviter and we can't really do anything about it here except try to disassociate them. 11 | Write a good reason here, it's going to get attention by virtue of being rare. 12 |
13 | 14 |7 | Scan the QR code below or click on it to open in your TOTP application of choice: 10 |
11 | 12 |
17 | Or to add to a device manually enter the secret:
18 | Reveal Secret
20 | <%= raw @qr_secret %>
21 |
25 | Once you have finished registering with your TOTP application, proceed to 26 | the next screen to verify your current TOTP code and actually enable 27 | Two-Factor Authentication on your account. 28 |
29 | 30 |31 | <%= button_to "Verify and Enable", twofa_verify_url, :method => :get %> 32 |
33 |
7 | 8 | (We have a max reply depth for technical reasons. 9 | But also, at this depth, previous discussions have always gone off the rails 10 | in topic or tone.) 11 |