├── rust ├── .env ├── .gitignore ├── migrations │ ├── 2019-12-26-021440_create_users │ │ ├── down.sql │ │ └── up.sql │ ├── 2019-12-26-021456_create_posts │ │ ├── down.sql │ │ └── up.sql │ ├── 2019-12-26-021519_create_tags │ │ ├── down.sql │ │ └── up.sql │ ├── 2019-12-26-021511_create_creators │ │ ├── down.sql │ │ └── up.sql │ ├── 2019-12-26-021534_create_post_tags │ │ ├── down.sql │ │ └── up.sql │ ├── 2019-12-26-021527_create_creator_tags │ │ ├── down.sql │ │ └── up.sql │ ├── 2019-12-26-021504_create_post_categories │ │ ├── down.sql │ │ └── up.sql │ ├── 2020-01-06-035808_add_full_text_index │ │ ├── down.sql │ │ └── up.sql │ └── 00000000000000_diesel_initial_setup │ │ ├── down.sql │ │ └── up.sql ├── diesel.toml ├── src │ ├── models.rs │ ├── social_network.rs │ ├── lib.rs │ ├── categories.rs │ ├── schema.rs │ └── db.rs └── Cargo.toml ├── .gitignore ├── .ruby-version ├── crystal ├── .env ├── tasks │ ├── .keep │ ├── db │ │ └── seed │ │ │ ├── sample_data.cr │ │ │ └── required_data.cr │ ├── import_posts.cr │ └── import_creators.cr ├── db │ └── migrations │ │ └── .keep ├── spec │ ├── flows │ │ ├── .keep │ │ ├── reset_password_spec_disabled.cr │ │ └── authentication_spec_disabled.cr │ ├── setup │ │ ├── .keep │ │ ├── clean_database.cr │ │ ├── reset_emails.cr │ │ ├── setup_database.cr │ │ ├── start_app_server.cr │ │ └── configure_lucky_flow.cr │ ├── support │ │ ├── .keep │ │ ├── factories │ │ │ ├── .keep │ │ │ ├── user_box.cr │ │ │ ├── post_box.cr │ │ │ └── creator_box.cr │ │ ├── flows │ │ │ ├── base_flow.cr │ │ │ ├── authentication_flow.cr │ │ │ └── reset_password_flow.cr │ │ ├── app_client.cr │ │ ├── context_helper.cr │ │ └── fixtures │ │ │ └── feedbin-entry.json │ ├── creator_spec.cr │ ├── requests │ │ └── api │ │ │ ├── me │ │ │ └── show_spec.cr │ │ │ ├── sign_ins │ │ │ └── create_spec.cr │ │ │ └── sign_ups │ │ │ └── create_spec.cr │ ├── avatar_spec.cr │ ├── sanitise_spec.cr │ ├── spec_helper.cr │ ├── tag_name_spec.cr │ ├── syndication │ │ └── rss_spec.cr │ ├── feedbin_spec.cr │ └── components │ │ └── posts │ │ └── summary_spec.cr ├── src │ ├── components │ │ ├── .keep │ │ ├── base_component.cr │ │ ├── shared │ │ │ ├── flash_messages.cr │ │ │ ├── field_errors.cr │ │ │ ├── layout_head.cr │ │ │ ├── header.cr │ │ │ └── field.cr │ │ ├── search │ │ │ └── form.cr │ │ └── posts │ │ │ ├── action_bar.cr │ │ │ ├── summary.cr │ │ │ ├── pagination.cr │ │ │ └── form.cr │ ├── css │ │ ├── mixins │ │ │ └── .keep │ │ ├── components │ │ │ └── .keep │ │ ├── variables │ │ │ └── .keep │ │ ├── admin.scss │ │ └── fonts.scss │ ├── emails │ │ ├── .keep │ │ ├── templates │ │ │ └── password_reset_request_email │ │ │ │ ├── text.ecr │ │ │ │ └── html.ecr │ │ ├── base_email.cr │ │ └── password_reset_request_email.cr │ ├── operations │ │ ├── .keep │ │ ├── mixins │ │ │ ├── .keep │ │ │ ├── user_from_email.cr │ │ │ └── password_validations.cr │ │ ├── save_post_tag.cr │ │ ├── save_creator_tag.cr │ │ ├── import_post.cr │ │ ├── save_creator.cr │ │ ├── save_post_category.cr │ │ ├── save_tag.cr │ │ ├── reset_password.cr │ │ ├── sign_up_user.cr │ │ ├── request_password_reset.cr │ │ ├── save_post.cr │ │ └── sign_in_user.cr │ ├── queries │ │ ├── .keep │ │ ├── mixins │ │ │ └── .keep │ │ ├── user_query.cr │ │ ├── creator_query.cr │ │ ├── post_tag_query.cr │ │ ├── creator_tag_query.cr │ │ ├── post_category_query.cr │ │ ├── category_query.cr │ │ ├── tag_query.cr │ │ └── post_query.cr │ ├── actions │ │ ├── mixins │ │ │ ├── .keep │ │ │ ├── password_resets │ │ │ │ ├── find_user.cr │ │ │ │ ├── token_from_session.cr │ │ │ │ ├── base.cr │ │ │ │ └── require_token.cr │ │ │ ├── auth │ │ │ │ ├── allow_guests.cr │ │ │ │ ├── test_backdoor.cr │ │ │ │ ├── redirect_signed_in_users.cr │ │ │ │ └── require_sign_in.cr │ │ │ ├── categories │ │ │ │ └── find_category.cr │ │ │ └── api │ │ │ │ └── auth │ │ │ │ ├── skip_require_auth_token.cr │ │ │ │ ├── helpers.cr │ │ │ │ └── require_auth_token.cr │ │ ├── me │ │ │ └── show.cr │ │ ├── api │ │ │ ├── me │ │ │ │ └── show.cr │ │ │ ├── sign_ups │ │ │ │ └── create.cr │ │ │ └── sign_ins │ │ │ │ └── create.cr │ │ ├── posts │ │ │ ├── new.cr │ │ │ ├── show.cr │ │ │ ├── edit.cr │ │ │ ├── index.cr │ │ │ ├── create.cr │ │ │ └── update.cr │ │ ├── sign_ins │ │ │ ├── new.cr │ │ │ ├── delete.cr │ │ │ └── create.cr │ │ ├── submit │ │ │ └── show.cr │ │ ├── tags │ │ │ └── index.cr │ │ ├── about │ │ │ └── show.cr │ │ ├── favicon │ │ │ └── show.cr │ │ ├── password_reset_requests │ │ │ ├── new.cr │ │ │ └── create.cr │ │ ├── robots │ │ │ └── show.cr │ │ ├── password_resets │ │ │ ├── edit.cr │ │ │ ├── create.cr │ │ │ └── new.cr │ │ ├── sign_ups │ │ │ ├── new.cr │ │ │ └── create.cr │ │ ├── rust_blogs │ │ │ └── index.cr │ │ ├── creators │ │ │ └── index.cr │ │ ├── json_feed │ │ │ └── show.cr │ │ ├── search │ │ │ └── show.cr │ │ ├── api_action.cr │ │ ├── home │ │ │ └── index.cr │ │ ├── categories │ │ │ └── show.cr │ │ ├── rss_feed │ │ │ └── show.cr │ │ ├── sitemap │ │ │ └── show.cr │ │ ├── errors │ │ │ └── show.cr │ │ └── feedbin │ │ │ └── show.cr │ ├── models │ │ ├── mixins │ │ │ └── .keep │ │ ├── base_model.cr │ │ ├── search_result.cr │ │ ├── creator_tag.cr │ │ ├── post_tag.cr │ │ ├── tag_name.cr │ │ ├── user.cr │ │ ├── search_results.cr │ │ ├── avatar.cr │ │ ├── post_category.cr │ │ ├── tag.cr │ │ ├── page.cr │ │ ├── post.cr │ │ ├── creator.cr │ │ ├── category.cr │ │ └── user_token.cr │ ├── serializers │ │ ├── .keep │ │ ├── user_serializer.cr │ │ ├── base_serializer.cr │ │ ├── errors │ │ │ └── show_serializer.cr │ │ ├── error_serializer.cr │ │ └── json_feed │ │ │ ├── post_serializer.cr │ │ │ └── show_serializer.cr │ ├── read_rust.cr │ ├── app_database.cr │ ├── pages │ │ ├── admin_layout.cr │ │ ├── mixins │ │ │ └── page │ │ │ │ └── render_markdown.cr │ │ ├── posts │ │ │ ├── show_page.cr │ │ │ ├── new_page.cr │ │ │ ├── edit_page.cr │ │ │ └── index_page.cr │ │ ├── tags │ │ │ ├── index_page.cr │ │ │ └── show_page.cr │ │ ├── password_reset_requests │ │ │ └── new_page.cr │ │ ├── search │ │ │ └── show_page.cr │ │ ├── auth_layout.cr │ │ ├── creators │ │ │ └── index_page.cr │ │ ├── password_resets │ │ │ └── new_page.cr │ │ ├── me │ │ │ └── show_page.cr │ │ ├── sign_ups │ │ │ └── new_page.cr │ │ ├── sign_ins │ │ │ └── new_page.cr │ │ ├── categories │ │ │ └── show_page.cr │ │ ├── submit │ │ │ └── show_page.cr │ │ └── main_layout.cr │ ├── shards.cr │ ├── start_server.cr │ ├── utils │ │ └── sanitise.cr │ ├── app_server.cr │ ├── app.cr │ ├── feedbin │ │ └── client.cr │ ├── js │ │ └── admin.js │ └── syndication │ │ └── rss.cr ├── .crystal-version ├── public │ ├── assets │ │ ├── images │ │ │ ├── .keep │ │ │ ├── u │ │ │ │ ├── Bat.png │ │ │ │ ├── Geal.png │ │ │ │ ├── djc.jpg │ │ │ │ ├── mre.png │ │ │ │ ├── wezm.jpg │ │ │ │ ├── zcat.jpg │ │ │ │ ├── Kornel.jpg │ │ │ │ ├── Plume.png │ │ │ │ ├── aeplay.jpg │ │ │ │ ├── antoyo.png │ │ │ │ ├── cyplo.jpg │ │ │ │ ├── drahnr.jpg │ │ │ │ ├── icefox.png │ │ │ │ ├── jonhoo.jpg │ │ │ │ ├── koute.jpg │ │ │ │ ├── nvzqz.jpg │ │ │ │ ├── sgrif.jpg │ │ │ │ ├── skade.png │ │ │ │ ├── tomaka.jpg │ │ │ │ ├── yrashk.jpg │ │ │ │ ├── Aardwolf.png │ │ │ │ ├── Aaron1011.png │ │ │ │ ├── MaikKlein.png │ │ │ │ ├── ag_dubs.jpg │ │ │ │ ├── arcterus.png │ │ │ │ ├── ivanceras.png │ │ │ │ ├── jackpot51.jpg │ │ │ │ ├── japaric.png │ │ │ │ ├── jplatte.png │ │ │ │ ├── kbknapp.jpg │ │ │ │ ├── killercup.jpg │ │ │ │ ├── mopemope.jpg │ │ │ │ ├── passcod.jpg │ │ │ │ ├── phansch.jpg │ │ │ │ ├── phil-opp.png │ │ │ │ ├── r-iendo.png │ │ │ │ ├── raphlinus.jpg │ │ │ │ ├── retep998.jpg │ │ │ │ ├── sebcrozet.jpg │ │ │ │ ├── stjepang.jpg │ │ │ │ ├── thumb │ │ │ │ │ ├── Bat.jpg │ │ │ │ │ ├── djc.jpg │ │ │ │ │ ├── mre.jpg │ │ │ │ │ ├── Geal.jpg │ │ │ │ │ ├── Plume.jpg │ │ │ │ │ ├── cyplo.jpg │ │ │ │ │ ├── koute.jpg │ │ │ │ │ ├── nvzqz.jpg │ │ │ │ │ ├── sgrif.jpg │ │ │ │ │ ├── skade.jpg │ │ │ │ │ ├── wezm.jpg │ │ │ │ │ ├── zcat.jpg │ │ │ │ │ ├── Aardwolf.jpg │ │ │ │ │ ├── Kornel.jpg │ │ │ │ │ ├── aeplay.jpg │ │ │ │ │ ├── ag_dubs.jpg │ │ │ │ │ ├── antoyo.jpg │ │ │ │ │ ├── arcterus.jpg │ │ │ │ │ ├── drahnr.jpg │ │ │ │ │ ├── icefox.jpg │ │ │ │ │ ├── japaric.jpg │ │ │ │ │ ├── jonhoo.jpg │ │ │ │ │ ├── jplatte.jpg │ │ │ │ │ ├── kbknapp.jpg │ │ │ │ │ ├── mopemope.jpg │ │ │ │ │ ├── passcod.jpg │ │ │ │ │ ├── phansch.jpg │ │ │ │ │ ├── phil-opp.jpg │ │ │ │ │ ├── r-iendo.jpg │ │ │ │ │ ├── retep998.jpg │ │ │ │ │ ├── stjepang.jpg │ │ │ │ │ ├── tomaka.jpg │ │ │ │ │ ├── xanewok.jpg │ │ │ │ │ ├── yrashk.jpg │ │ │ │ │ ├── Aaron1011.jpg │ │ │ │ │ ├── MaikKlein.jpg │ │ │ │ │ ├── XAMPPRocky.jpg │ │ │ │ │ ├── dessalines.jpg │ │ │ │ │ ├── fulmicoton.jpg │ │ │ │ │ ├── ivanceras.jpg │ │ │ │ │ ├── jackpot51.jpg │ │ │ │ │ ├── killercup.jpg │ │ │ │ │ ├── raphlinus.jpg │ │ │ │ │ ├── sebcrozet.jpg │ │ │ │ │ ├── yupferris.jpg │ │ │ │ │ ├── DenisKolodin.jpg │ │ │ │ │ ├── chriskrycho.jpg │ │ │ │ │ ├── jackyalcine.jpg │ │ │ │ │ ├── measlytwerp.jpg │ │ │ │ │ ├── GuillaumeGomez.jpg │ │ │ │ │ ├── QuietMisdreavus.jpg │ │ │ │ │ └── waywardmonkeys.jpg │ │ │ │ ├── xanewok.jpg │ │ │ │ ├── yupferris.jpg │ │ │ │ ├── XAMPPRocky.png │ │ │ │ ├── chriskrycho.jpg │ │ │ │ ├── dessalines.jpg │ │ │ │ ├── fulmicoton.jpg │ │ │ │ ├── jackyalcine.jpg │ │ │ │ ├── measlytwerp.png │ │ │ │ ├── DenisKolodin.jpg │ │ │ │ ├── GuillaumeGomez.png │ │ │ │ ├── waywardmonkeys.png │ │ │ │ ├── QuietMisdreavus.png │ │ │ │ └── Amethyst.svg │ │ │ ├── jsonfeed.png │ │ │ ├── facebook.svg │ │ │ ├── rss.svg │ │ │ ├── twitter.svg │ │ │ ├── github.svg │ │ │ ├── mastodon.svg │ │ │ ├── heart.svg │ │ │ └── logo.svg │ │ └── fonts │ │ │ ├── nunitosans-bold-webfont.woff │ │ │ ├── nunitosans-bold-webfont.woff2 │ │ │ ├── nunitosans-italic-webfont.woff │ │ │ ├── nunitosans-italic-webfont.woff2 │ │ │ ├── nunitosans-regular-webfont.woff │ │ │ ├── nunitosans-regular-webfont.woff2 │ │ │ ├── nunitosans-semibold-webfont.woff │ │ │ └── nunitosans-semibold-webfont.woff2 │ └── favicon.ico ├── config │ ├── watch.yml │ ├── error_handler.cr │ ├── html_page.cr │ ├── env.cr │ ├── colors.cr │ ├── authentic.cr │ ├── route_helper.cr │ ├── feedbin.cr │ ├── application.cr │ ├── cookies.cr │ ├── email.cr │ ├── read_rust.cr │ ├── database.cr │ ├── log.cr │ └── server.cr ├── Procfile ├── Procfile.dev ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── README.md ├── bs-config.js ├── package.json ├── tasks.cr ├── shard.yml └── script │ └── setup ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── support_rust.md │ └── missing_post.md └── contributing.md ├── .env.sample ├── LICENSE └── .cirrus.yml /rust/.env: -------------------------------------------------------------------------------- 1 | ../.env -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.3 2 | -------------------------------------------------------------------------------- /crystal/.env: -------------------------------------------------------------------------------- 1 | ../.env -------------------------------------------------------------------------------- /crystal/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/db/migrations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/spec/flows/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/spec/setup/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/spec/support/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/components/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/css/mixins/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/emails/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/operations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/queries/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/css/components/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/css/variables/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/models/mixins/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/queries/mixins/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/serializers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: wezm 2 | -------------------------------------------------------------------------------- /crystal/.crystal-version: -------------------------------------------------------------------------------- 1 | 1.6.2 2 | -------------------------------------------------------------------------------- /crystal/public/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/spec/support/factories/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/operations/mixins/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/read_rust.cr: -------------------------------------------------------------------------------- 1 | require "./start_server" 2 | -------------------------------------------------------------------------------- /crystal/config/watch.yml: -------------------------------------------------------------------------------- 1 | host: 127.0.0.1 2 | port: 5000 3 | -------------------------------------------------------------------------------- /rust/.gitignore: -------------------------------------------------------------------------------- 1 | /public 2 | .idea 3 | /target/ 4 | **/*.rs.bk 5 | -------------------------------------------------------------------------------- /crystal/Procfile: -------------------------------------------------------------------------------- 1 | web: ./bin/read_rust 2 | release: lucky db.migrate 3 | -------------------------------------------------------------------------------- /crystal/src/app_database.cr: -------------------------------------------------------------------------------- 1 | class AppDatabase < Avram::Database 2 | end 3 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021440_create_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users; 2 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021456_create_posts/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE posts; 2 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021519_create_tags/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE tags; 2 | -------------------------------------------------------------------------------- /crystal/Procfile.dev: -------------------------------------------------------------------------------- 1 | web: lucky watch --reload-browser 2 | assets: yarn watch 3 | -------------------------------------------------------------------------------- /crystal/src/queries/user_query.cr: -------------------------------------------------------------------------------- 1 | class UserQuery < User::BaseQuery 2 | end 3 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021511_create_creators/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE creators; 2 | -------------------------------------------------------------------------------- /crystal/src/queries/creator_query.cr: -------------------------------------------------------------------------------- 1 | class CreatorQuery < Creator::BaseQuery 2 | end 3 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021534_create_post_tags/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE post_tags; 2 | -------------------------------------------------------------------------------- /crystal/spec/creator_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Creator do 4 | end 5 | -------------------------------------------------------------------------------- /crystal/src/queries/post_tag_query.cr: -------------------------------------------------------------------------------- 1 | class PostTagQuery < PostTag::BaseQuery 2 | end 3 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021527_create_creator_tags/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE creator_tags; 2 | -------------------------------------------------------------------------------- /crystal/spec/setup/clean_database.cr: -------------------------------------------------------------------------------- 1 | Spec.before_each do 2 | AppDatabase.truncate 3 | end 4 | -------------------------------------------------------------------------------- /crystal/spec/setup/reset_emails.cr: -------------------------------------------------------------------------------- 1 | Spec.before_each do 2 | Carbon::DevAdapter.reset 3 | end 4 | -------------------------------------------------------------------------------- /crystal/src/operations/save_post_tag.cr: -------------------------------------------------------------------------------- 1 | class SavePostTag < PostTag::SaveOperation 2 | end 3 | -------------------------------------------------------------------------------- /crystal/src/queries/creator_tag_query.cr: -------------------------------------------------------------------------------- 1 | class CreatorTagQuery < CreatorTag::BaseQuery 2 | end 3 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021504_create_post_categories/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE post_categories; 2 | -------------------------------------------------------------------------------- /crystal/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/favicon.ico -------------------------------------------------------------------------------- /crystal/src/operations/save_creator_tag.cr: -------------------------------------------------------------------------------- 1 | class SaveCreatorTag < CreatorTag::SaveOperation 2 | end 3 | -------------------------------------------------------------------------------- /crystal/src/components/base_component.cr: -------------------------------------------------------------------------------- 1 | abstract class BaseComponent < Lucky::BaseComponent 2 | end 3 | -------------------------------------------------------------------------------- /crystal/src/queries/post_category_query.cr: -------------------------------------------------------------------------------- 1 | class PostCategoryQuery < PostCategory::BaseQuery 2 | end 3 | -------------------------------------------------------------------------------- /crystal/spec/setup/setup_database.cr: -------------------------------------------------------------------------------- 1 | Db::Create.new(quiet: true).call 2 | Db::Migrate.new(quiet: true).call 3 | -------------------------------------------------------------------------------- /crystal/public/assets/images/u/Bat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/Bat.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/Geal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/Geal.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/djc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/djc.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/mre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/mre.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/wezm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/wezm.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/zcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/zcat.jpg -------------------------------------------------------------------------------- /crystal/src/actions/me/show.cr: -------------------------------------------------------------------------------- 1 | class Me::Show < BrowserAction 2 | get "/me" do 3 | html ShowPage 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /crystal/public/assets/images/jsonfeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/jsonfeed.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/Kornel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/Kornel.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/Plume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/Plume.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/aeplay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/aeplay.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/antoyo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/antoyo.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/cyplo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/cyplo.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/drahnr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/drahnr.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/icefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/icefox.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/jonhoo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/jonhoo.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/koute.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/koute.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/nvzqz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/nvzqz.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/sgrif.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/sgrif.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/skade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/skade.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/tomaka.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/tomaka.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/yrashk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/yrashk.jpg -------------------------------------------------------------------------------- /crystal/spec/support/flows/base_flow.cr: -------------------------------------------------------------------------------- 1 | # Add methods that all or most Flows need to share 2 | class BaseFlow < LuckyFlow 3 | end 4 | -------------------------------------------------------------------------------- /rust/migrations/2020-01-06-035808_add_full_text_index/down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX search_index; 2 | DROP MATERIALIZED VIEW search_view; 3 | -------------------------------------------------------------------------------- /crystal/public/assets/images/u/Aardwolf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/Aardwolf.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/Aaron1011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/Aaron1011.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/MaikKlein.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/MaikKlein.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/ag_dubs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/ag_dubs.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/arcterus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/arcterus.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/ivanceras.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/ivanceras.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/jackpot51.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/jackpot51.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/japaric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/japaric.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/jplatte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/jplatte.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/kbknapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/kbknapp.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/killercup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/killercup.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/mopemope.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/mopemope.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/passcod.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/passcod.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/phansch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/phansch.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/phil-opp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/phil-opp.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/r-iendo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/r-iendo.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/raphlinus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/raphlinus.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/retep998.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/retep998.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/sebcrozet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/sebcrozet.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/stjepang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/stjepang.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/Bat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/Bat.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/djc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/djc.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/mre.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/mre.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/xanewok.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/xanewok.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/yupferris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/yupferris.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/XAMPPRocky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/XAMPPRocky.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/chriskrycho.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/chriskrycho.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/dessalines.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/dessalines.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/fulmicoton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/fulmicoton.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/jackyalcine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/jackyalcine.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/measlytwerp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/measlytwerp.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/Geal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/Geal.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/Plume.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/Plume.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/cyplo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/cyplo.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/koute.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/koute.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/nvzqz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/nvzqz.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/sgrif.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/sgrif.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/skade.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/skade.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/wezm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/wezm.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/zcat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/zcat.jpg -------------------------------------------------------------------------------- /crystal/config/error_handler.cr: -------------------------------------------------------------------------------- 1 | Lucky::ErrorHandler.configure do |settings| 2 | settings.show_debug_output = !LuckyEnv.production? 3 | end 4 | -------------------------------------------------------------------------------- /crystal/config/html_page.cr: -------------------------------------------------------------------------------- 1 | Lucky::HTMLPage.configure do |settings| 2 | settings.render_component_comments = !LuckyEnv.production? 3 | end 4 | -------------------------------------------------------------------------------- /crystal/public/assets/images/u/DenisKolodin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/DenisKolodin.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/GuillaumeGomez.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/GuillaumeGomez.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/Aardwolf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/Aardwolf.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/Kornel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/Kornel.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/aeplay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/aeplay.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/ag_dubs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/ag_dubs.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/antoyo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/antoyo.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/arcterus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/arcterus.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/drahnr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/drahnr.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/icefox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/icefox.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/japaric.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/japaric.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/jonhoo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/jonhoo.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/jplatte.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/jplatte.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/kbknapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/kbknapp.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/mopemope.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/mopemope.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/passcod.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/passcod.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/phansch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/phansch.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/phil-opp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/phil-opp.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/r-iendo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/r-iendo.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/retep998.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/retep998.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/stjepang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/stjepang.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/tomaka.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/tomaka.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/xanewok.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/xanewok.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/yrashk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/yrashk.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/waywardmonkeys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/waywardmonkeys.png -------------------------------------------------------------------------------- /rust/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | -------------------------------------------------------------------------------- /crystal/public/assets/images/u/QuietMisdreavus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/QuietMisdreavus.png -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/Aaron1011.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/Aaron1011.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/MaikKlein.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/MaikKlein.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/XAMPPRocky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/XAMPPRocky.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/dessalines.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/dessalines.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/fulmicoton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/fulmicoton.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/ivanceras.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/ivanceras.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/jackpot51.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/jackpot51.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/killercup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/killercup.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/raphlinus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/raphlinus.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/sebcrozet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/sebcrozet.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/yupferris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/yupferris.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/DenisKolodin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/DenisKolodin.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/chriskrycho.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/chriskrycho.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/jackyalcine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/jackyalcine.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/measlytwerp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/measlytwerp.jpg -------------------------------------------------------------------------------- /crystal/src/actions/api/me/show.cr: -------------------------------------------------------------------------------- 1 | class Api::Me::Show < ApiAction 2 | get "/api/me" do 3 | json UserSerializer.new(current_user) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /crystal/src/actions/posts/new.cr: -------------------------------------------------------------------------------- 1 | class Posts::New < BrowserAction 2 | get "/posts/new" do 3 | html NewPage, form: SavePost.new 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /crystal/src/emails/templates/password_reset_request_email/text.ecr: -------------------------------------------------------------------------------- 1 | Please reset your password: 2 | 3 | <%= PasswordResets::New.url(@user.id, @token) %> 4 | -------------------------------------------------------------------------------- /crystal/public/assets/fonts/nunitosans-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/fonts/nunitosans-bold-webfont.woff -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/GuillaumeGomez.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/GuillaumeGomez.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/QuietMisdreavus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/QuietMisdreavus.jpg -------------------------------------------------------------------------------- /crystal/public/assets/images/u/thumb/waywardmonkeys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/images/u/thumb/waywardmonkeys.jpg -------------------------------------------------------------------------------- /crystal/src/models/base_model.cr: -------------------------------------------------------------------------------- 1 | abstract class BaseModel < Avram::Model 2 | def self.database : Avram::Database.class 3 | AppDatabase 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /crystal/src/pages/admin_layout.cr: -------------------------------------------------------------------------------- 1 | require "./main_layout" 2 | 3 | abstract class AdminLayout < MainLayout 4 | def admin_js? 5 | true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /crystal/public/assets/fonts/nunitosans-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/fonts/nunitosans-bold-webfont.woff2 -------------------------------------------------------------------------------- /crystal/public/assets/fonts/nunitosans-italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/fonts/nunitosans-italic-webfont.woff -------------------------------------------------------------------------------- /crystal/public/assets/fonts/nunitosans-italic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/fonts/nunitosans-italic-webfont.woff2 -------------------------------------------------------------------------------- /crystal/public/assets/fonts/nunitosans-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/fonts/nunitosans-regular-webfont.woff -------------------------------------------------------------------------------- /crystal/public/assets/fonts/nunitosans-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/fonts/nunitosans-regular-webfont.woff2 -------------------------------------------------------------------------------- /crystal/public/assets/fonts/nunitosans-semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/fonts/nunitosans-semibold-webfont.woff -------------------------------------------------------------------------------- /crystal/public/assets/fonts/nunitosans-semibold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wezm/read-rust/HEAD/crystal/public/assets/fonts/nunitosans-semibold-webfont.woff2 -------------------------------------------------------------------------------- /crystal/src/actions/mixins/password_resets/find_user.cr: -------------------------------------------------------------------------------- 1 | module Auth::PasswordResets::FindUser 2 | private def user : User 3 | UserQuery.find(user_id) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /crystal/src/models/search_result.cr: -------------------------------------------------------------------------------- 1 | class SearchResult 2 | getter post 3 | getter summary 4 | 5 | def initialize(@post : Post, @summary : String) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /crystal/src/emails/templates/password_reset_request_email/html.ecr: -------------------------------------------------------------------------------- 1 |

Please reset your password

2 | 3 | Reset password 4 | -------------------------------------------------------------------------------- /crystal/src/serializers/user_serializer.cr: -------------------------------------------------------------------------------- 1 | class UserSerializer < BaseSerializer 2 | def initialize(@user : User) 3 | end 4 | 5 | def render 6 | {email: @user.email} 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /crystal/src/operations/import_post.cr: -------------------------------------------------------------------------------- 1 | class ImportPost < Post::SaveOperation 2 | permit_columns guid, title, url, twitter_url, mastodon_url, author, summary, tweeted_at, tooted_at, created_at 3 | end 4 | -------------------------------------------------------------------------------- /crystal/src/operations/save_creator.cr: -------------------------------------------------------------------------------- 1 | class SaveCreator < Creator::SaveOperation 2 | permit_columns name, avatar, support_link_name, support_link_url, code_link_name, code_link_url, description 3 | end 4 | -------------------------------------------------------------------------------- /crystal/src/actions/sign_ins/new.cr: -------------------------------------------------------------------------------- 1 | class SignIns::New < BrowserAction 2 | include Auth::RedirectSignedInUsers 3 | 4 | get "/sign_in" do 5 | html NewPage, operation: SignInUser.new 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /crystal/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /crystal/spec/setup/start_app_server.cr: -------------------------------------------------------------------------------- 1 | app_server = AppServer.new 2 | 3 | spawn do 4 | app_server.listen 5 | end 6 | 7 | Spec.after_suite do 8 | LuckyFlow.shutdown 9 | app_server.close 10 | end 11 | -------------------------------------------------------------------------------- /crystal/src/pages/mixins/page/render_markdown.cr: -------------------------------------------------------------------------------- 1 | module Page::RenderMarkdown 2 | def render_markdown(source) 3 | options = Markd::Options.new(smart: true) 4 | Markd.to_html(source, options) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /crystal/src/operations/mixins/user_from_email.cr: -------------------------------------------------------------------------------- 1 | module UserFromEmail 2 | private def user_from_email : User? 3 | email.value.try do |value| 4 | UserQuery.new.email(value).first? 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /crystal/config/env.cr: -------------------------------------------------------------------------------- 1 | # Environments are managed using `LuckyEnv`. By default, development, production 2 | # and test are supported. 3 | 4 | # If you need additional environment support, add it here 5 | # LuckyEnv.add_env :staging -------------------------------------------------------------------------------- /crystal/src/actions/submit/show.cr: -------------------------------------------------------------------------------- 1 | class Submit::Show < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | before cache_in_varnish(2.minutes) 5 | 6 | get "/submit" do 7 | html ShowPage 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021519_create_tags/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS tags ( 2 | id bigserial PRIMARY KEY, 3 | name text NOT NULL 4 | ); 5 | 6 | CREATE UNIQUE INDEX IF NOT EXISTS tags_name_index ON tags (name); 7 | -------------------------------------------------------------------------------- /crystal/src/models/creator_tag.cr: -------------------------------------------------------------------------------- 1 | class CreatorTag < BaseModel 2 | skip_default_columns 3 | 4 | table do 5 | primary_key id : Int64 6 | belongs_to creator : Creator 7 | belongs_to tag : Tag 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /crystal/src/actions/sign_ins/delete.cr: -------------------------------------------------------------------------------- 1 | class SignIns::Delete < BrowserAction 2 | delete "/sign_out" do 3 | cache_friendly_sign_out 4 | flash.info = "You have been signed out" 5 | redirect to: SignIns::New 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /crystal/spec/support/factories/user_box.cr: -------------------------------------------------------------------------------- 1 | class UserFactory < Avram::Factory 2 | def initialize 3 | email "#{sequence("test-email")}@example.com" 4 | encrypted_password Authentic.generate_encrypted_password("password") 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /crystal/src/actions/tags/index.cr: -------------------------------------------------------------------------------- 1 | class Tags::Index < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | before cache_in_varnish(2.minutes) 5 | 6 | get "/tags" do 7 | html IndexPage, tags: TagQuery.with_posts 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /crystal/config/colors.cr: -------------------------------------------------------------------------------- 1 | # This enables the color output when in development or test 2 | # Check out the Colorize docs for more information 3 | # https://crystal-lang.org/api/Colorize.html 4 | Colorize.enabled = LuckyEnv.development? || LuckyEnv.test? 5 | -------------------------------------------------------------------------------- /crystal/src/actions/about/show.cr: -------------------------------------------------------------------------------- 1 | class About::Show < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | before cache_in_varnish(2.minutes) 5 | 6 | get "/about" do 7 | html ShowPage, categories: CategoryQuery.new 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /crystal/src/models/post_tag.cr: -------------------------------------------------------------------------------- 1 | class PostTag < BaseModel 2 | skip_default_columns 3 | delegate name, to: tag 4 | 5 | table do 6 | primary_key id : Int64 7 | belongs_to post : Post 8 | belongs_to tag : Tag 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /crystal/src/operations/save_post_category.cr: -------------------------------------------------------------------------------- 1 | class SavePostCategory < PostCategory::SaveOperation 2 | permit_columns :post_id, :category_id 3 | 4 | before_save do 5 | validate_inclusion_of category_id, in: Category.valid_ids 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/password_resets/token_from_session.cr: -------------------------------------------------------------------------------- 1 | module Auth::PasswordResets::TokenFromSession 2 | private def token : String 3 | session.get?(:password_reset_token) || raise "Password reset token not found in session" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /crystal/src/actions/favicon/show.cr: -------------------------------------------------------------------------------- 1 | class Favicon::Show < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | before cache_publicly(1.day) 5 | 6 | get "/favicon.ico" do 7 | file "public/favicon.ico", disposition: "inline" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/password_resets/base.cr: -------------------------------------------------------------------------------- 1 | module Auth::PasswordResets::Base 2 | macro included 3 | include Auth::RedirectSignedInUsers 4 | include Auth::PasswordResets::FindUser 5 | include Auth::PasswordResets::RequireToken 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /crystal/src/actions/password_reset_requests/new.cr: -------------------------------------------------------------------------------- 1 | class PasswordResetRequests::New < BrowserAction 2 | include Auth::RedirectSignedInUsers 3 | 4 | get "/password_reset_requests/new" do 5 | html NewPage, operation: RequestPasswordReset.new 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /crystal/src/models/tag_name.cr: -------------------------------------------------------------------------------- 1 | class TagName 2 | def initialize(@tag_name : String) 3 | end 4 | 5 | def format : String 6 | File.extname(@tag_name) 7 | end 8 | 9 | def name : String 10 | File.basename(@tag_name, format) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /crystal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "noEmit": true, 6 | "strictNullChecks": true, 7 | "noImplicitAny": true 8 | }, 9 | "files": [ 10 | "src/js/admin.js" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /crystal/src/actions/api/sign_ups/create.cr: -------------------------------------------------------------------------------- 1 | class Api::SignUps::Create < ApiAction 2 | include Api::Auth::SkipRequireAuthToken 3 | 4 | post "/api/sign_ups" do 5 | user = SignUpUser.create!(params) 6 | json({token: UserToken.generate(user)}) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /crystal/.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | start_server 7 | *.dwarf 8 | *.local.cr 9 | /tmp 10 | /public/js 11 | /public/css 12 | /public/fonts 13 | /public/images 14 | /public/mix-manifest.json 15 | /node_modules 16 | yarn-error.log 17 | -------------------------------------------------------------------------------- /crystal/src/serializers/base_serializer.cr: -------------------------------------------------------------------------------- 1 | abstract class BaseSerializer < Lucky::Serializer 2 | def self.for_collection(collection : Enumerable, *args, **named_args) 3 | collection.map do |object| 4 | new(object, *args, **named_args) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/auth/allow_guests.cr: -------------------------------------------------------------------------------- 1 | module Auth::AllowGuests 2 | macro included 3 | skip require_sign_in 4 | end 5 | 6 | # Since sign in is not required, current_user might be nil 7 | def current_user : User? 8 | current_user? 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /crystal/src/shards.cr: -------------------------------------------------------------------------------- 1 | # Load .env file before any other config or app code 2 | require "lucky_env" 3 | LuckyEnv.load?(".env") 4 | 5 | # Require your shards here 6 | require "lucky" 7 | require "avram/lucky" 8 | require "carbon" 9 | require "authentic" 10 | require "jwt" 11 | -------------------------------------------------------------------------------- /crystal/src/actions/robots/show.cr: -------------------------------------------------------------------------------- 1 | class Robots::Show < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | before cache_publicly(1.hour) 5 | 6 | get "/robots.txt" do 7 | plain_text "User-Agent: * 8 | Disallow: 9 | Sitemap: #{Sitemap::Show.url} 10 | " 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/categories/find_category.cr: -------------------------------------------------------------------------------- 1 | module Categories::FindCategory 2 | private def category 3 | if category = CategoryQuery.new.slug(slug).first? 4 | category 5 | else 6 | raise Lucky::RouteNotFoundError.new(context) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /crystal/spec/support/app_client.cr: -------------------------------------------------------------------------------- 1 | class ApiClient < Lucky::BaseHTTPClient 2 | def initialize 3 | super 4 | headers("Content-Type": "application/json") 5 | end 6 | 7 | def self.auth(user : User) 8 | new.headers("Authorization": UserToken.generate(user)) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /crystal/src/pages/posts/show_page.cr: -------------------------------------------------------------------------------- 1 | class Posts::ShowPage < MainLayout 2 | needs post : Post 3 | quick_def page_title, @post.title 4 | quick_def page_description, @post.summary 5 | 6 | def content 7 | mount Posts::Summary, @post, @current_user, show_categories: true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /crystal/spec/support/factories/post_box.cr: -------------------------------------------------------------------------------- 1 | class PostFactory < Avram::Factory 2 | def initialize 3 | guid UUID.random(Random.new, UUID::Variant::RFC4122, UUID::Version::V4) 4 | title "Test" 5 | url "https://example.com/" 6 | author "Test Suite" 7 | summary "Summary" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/api/auth/skip_require_auth_token.cr: -------------------------------------------------------------------------------- 1 | module Api::Auth::SkipRequireAuthToken 2 | macro included 3 | skip require_auth_token 4 | end 5 | 6 | # Since sign in is not required, current_user might be nil 7 | def current_user : User? 8 | current_user? 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /crystal/src/actions/password_resets/edit.cr: -------------------------------------------------------------------------------- 1 | class PasswordResets::Edit < BrowserAction 2 | include Auth::PasswordResets::Base 3 | include Auth::PasswordResets::TokenFromSession 4 | 5 | get "/password_resets/:user_id/edit" do 6 | html NewPage, operation: ResetPassword.new, user_id: user_id.to_i 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /crystal/public/assets/images/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/actions/posts/show.cr: -------------------------------------------------------------------------------- 1 | class Posts::Show < BrowserAction 2 | before cache_in_varnish(2.minutes) 3 | 4 | get "/posts/:post_id" do 5 | post = PostQuery.new.preload_post_categories.preload_tags.find(post_id) 6 | weak_etag(post.updated_at.to_unix) 7 | 8 | html ShowPage, post: post 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /crystal/src/actions/sign_ups/new.cr: -------------------------------------------------------------------------------- 1 | class SignUps::New < BrowserAction 2 | include Auth::RedirectSignedInUsers 3 | 4 | get "/sign_up" do 5 | if ReadRust::Config.allow_sign_up? 6 | html NewPage, operation: SignUpUser.new 7 | else 8 | raise Lucky::RouteNotFoundError.new(context) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /crystal/src/models/user.cr: -------------------------------------------------------------------------------- 1 | class User < BaseModel 2 | include Carbon::Emailable 3 | include Authentic::PasswordAuthenticatable 4 | 5 | table do 6 | column email : String 7 | column encrypted_password : String 8 | end 9 | 10 | def emailable : Carbon::Address 11 | Carbon::Address.new(email) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /crystal/src/actions/rust_blogs/index.cr: -------------------------------------------------------------------------------- 1 | class RustBlogs::Index < BrowserAction 2 | include Auth::AllowGuests 3 | include Lucky::SkipRouteStyleCheck 4 | 5 | before cache_publicly(1.hour) 6 | 7 | get "/rust-blogs.opml" do 8 | file "public/rust-blogs.opml", content_type: "application/xml", disposition: "inline" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /crystal/src/emails/base_email.cr: -------------------------------------------------------------------------------- 1 | abstract class BaseEmail < Carbon::Email 2 | # You can add defaults using the 'inherited' hook 3 | # 4 | # Example: 5 | # 6 | # macro inherited 7 | # from default_from 8 | # end 9 | # 10 | # def default_from 11 | # Carbon::Address.new("support@app.com") 12 | # end 13 | end 14 | -------------------------------------------------------------------------------- /crystal/public/assets/images/rss.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/operations/save_tag.cr: -------------------------------------------------------------------------------- 1 | class SaveTag < Tag::SaveOperation 2 | before_save validate_size_of name, max: 25 3 | before_save validate_name 4 | 5 | private def validate_name 6 | if (name.value || "").strip.downcase !~ /\A[a-z][a-z0-9-]*\z/ 7 | name.add_error "must only contain a-z, 0-9, or hyphen" 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /crystal/src/pages/posts/new_page.cr: -------------------------------------------------------------------------------- 1 | class Posts::NewPage < AdminLayout 2 | needs form : SavePost 3 | quick_def page_title, "New Post" 4 | quick_def page_description, "" 5 | 6 | def content 7 | form_for Posts::Create, id: "new-post-form", class: "form-stacked" do 8 | mount Posts::Form, @form, post: nil 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/auth/test_backdoor.cr: -------------------------------------------------------------------------------- 1 | module Auth::TestBackdoor 2 | macro included 3 | before test_backdoor 4 | end 5 | 6 | private def test_backdoor 7 | if LuckyEnv.test? && (user_id = params.get?(:backdoor_user_id)) 8 | user = UserQuery.find(user_id) 9 | sign_in user 10 | end 11 | continue 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /crystal/config/authentic.cr: -------------------------------------------------------------------------------- 1 | require "./server" 2 | 3 | Authentic.configure do |settings| 4 | settings.secret_key = Lucky::Server.settings.secret_key_base 5 | 6 | unless LuckyEnv.production? 7 | # This value can be between 4 and 31 8 | fastest_encryption_possible = 4 9 | settings.encryption_cost = fastest_encryption_possible 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /crystal/src/models/search_results.cr: -------------------------------------------------------------------------------- 1 | class SearchResults 2 | getter results 3 | getter total 4 | getter page 5 | 6 | def self.none 7 | new([] of SearchResult, Page.new(1), 0) 8 | end 9 | 10 | def initialize(@results : Array(SearchResult), @page : Page, @total : UInt32) 11 | end 12 | 13 | def empty? 14 | @total == 0 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /crystal/src/start_server.cr: -------------------------------------------------------------------------------- 1 | require "./app" 2 | 3 | if LuckyEnv.development? 4 | Avram::Migrator::Runner.new.ensure_migrated! 5 | Avram::SchemaEnforcer.ensure_correct_column_mappings! 6 | end 7 | Habitat.raise_if_missing_settings! 8 | 9 | app_server = AppServer.new 10 | 11 | Signal::INT.trap do 12 | app_server.close 13 | end 14 | 15 | app_server.listen 16 | -------------------------------------------------------------------------------- /crystal/src/actions/posts/edit.cr: -------------------------------------------------------------------------------- 1 | class Posts::Edit < BrowserAction 2 | get "/posts/:post_id/edit" do 3 | post = PostQuery.new.preload_post_categories.preload_tags.find(post_id) 4 | form = SavePost.new(post) 5 | form.tags.value = post.tags.map(&.name).join(" ") 6 | 7 | html EditPage, 8 | form: form, 9 | post: post 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /crystal/src/serializers/errors/show_serializer.cr: -------------------------------------------------------------------------------- 1 | class ErrorSerializer < BaseSerializer 2 | def initialize( 3 | @message : String, 4 | @details : String? = nil, 5 | @param : String? = nil # If there was a problem with a specific param 6 | ) 7 | end 8 | 9 | def render 10 | {message: @message, param: @param, details: @details} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /crystal/src/models/avatar.cr: -------------------------------------------------------------------------------- 1 | class Avatar 2 | getter image 3 | 4 | def initialize(image : String) 5 | @image = Path[image] 6 | end 7 | 8 | def thumbnail_path : Path 9 | if image.extension == ".svg" 10 | Path["images/u"].join(image) 11 | else 12 | Path["images/u/thumb"].join(image.basename(image.extension) + ".jpg") 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /crystal/src/models/post_category.cr: -------------------------------------------------------------------------------- 1 | class PostCategory < BaseModel 2 | skip_default_columns 3 | delegate slug, name, to: category 4 | 5 | table do 6 | primary_key id : Int64 7 | belongs_to post : Post 8 | column category_id : Int16 9 | end 10 | 11 | def category : Category 12 | Category::ALL.find { |cat| cat.id == category_id }.not_nil! 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /crystal/src/models/tag.cr: -------------------------------------------------------------------------------- 1 | class Tag < BaseModel 2 | skip_default_columns 3 | 4 | table do 5 | primary_key id : Int64 6 | column name : String 7 | has_many creator_tags : CreatorTag 8 | has_many creators : Creator, through: [:creator_tags, :creator] 9 | has_many post_tags : PostTag 10 | has_many posts : Post, through: [:post_tags, :post] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021440_create_users/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS users ( 2 | id bigserial PRIMARY KEY, 3 | created_at timestamp with time zone NOT NULL, 4 | updated_at timestamp with time zone NOT NULL, 5 | email text NOT NULL, 6 | encrypted_password text NOT NULL 7 | ); 8 | 9 | CREATE UNIQUE INDEX IF NOT EXISTS users_email_index ON users (email); 10 | -------------------------------------------------------------------------------- /crystal/src/pages/posts/edit_page.cr: -------------------------------------------------------------------------------- 1 | class Posts::EditPage < MainLayout 2 | needs form : SavePost 3 | needs post : Post 4 | quick_def page_title, "Edit Post" 5 | quick_def page_description, "" 6 | 7 | def content 8 | form_for Posts::Update.with(@post.id), id: "edit-post-form", class: "form-stacked" do 9 | mount Posts::Form, @form, @post 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support_rust.md: -------------------------------------------------------------------------------- 1 | If you are suggesting a new entry to the Support Rust page, please include the 2 | following details: 3 | 4 | **Name:** _Preferred name to show on the page._ 5 | **Source Code URL:** _Primary place where published code lives._ 6 | **Support URL:** _Patreon, GitHub, Liberapay, etc._ 7 | **Description:** _Short description of what this person or project is working on._ 8 | -------------------------------------------------------------------------------- /crystal/spec/support/factories/creator_box.cr: -------------------------------------------------------------------------------- 1 | class CreatorFactory < Avram::Factory 2 | def initialize 3 | name "Test Creator" 4 | avatar "test.jpg" 5 | support_link_name "Support on Patreon" 6 | support_link_url "http://example.com/support" 7 | code_link_name "test" 8 | code_link_url "http://example.com/code/test" 9 | description "Description" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /rust/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /crystal/src/operations/reset_password.cr: -------------------------------------------------------------------------------- 1 | class ResetPassword < User::SaveOperation 2 | # Change password validations in src/operations/mixins/password_validations.cr 3 | include PasswordValidations 4 | 5 | attribute password : String 6 | attribute password_confirmation : String 7 | 8 | before_save do 9 | Authentic.copy_and_encrypt password, to: encrypted_password 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /crystal/src/actions/api/sign_ins/create.cr: -------------------------------------------------------------------------------- 1 | class Api::SignIns::Create < ApiAction 2 | include Api::Auth::SkipRequireAuthToken 3 | 4 | post "/api/sign_ins" do 5 | SignInUser.run(params) do |operation, user| 6 | if user 7 | json({token: UserToken.generate(user)}) 8 | else 9 | raise Avram::InvalidOperationError.new(operation) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /crystal/src/components/shared/flash_messages.cr: -------------------------------------------------------------------------------- 1 | class Shared::FlashMessages < BaseComponent 2 | needs flash : Lucky::FlashStore 3 | 4 | def render 5 | @flash.each do |flash_type, flash_message| 6 | div class: "flash-container" do 7 | div class: "flash flash-#{flash_type}", flow_id: "flash" do 8 | text flash_message 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /crystal/src/queries/category_query.cr: -------------------------------------------------------------------------------- 1 | class CategoryQuery 2 | include Enumerable(Category) 3 | 4 | def each 5 | Category::ALL.each do |category| 6 | yield category 7 | end 8 | end 9 | 10 | def slug(slug) 11 | Category::ALL.select { |category| category.slug == slug } 12 | end 13 | 14 | def without_all 15 | Category::ALL.reject { |category| category.all? } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /crystal/config/route_helper.cr: -------------------------------------------------------------------------------- 1 | # This is used when generating URLs for your application 2 | Lucky::RouteHelper.configure do |settings| 3 | if LuckyEnv.production? 4 | # Example: https://my_app.com 5 | settings.base_uri = ENV.fetch("APP_DOMAIN") 6 | else 7 | # Set domain to the default host/port in development/test 8 | settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /crystal/src/models/page.cr: -------------------------------------------------------------------------------- 1 | # A type representing a page number 2 | # 3 | # Will always be in the range 1..=UInt16::MAX 4 | struct Page 5 | delegate to_i, to_u16, to_u32, to_s, succ, pred, to: @page 6 | 7 | def initialize(page) 8 | if page < 1 || page > UInt16::MAX 9 | @page = 1_u16 10 | else 11 | @page = page.to_u16 12 | end 13 | end 14 | 15 | def first? 16 | @page == 1_u16 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /crystal/src/operations/mixins/password_validations.cr: -------------------------------------------------------------------------------- 1 | module PasswordValidations 2 | macro included 3 | before_save run_password_validations 4 | end 5 | 6 | private def run_password_validations 7 | validate_required password, password_confirmation 8 | validate_confirmation_of password, with: password_confirmation 9 | # 72 is the limit of BCrypt 10 | validate_size_of password, min: 6, max: 72 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /crystal/src/pages/tags/index_page.cr: -------------------------------------------------------------------------------- 1 | class Tags::IndexPage < MainLayout 2 | needs tags : Array(String) 3 | quick_def page_title, "Tags" 4 | quick_def page_description, "This page lists all the tags that posts are categorised by." 5 | 6 | def content 7 | div class: "justify-text" do 8 | @tags.each do |tag| 9 | text " " 10 | link tag, to: Tags::Show.with(tag), class: "tag" 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /crystal/src/components/shared/field_errors.cr: -------------------------------------------------------------------------------- 1 | class Shared::FieldErrors(T) < BaseComponent 2 | needs field : Avram::PermittedAttribute(T) 3 | 4 | # Customize the markup and styles to match your application 5 | def render 6 | unless @field.valid? 7 | div class: "error" do 8 | label_text = Wordsmith::Inflector.humanize(@field.name.to_s) 9 | text "#{label_text} #{@field.errors.first}" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /crystal/public/assets/images/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://wmoore@localhost/read_rust_development 2 | TEST_DATABASE_URL=postgres://wmoore@localhost/read_rust_test 3 | MASTODON_BASE=https://botsin.space 4 | MASTODON_CLIENT_ID= 5 | MASTODON_CLIENT_SECRET= 6 | MASTODON_REDIRECT=urn:ietf:wg:oauth:2.0:oob 7 | MASTODON_TOKEN= 8 | TWITTER_CONSUMER_KEY= 9 | TWITTER_CONSUMER_SECRET= 10 | TWITTER_ACCESS_KEY= 11 | TWITTER_ACCESS_SECRET= 12 | FEEDBIN_USERNAME= 13 | FEEDBIN_PASSWORD= 14 | READRUST_ALLOW_SIGNUP=0 15 | -------------------------------------------------------------------------------- /crystal/src/serializers/error_serializer.cr: -------------------------------------------------------------------------------- 1 | # This is the default error serializer generated by Lucky. 2 | # Feel free to customize it in any way you like. 3 | class ErrorSerializer < BaseSerializer 4 | def initialize( 5 | @message : String, 6 | @details : String? = nil, 7 | @param : String? = nil # If there was a problem with a specific param 8 | ) 9 | end 10 | 11 | def render 12 | {message: @message, param: @param, details: @details} 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /crystal/spec/requests/api/me/show_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | describe Api::Me::Show do 4 | it "returns the signed in user" do 5 | user = UserFactory.create 6 | 7 | response = ApiClient.auth(user).exec(Api::Me::Show) 8 | 9 | response.should send_json(200, email: user.email) 10 | end 11 | 12 | it "fails if not authenticated" do 13 | response = ApiClient.exec(Api::Me::Show) 14 | 15 | response.status_code.should eq(401) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021511_create_creators/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS creators ( 2 | id bigserial PRIMARY KEY, 3 | name text NOT NULL, 4 | avatar text NOT NULL, 5 | support_link_name text NOT NULL, 6 | support_link_url text NOT NULL, 7 | code_link_name text NOT NULL, 8 | code_link_url text NOT NULL, 9 | description text NOT NULL, 10 | created_at timestamp with time zone NOT NULL, 11 | updated_at timestamp with time zone NOT NULL 12 | ); 13 | -------------------------------------------------------------------------------- /crystal/src/emails/password_reset_request_email.cr: -------------------------------------------------------------------------------- 1 | class PasswordResetRequestEmail < BaseEmail 2 | Habitat.create { setting stubbed_token : String? } 3 | delegate stubbed_token, to: :settings 4 | 5 | def initialize(@user : User) 6 | @token = stubbed_token || Authentic.generate_password_reset_token(@user) 7 | end 8 | 9 | to @user 10 | from "myapp@support.com" # or set a default in src/emails/base_email.cr 11 | subject "Reset your password" 12 | templates html, text 13 | end 14 | -------------------------------------------------------------------------------- /crystal/src/actions/creators/index.cr: -------------------------------------------------------------------------------- 1 | class Creators::Index < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | before cache_in_varnish(5.minutes) 5 | 6 | get "/support" do 7 | weak_etag(last_modified.to_unix) 8 | 9 | html IndexPage, creators: CreatorQuery.new.preload_tags 10 | end 11 | 12 | private def last_modified 13 | time = CreatorQuery.new.updated_at.select_max 14 | if time.nil? 15 | Time.utc 16 | else 17 | time 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /crystal/src/actions/posts/index.cr: -------------------------------------------------------------------------------- 1 | class Posts::Index < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | before cache_in_varnish(1.minute) 5 | 6 | get "/all" do 7 | weak_etag(last_modified.to_unix) 8 | 9 | html Posts::IndexPage, posts: PostQuery.new.created_at.desc_order 10 | end 11 | 12 | private def last_modified : Time 13 | time = PostQuery.new.updated_at.select_max 14 | if time.nil? 15 | Time.utc 16 | else 17 | time 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /crystal/src/components/search/form.cr: -------------------------------------------------------------------------------- 1 | class Search::Form < BaseComponent 2 | needs query : String = "" 3 | 4 | def render 5 | form action: "/search", class: "search-form", method: "get" do 6 | input aria_label: "Search Read Rust", autocapitalize: "off", autocomplete: "off", id: "q", maxlength: "255", name: "q", placeholder: "Search", title: "Search Read Rust", type: "search", value: @query 7 | text " " 8 | input type: "submit", value: "Search" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /crystal/src/operations/sign_up_user.cr: -------------------------------------------------------------------------------- 1 | class SignUpUser < User::SaveOperation 2 | param_key :user 3 | # Change password validations in src/operations/mixins/password_validations.cr 4 | include PasswordValidations 5 | 6 | permit_columns email 7 | attribute password : String 8 | attribute password_confirmation : String 9 | 10 | before_save do 11 | validate_uniqueness_of email 12 | Authentic.copy_and_encrypt(password, to: encrypted_password) if password.valid? 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /crystal/src/models/post.cr: -------------------------------------------------------------------------------- 1 | class Post < BaseModel 2 | table do 3 | column guid : UUID 4 | column title : String 5 | column url : String 6 | column twitter_url : String? 7 | column mastodon_url : String? 8 | column author : String 9 | column summary : String 10 | column tweeted_at : Time? 11 | column tooted_at : Time? 12 | has_many post_categories : PostCategory 13 | has_many post_tags : PostTag 14 | has_many tags : Tag, through: [:post_tags, :tag] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /crystal/README.md: -------------------------------------------------------------------------------- 1 | # read_rust 2 | 3 | This is a project written using [Lucky](https://luckyframework.org). Enjoy! 4 | 5 | ### Setting up the project 6 | 7 | 1. [Install required dependencies](http://luckyframework.org/guides/installing.html#install-required-dependencies) 8 | 1. Run `script/setup` 9 | 1. Run `lucky dev` to start the app 10 | 11 | ### Learning Lucky 12 | 13 | Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](http://luckyframework.org/guides). 14 | -------------------------------------------------------------------------------- /crystal/src/serializers/json_feed/post_serializer.cr: -------------------------------------------------------------------------------- 1 | class JsonFeed::PostSerializer < BaseSerializer 2 | def initialize(@post : Post) 3 | end 4 | 5 | def render 6 | { 7 | id: @post.guid.to_s, 8 | title: @post.title, 9 | content_text: @post.summary, 10 | url: @post.url, 11 | date_published: @post.created_at.to_rfc3339, 12 | author: { name: @post.author }, 13 | tags: @post.post_categories.map(&.name), 14 | } 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/auth/redirect_signed_in_users.cr: -------------------------------------------------------------------------------- 1 | module Auth::RedirectSignedInUsers 2 | macro included 3 | include Auth::AllowGuests 4 | before redirect_signed_in_users 5 | end 6 | 7 | private def redirect_signed_in_users 8 | if current_user? 9 | flash.success = "You are already signed in" 10 | redirect to: Home::Index 11 | else 12 | continue 13 | end 14 | end 15 | 16 | # current_user returns nil because signed in users are redirected. 17 | def current_user 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /crystal/src/queries/tag_query.cr: -------------------------------------------------------------------------------- 1 | class TagQuery < Tag::BaseQuery 2 | # Return the names of the tags that have at least one associated post 3 | def self.with_posts : Array(String) 4 | names = [] of String 5 | 6 | AppDatabase.run do |db| 7 | db.query_each "SELECT tags.name FROM tags, post_tags WHERE post_tags.tag_id = tags.id GROUP BY tags.name HAVING count(tags.name) > 0 ORDER BY tags.name;" do |result_set| 8 | names << result_set.read(String) 9 | end 10 | end 11 | 12 | names 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /crystal/spec/avatar_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Avatar do 4 | describe "avatar_thumbnail" do 5 | context "svg" do 6 | it { Avatar.new("test.svg").thumbnail_path.should eq Path["images/u/test.svg"] } 7 | end 8 | 9 | context "jpg" do 10 | it { Avatar.new("test.jpg").thumbnail_path.should eq Path["images/u/thumb/test.jpg"] } 11 | end 12 | 13 | context "png" do 14 | it { Avatar.new("test.png").thumbnail_path.should eq Path["images/u/thumb/test.jpg"] } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021504_create_post_categories/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS post_categories ( 2 | id bigserial PRIMARY KEY, 3 | post_id bigint NOT NULL, 4 | category_id smallint NOT NULL, 5 | CONSTRAINT post_categories_post_id_fkey FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE 6 | ); 7 | 8 | CREATE UNIQUE INDEX IF NOT EXISTS post_categories_post_id_category_id_index ON post_categories (post_id, category_id); 9 | CREATE INDEX IF NOT EXISTS post_categories_post_id_index ON post_categories (post_id); 10 | -------------------------------------------------------------------------------- /crystal/src/actions/json_feed/show.cr: -------------------------------------------------------------------------------- 1 | class JsonFeed::Show < BrowserAction 2 | include Auth::AllowGuests 3 | include Categories::FindCategory 4 | 5 | before cache_in_varnish(2.minutes) 6 | 7 | get "/:slug/feed.json" do 8 | unconditional_weak_etag(last_modified.to_unix) 9 | 10 | json ShowSerializer.new(category) 11 | end 12 | 13 | private def last_modified 14 | time = PostQuery.new.last_modified_in_category(category) 15 | if time.nil? 16 | Time.utc 17 | else 18 | time 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /crystal/src/models/creator.cr: -------------------------------------------------------------------------------- 1 | class Creator < BaseModel 2 | table do 3 | column name : String 4 | column avatar : String 5 | column support_link_name : String 6 | column support_link_url : String 7 | column code_link_name : String 8 | column code_link_url : String 9 | column description : String 10 | has_many creator_tags : CreatorTag 11 | has_many tags : Tag, through: [:creator_tags, :tag] 12 | end 13 | 14 | def avatar_thumbnail : String 15 | Avatar.new(avatar).thumbnail_path.to_s 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /crystal/config/feedbin.cr: -------------------------------------------------------------------------------- 1 | Feedbin::Client.configure do |settings| 2 | settings.username = feedbin_user_from_env 3 | settings.password = feedbin_pass_from_env 4 | end 5 | 6 | private def feedbin_user_from_env 7 | ENV["FEEDBIN_USERNAME"]? || warn_missing_credentials 8 | end 9 | 10 | private def feedbin_pass_from_env 11 | ENV["FEEDBIN_PASSWORD"]? || warn_missing_credentials 12 | end 13 | 14 | private def warn_missing_credentials 15 | puts "FEEDBIN_USERNAME and/or FEEDBIN_PASSWORD are not set, Feedbin integration won't work".colorize.red 16 | "" 17 | end 18 | -------------------------------------------------------------------------------- /crystal/src/actions/password_reset_requests/create.cr: -------------------------------------------------------------------------------- 1 | class PasswordResetRequests::Create < BrowserAction 2 | include Auth::RedirectSignedInUsers 3 | 4 | post "/password_reset_requests" do 5 | RequestPasswordReset.run(params) do |operation, user| 6 | if user 7 | PasswordResetRequestEmail.new(user).deliver 8 | flash.success = "You should receive an email on how to reset your password shortly" 9 | redirect SignIns::New 10 | else 11 | html NewPage, operation: operation 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /crystal/src/actions/search/show.cr: -------------------------------------------------------------------------------- 1 | class Search::Show < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | param q : String = "" 5 | param page : Int32 = 1 # Trying to make this UInt16 or UInt32 gives Error: undefined constant UInt32::Lucky 6 | 7 | get "/search" do 8 | if q.blank? 9 | flash.failure = "You need to specify what to search for" 10 | html Search::ShowPage, query: q, results: SearchResults.none 11 | else 12 | html Search::ShowPage, query: q, results: PostQuery.search(q, Page.new(page)) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | ## Missing Posts 2 | 3 | Adding new posts to Read Rust is currently a manual process and I generally 4 | update the feed a few times a week. I read Mastodon, Twitter and 5 | [Reddit][rust-reddit] as well as subscribe to a lot of RSS feeds to discover 6 | new posts. Please only submit posts written in, or after 2018. 7 | 8 | For all other posts [create an issue on GitHub][add-post]. 9 | 10 | [rust-reddit]: https://www.reddit.com/r/rust/ 11 | [add-post]: https://github.com/wezm/read-rust/issues/new?labels=missing-post&title=Add+post&template=missing_post.md 12 | -------------------------------------------------------------------------------- /crystal/public/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/spec/flows/reset_password_spec_disabled.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | describe "Reset password flow", tags: "flow" do 4 | it "works" do 5 | user = UserFactory.create 6 | flow = ResetPasswordFlow.new(user) 7 | 8 | flow.request_password_reset 9 | flow.should_have_sent_reset_email 10 | flow.reset_password "new-password" 11 | flow.should_be_signed_in 12 | flow.sign_out 13 | flow.sign_in "wrong-password" 14 | flow.should_have_password_error 15 | flow.sign_in "new-password" 16 | flow.should_be_signed_in 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/password_resets/require_token.cr: -------------------------------------------------------------------------------- 1 | module Auth::PasswordResets::RequireToken 2 | macro included 3 | before require_valid_password_reset_token 4 | end 5 | 6 | abstract def token : String 7 | abstract def user : User 8 | 9 | private def require_valid_password_reset_token 10 | if Authentic.valid_password_reset_token?(user, token) 11 | continue 12 | else 13 | flash.failure = "The password reset link is incorrect or expired. Please try again." 14 | redirect to: PasswordResetRequests::New 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /crystal/src/pages/password_reset_requests/new_page.cr: -------------------------------------------------------------------------------- 1 | class PasswordResetRequests::NewPage < AuthLayout 2 | needs operation : RequestPasswordReset 3 | quick_def page_title, "Password Reset" 4 | quick_def page_description, "" 5 | 6 | def content 7 | h1 "Reset your password" 8 | render_form(@operation) 9 | end 10 | 11 | private def render_form(op) 12 | form_for PasswordResetRequests::Create do 13 | mount Shared::Field, op.email, &.email_input 14 | submit "Reset Password", flow_id: "request-password-reset-button" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /crystal/src/utils/sanitise.cr: -------------------------------------------------------------------------------- 1 | @[Link("striptags")] 2 | lib LibStriptags 3 | fun strip_tags(input : UInt8*, input_len : Int32, output : UInt8**, output_len : Int32*) 4 | fun strip_tags_free(string : UInt8*) 5 | end 6 | 7 | module Sanitise 8 | def self.strip_tags(input : String) : String? 9 | LibStriptags.strip_tags(input.to_unsafe, input.bytesize, out output, out length) 10 | 11 | if output.null? 12 | return nil 13 | end 14 | 15 | result = String.new(output, length) 16 | LibStriptags.strip_tags_free(output) 17 | result.strip 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /crystal/public/assets/images/mastodon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crystal/src/actions/sign_ins/create.cr: -------------------------------------------------------------------------------- 1 | class SignIns::Create < BrowserAction 2 | include Auth::RedirectSignedInUsers 3 | 4 | post "/sign_ins" do 5 | SignInUser.run(params) do |operation, authenticated_user| 6 | if authenticated_user 7 | cache_friendly_sign_in(authenticated_user) 8 | flash.success = "You're now signed in" 9 | Authentic.redirect_to_originally_requested_path(self, fallback: Home::Index) 10 | else 11 | flash.failure = "Sign in failed" 12 | html NewPage, operation: operation 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021456_create_posts/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS posts ( 2 | id bigserial PRIMARY KEY, 3 | guid uuid NOT NULL, 4 | title text NOT NULL, 5 | url text NOT NULL, 6 | twitter_url text, 7 | mastodon_url text, 8 | author text NOT NULL, 9 | summary text NOT NULL, 10 | tweeted_at timestamp with time zone, 11 | tooted_at timestamp with time zone, 12 | created_at timestamp with time zone NOT NULL, 13 | updated_at timestamp with time zone NOT NULL 14 | ); 15 | 16 | CREATE UNIQUE INDEX IF NOT EXISTS posts_url_index ON posts (url); 17 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/auth/require_sign_in.cr: -------------------------------------------------------------------------------- 1 | module Auth::RequireSignIn 2 | macro included 3 | before require_sign_in 4 | end 5 | 6 | private def require_sign_in 7 | if current_user? 8 | continue 9 | else 10 | Authentic.remember_requested_path(self) 11 | flash.info = "Please sign in first" 12 | redirect to: SignIns::New 13 | end 14 | end 15 | 16 | # Tells the compiler that the current_user is not nil since we have checked 17 | # that the user is signed in 18 | private def current_user : User 19 | current_user?.not_nil! 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /crystal/src/actions/password_resets/create.cr: -------------------------------------------------------------------------------- 1 | class PasswordResets::Create < BrowserAction 2 | include Auth::PasswordResets::Base 3 | include Auth::PasswordResets::TokenFromSession 4 | 5 | post "/password_resets/:user_id" do 6 | ResetPassword.update(user, params) do |operation, user| 7 | if operation.saved? 8 | session.delete(:password_reset_token) 9 | sign_in user 10 | flash.success = "Your password has been reset" 11 | redirect to: Home::Index 12 | else 13 | html NewPage, operation: operation, user_id: user_id.to_i 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /crystal/src/models/category.cr: -------------------------------------------------------------------------------- 1 | require "json_mapping" 2 | 3 | class Category 4 | JSON.mapping( 5 | id: Int16, 6 | name: String, 7 | hashtag: String, 8 | slug: String, 9 | year: UInt16?, 10 | description: String, 11 | ) 12 | 13 | ALL = Array(Category).from_json({{ read_file("../content/_data/categories.json") }}) 14 | VALID_IDS = ALL.compact_map { |category| category.all? ? nil : category.id } 15 | 16 | # Return the list of category ids that are valid for a PostCategory record 17 | def self.valid_ids 18 | VALID_IDS 19 | end 20 | 21 | def all? 22 | id.zero? 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /crystal/bs-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | | Browser-sync config file 3 | | 4 | | For up-to-date information about the options: 5 | | http://www.browsersync.io/docs/options/ 6 | | 7 | */ 8 | 9 | module.exports = { 10 | snippetOptions: { 11 | rule: { 12 | match: /<\/head>/i, 13 | fn: function (snippet, match) { 14 | return snippet + match; 15 | } 16 | } 17 | }, 18 | files: ["public/css/**/*.css", "public/js/**/*.js"], 19 | watchEvents: ["change"], 20 | open: false, 21 | browser: "default", 22 | ghostMode: false, 23 | ui: false, 24 | online: false, 25 | logConnections: false 26 | }; 27 | -------------------------------------------------------------------------------- /crystal/src/operations/request_password_reset.cr: -------------------------------------------------------------------------------- 1 | class RequestPasswordReset < Avram::Operation 2 | # You can modify this in src/operations/mixins/user_from_email.cr 3 | include UserFromEmail 4 | 5 | attribute email : String 6 | 7 | # Run validations and yield the form and the user if valid 8 | def run 9 | user = user_from_email 10 | validate(user) 11 | 12 | if valid? 13 | user 14 | else 15 | nil 16 | end 17 | end 18 | 19 | def validate(user : User?) 20 | validate_required email 21 | if user.nil? 22 | email.add_error "is not in our system" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /crystal/src/actions/sign_ups/create.cr: -------------------------------------------------------------------------------- 1 | class SignUps::Create < BrowserAction 2 | include Auth::RedirectSignedInUsers 3 | 4 | post "/sign_ups" do 5 | if ReadRust::Config.allow_sign_up? 6 | SignUpUser.create(params) do |operation, user| 7 | if user 8 | flash.info = "Thanks for signing up" 9 | cache_friendly_sign_in(user) 10 | redirect to: Home::Index 11 | else 12 | flash.info = "Couldn't sign you up" 13 | html NewPage, operation: operation 14 | end 15 | end 16 | else 17 | raise Lucky::RouteNotFoundError.new(context) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /crystal/src/actions/api_action.cr: -------------------------------------------------------------------------------- 1 | # Include modules and add methods that are for all API requests 2 | abstract class ApiAction < Lucky::Action 3 | accepted_formats [:json] 4 | 5 | include Api::Auth::Helpers 6 | 7 | # By default all actions require sign in. 8 | # Add 'include Api::Auth::SkipRequireAuthToken' to your actions to allow all requests. 9 | include Api::Auth::RequireAuthToken 10 | 11 | # By default all actions are required to use underscores to separate words. 12 | # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes. 13 | include Lucky::EnforceUnderscoredRoute 14 | end 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/missing_post.md: -------------------------------------------------------------------------------- 1 | Please note that I'm winding down the Read Rust posting frequency and currently 2 | mainly publishing [Rust 2021] posts. For more information see my post: 3 | 4 | https://www.wezm.net/v2/posts/2020/slowing-read-rust-posting/ 5 | 6 | [Rust 2021]: https://blog.rust-lang.org/2020/09/03/Planning-2021-Roadmap.html 7 | 8 | If you'd still like to submit your post please fill in the following: 9 | 10 | Please add this post: 11 | 12 | **Post URL:** 13 | **Author Name:** 14 | **Categories:** pick one: Crates, Embedded, Games and Graphics, Getting Started, Language, Performance, Tools and Applications, Web and Network Services 15 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021534_create_post_tags/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS post_tags ( 2 | id bigserial PRIMARY KEY, 3 | post_id bigint NOT NULL, 4 | tag_id bigint NOT NULL, 5 | CONSTRAINT post_tags_post_id_fkey FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, 6 | CONSTRAINT post_tags_tag_id_fkey FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE 7 | ); 8 | 9 | CREATE INDEX IF NOT EXISTS post_tags_post_id_index ON post_tags (post_id); 10 | CREATE INDEX IF NOT EXISTS post_tags_tag_id_index ON post_tags (tag_id); 11 | CREATE UNIQUE INDEX IF NOT EXISTS post_tags_post_id_tag_id_index ON post_tags (post_id, tag_id); 12 | -------------------------------------------------------------------------------- /crystal/src/actions/home/index.cr: -------------------------------------------------------------------------------- 1 | class Home::Index < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | before cache_in_varnish(1.minute) 5 | 6 | get "/" do 7 | category = CategoryQuery.new.slug("all").first 8 | recent_posts = PostQuery.new.preload_post_categories.recent_in_category(category).limit(10) 9 | weak_etag(last_modified.to_unix) 10 | 11 | html Categories::IndexPage, categories: CategoryQuery.new.without_all, recent_posts: recent_posts 12 | end 13 | 14 | private def last_modified 15 | time = PostQuery.new.updated_at.select_max 16 | if time.nil? 17 | Time.utc 18 | else 19 | time 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /crystal/src/pages/search/show_page.cr: -------------------------------------------------------------------------------- 1 | class Search::ShowPage < MainLayout 2 | needs results : SearchResults 3 | 4 | quick_def page_title, "Search Results" 5 | quick_def page_description, "" 6 | 7 | def content 8 | mount Search::Form, @query 9 | 10 | if @results.empty? 11 | para "No results were found." 12 | else 13 | @results.results.each do |result| 14 | mount Posts::Summary, result.post, @current_user, show_categories: true, highlight: result.summary 15 | end 16 | 17 | mount Posts::Pagination, query: @query, page: @results.page, per_page: PostQuery::PER_PAGE.to_u16, total: @results.total 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /crystal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "UNLICENSED", 3 | "private": true, 4 | "dependencies": { 5 | "rails-ujs": "^5.1.4" 6 | }, 7 | "scripts": { 8 | "heroku-postbuild": "yarn prod", 9 | "dev": "yarn run mix", 10 | "watch": "yarn run mix watch", 11 | "prod": "yarn run mix --production" 12 | }, 13 | "devDependencies": { 14 | "@babel/compat-data": "^7.9.0", 15 | "browser-sync": "^2.18.13", 16 | "compression-webpack-plugin": "^7.0.0", 17 | "laravel-mix": "^6.0.0", 18 | "postcss": "^8.1.0", 19 | "resolve-url-loader": "^3.1.1", 20 | "sass": "^1.26.10", 21 | "sass-loader": "^10.0.2", 22 | "typescript": "^3.6.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crystal/src/pages/auth_layout.cr: -------------------------------------------------------------------------------- 1 | abstract class AuthLayout 2 | include Lucky::HTMLPage 3 | 4 | abstract def content 5 | abstract def page_title 6 | 7 | def extra_css 8 | "css/admin.css" 9 | end 10 | 11 | def render 12 | html_doctype 13 | 14 | html lang: "en" do 15 | mount Shared::LayoutHead, page_title: page_title, page_description: "", categories: CategoryQuery.new, app_js: false, admin: false, extra_css: extra_css 16 | 17 | body do 18 | mount Shared::Header, nil 19 | 20 | main class: "main" do 21 | mount Shared::FlashMessages, @context.flash 22 | content 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /crystal/src/components/posts/action_bar.cr: -------------------------------------------------------------------------------- 1 | class Posts::ActionBar < BaseComponent 2 | needs post : Post 3 | needs current_user : User? 4 | 5 | def render 6 | @current_user.try do 7 | div class: "post-action-bar" do 8 | link "Show", Posts::Show.with(@post.id) 9 | link "Edit", Posts::Edit.with(@post.id) 10 | twitter_url = @post.twitter_url 11 | mastodon_url = @post.mastodon_url 12 | a(href: twitter_url) { text "Tweet URL" } if twitter_url 13 | a(href: mastodon_url) { text "Fediverse URL" } if mastodon_url 14 | # link "Delete", Posts::Delete.with(@post.id), data_confirm: "Are you sure?" 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /crystal/src/actions/categories/show.cr: -------------------------------------------------------------------------------- 1 | class Categories::Show < BrowserAction 2 | include Auth::AllowGuests 3 | 4 | before cache_in_varnish(1.minute) 5 | 6 | get "/:slug" do 7 | if category = CategoryQuery.new.slug(slug).first? 8 | weak_etag(last_modified(category).to_unix) 9 | 10 | html ShowPage, category: category, posts: PostQuery.new.preload_tags.recent_in_category(category) 11 | else 12 | raise Lucky::RouteNotFoundError.new(context) 13 | end 14 | end 15 | 16 | private def last_modified(category) 17 | time = PostQuery.new.last_modified_in_category(category) 18 | if time.nil? 19 | Time.utc 20 | else 21 | time 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /rust/migrations/2019-12-26-021527_create_creator_tags/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS creator_tags ( 2 | id bigserial PRIMARY KEY, 3 | creator_id bigint NOT NULL, 4 | tag_id bigint NOT NULL, 5 | CONSTRAINT creator_tags_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES creators(id) ON DELETE CASCADE, 6 | CONSTRAINT creator_tags_tag_id_fkey FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE 7 | ); 8 | 9 | CREATE INDEX IF NOT EXISTS creator_tags_creator_id_index ON creator_tags (creator_id); 10 | CREATE INDEX IF NOT EXISTS creator_tags_tag_id_index ON creator_tags (tag_id); 11 | CREATE UNIQUE INDEX IF NOT EXISTS creator_tags_creator_id_tag_id_index ON creator_tags (creator_id, tag_id); 12 | -------------------------------------------------------------------------------- /rust/migrations/2020-01-06-035808_add_full_text_index/up.sql: -------------------------------------------------------------------------------- 1 | CREATE MATERIALIZED VIEW search_view AS 2 | SELECT posts.id, 3 | posts.summary, 4 | setweight(to_tsvector('english', posts.title), 'A') || 5 | setweight(to_tsvector('english', coalesce(string_agg(tags.name, ' '), '')), 'B') || 6 | setweight(to_tsvector('english', posts.summary), 'C') || 7 | setweight(to_tsvector('english', posts.author), 'D') AS vector 8 | FROM posts 9 | LEFT JOIN post_tags ON (posts.id = post_tags.post_id) 10 | LEFT JOIN tags ON (post_tags.tag_id = tags.id) 11 | GROUP BY posts.id; 12 | 13 | CREATE INDEX search_index ON search_view USING GIN (vector); 14 | -------------------------------------------------------------------------------- /crystal/src/pages/creators/index_page.cr: -------------------------------------------------------------------------------- 1 | class Creators::IndexPage < MainLayout 2 | quick_def page_title, "Support Rust" 3 | quick_def page_description, "This page used to list people and projects contributing to the Rust ecosystem that are accepting financial contributions." 4 | 5 | def app_js? 6 | true 7 | end 8 | 9 | def content 10 | para do 11 | text "This page used to list people and projects in the Rust ecosystem that were accepting financial contributions." 12 | text " It became stale so has been removed. To support folks consider using " 13 | a "cargo fund", href: "https://github.com/acfoltzer/cargo-fund" 14 | text " on your projects." 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /crystal/tasks.cr: -------------------------------------------------------------------------------- 1 | # This file loads your app and all your tasks when running 'lucky' 2 | # 3 | # Run 'lucky --help' to see all available tasks. 4 | # 5 | # Learn to create your own tasks: 6 | # https://luckyframework.org/guides/command-line-tasks/custom-tasks 7 | 8 | # See `LuckyEnv#task?` 9 | ENV["LUCKY_TASK"] = "true" 10 | 11 | # Load Lucky and the app (actions, models, etc.) 12 | require "./src/app" 13 | require "lucky_task" 14 | 15 | # You can add your own tasks here in the ./tasks folder 16 | require "./tasks/**" 17 | 18 | # Load migrations 19 | require "./db/migrations/**" 20 | 21 | # Load Lucky tasks (dev, routes, etc.) 22 | require "lucky/tasks/**" 23 | require "avram/lucky/tasks" 24 | 25 | LuckyTask::Runner.run -------------------------------------------------------------------------------- /crystal/src/pages/password_resets/new_page.cr: -------------------------------------------------------------------------------- 1 | class PasswordResets::NewPage < AuthLayout 2 | needs operation : ResetPassword 3 | needs user_id : Int32 4 | quick_def page_title, "Password Reset" 5 | quick_def page_description, "" 6 | 7 | def content 8 | h1 "Reset your password" 9 | render_password_reset_form(@operation) 10 | end 11 | 12 | private def render_password_reset_form(op) 13 | form_for PasswordResets::Create.with(@user_id) do 14 | mount Shared::Field, op.password, &.password_input(autofocus: "true") 15 | mount Shared::Field, op.password_confirmation, &.password_input 16 | 17 | submit "Update Password", flow_id: "update-password-button" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /crystal/public/assets/images/heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rust/src/models.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use uuid::Uuid; 3 | 4 | use crate::schema::posts; 5 | 6 | #[derive(Identifiable, Queryable)] 7 | pub struct Post { 8 | pub id: i64, 9 | pub guid: Uuid, 10 | pub title: String, 11 | pub url: String, 12 | pub twitter_url: Option, 13 | pub mastodon_url: Option, 14 | pub author: String, 15 | pub summary: String, 16 | pub tweeted_at: Option>, 17 | pub tooted_at: Option>, 18 | pub created_at: DateTime, 19 | pub updated_at: DateTime, 20 | } 21 | 22 | #[derive(Queryable)] 23 | pub struct PostCategory { 24 | pub id: i64, 25 | pub post_id: i64, 26 | pub category_id: i16, 27 | } 28 | -------------------------------------------------------------------------------- /crystal/src/actions/password_resets/new.cr: -------------------------------------------------------------------------------- 1 | class PasswordResets::New < BrowserAction 2 | include Auth::PasswordResets::Base 3 | 4 | param token : String 5 | 6 | get "/password_resets/:user_id" do 7 | redirect_to_edit_form_without_token_param 8 | end 9 | 10 | # This is to prevent password reset tokens from being scraped in the HTTP Referer header 11 | # See more info here: https://github.com/thoughtbot/clearance/pull/707 12 | private def redirect_to_edit_form_without_token_param 13 | make_token_available_to_future_actions 14 | redirect to: PasswordResets::Edit.with(user_id) 15 | end 16 | 17 | private def make_token_available_to_future_actions 18 | session.set(:password_reset_token, token) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /crystal/src/actions/mixins/api/auth/helpers.cr: -------------------------------------------------------------------------------- 1 | module Api::Auth::Helpers 2 | def current_user? : User? 3 | auth_token.try do |value| 4 | user_from_auth_token(value) 5 | end 6 | end 7 | 8 | private def auth_token : String? 9 | bearer_token || token_param 10 | end 11 | 12 | private def bearer_token : String? 13 | context.request.headers["Authorization"]? 14 | .try(&.gsub("Bearer", "")) 15 | .try(&.strip) 16 | end 17 | 18 | private def token_param : String? 19 | params.get?(:auth_token) 20 | end 21 | 22 | private def user_from_auth_token(token : String) : User? 23 | UserToken.decode_user_id(token).try do |user_id| 24 | UserQuery.new.id(user_id).first? 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /crystal/src/pages/me/show_page.cr: -------------------------------------------------------------------------------- 1 | class Me::ShowPage < MainLayout 2 | quick_def page_title, "Your Profile" 3 | quick_def page_description, "" 4 | 5 | def content 6 | h1 "This is your profile" 7 | @current_user.try do |user| 8 | # FIXME: How to deal with this nicely 9 | h3 "Email: #{user.email}" 10 | end 11 | helpful_tips 12 | end 13 | 14 | private def helpful_tips 15 | h3 "Next, you may want to:" 16 | ul do 17 | li "Modify this page: src/pages/me/show_page.cr" 18 | li "Change where you go after sign in: src/actions/home/index.cr" 19 | li "To add pages that do not require sign in, include the" + 20 | "Auth::AllowGuests module in your actions" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /crystal/src/operations/save_post.cr: -------------------------------------------------------------------------------- 1 | class SavePost < Post::SaveOperation 2 | permit_columns title, url, twitter_url, mastodon_url, author, summary 3 | 4 | attribute tags : String 5 | 6 | before_save assign_guid 7 | before_save validate_tags 8 | before_save validate_url 9 | 10 | private def assign_guid 11 | guid.value ||= UUID.random 12 | end 13 | 14 | private def validate_tags 15 | if (tags.value || "").strip.downcase !~ /\A[ a-z0-9-]*\z/ 16 | tags.add_error "must be space separated and only contain a-z, 0-9, or hyphen" 17 | end 18 | end 19 | 20 | private def validate_url 21 | if (url.value || "").includes?("utm_source") 22 | url.add_error "must not contain tracking parameters" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /crystal/src/serializers/json_feed/show_serializer.cr: -------------------------------------------------------------------------------- 1 | class JsonFeed::ShowSerializer < BaseSerializer 2 | def initialize(@category : Category) 3 | end 4 | 5 | def render 6 | posts = PostQuery.new.preload_post_categories.recent_in_category(@category).limit(100) 7 | { 8 | version: "https://jsonfeed.org/version/1", 9 | title: "Read Rust - #{@category.name}", 10 | home_page_url: "https://readrust.net/", 11 | 12 | feed_url: JsonFeed::Show.with(@category.slug).url, 13 | description: @category.description, 14 | author: { 15 | name: "Wesley Moore", 16 | url: "https://www.wezm.net/", 17 | }, 18 | items: posts.map { |post| PostSerializer.new(post) }, 19 | } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /crystal/config/application.cr: -------------------------------------------------------------------------------- 1 | # This file may be used for custom Application configurations. 2 | # It will be loaded before other config files. 3 | # 4 | # Read more on configuration: 5 | # https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code 6 | 7 | # Use this code as an example: 8 | # 9 | # ``` 10 | # module Application 11 | # Habitat.create do 12 | # setting support_email : String 13 | # setting lock_with_basic_auth : Bool 14 | # end 15 | # end 16 | # 17 | # Application.configure do |settings| 18 | # settings.support_email = "support@myapp.io" 19 | # settings.lock_with_basic_auth = LuckEnv.staging? 20 | # end 21 | # 22 | # # In your application, call 23 | # # `Application.settings.support_email` anywhere you need it. 24 | # ``` -------------------------------------------------------------------------------- /crystal/public/assets/images/u/Amethyst.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /rust/src/social_network.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::rc::Rc; 3 | 4 | use diesel::pg::PgConnection; 5 | use diesel::prelude::*; 6 | 7 | use crate::categories::Category; 8 | use crate::models::Post; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 11 | pub enum AccessMode { 12 | ReadOnly, 13 | ReadWrite, 14 | } 15 | 16 | pub trait SocialNetwork: Sized { 17 | fn from_env(access_mode: AccessMode) -> Result>; 18 | 19 | fn register() -> Result<(), Box>; 20 | 21 | fn unpublished_posts(connection: &PgConnection) -> QueryResult>; 22 | 23 | fn publish_post(&self, post: &Post, categories: &[Rc]) -> Result<(), Box>; 24 | 25 | fn mark_post_published(&self, connection: &PgConnection, post: Post) -> QueryResult<()>; 26 | } 27 | -------------------------------------------------------------------------------- /crystal/src/app_server.cr: -------------------------------------------------------------------------------- 1 | class AppServer < Lucky::BaseAppServer 2 | def middleware : Array(HTTP::Handler) 3 | [ 4 | Lucky::RequestIdHandler.new, 5 | Lucky::ForceSSLHandler.new, 6 | Lucky::HttpMethodOverrideHandler.new, 7 | Lucky::LogHandler.new, 8 | Lucky::ErrorHandler.new(action: Errors::Show), 9 | Lucky::RemoteIpHandler.new, 10 | Lucky::RouteHandler.new, 11 | Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"), 12 | Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false), 13 | Lucky::RouteNotFoundHandler.new, 14 | ] of HTTP::Handler 15 | end 16 | 17 | def protocol 18 | "http" 19 | end 20 | 21 | def listen 22 | server.listen(host, port, reuse_port: false) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "read-rust" 3 | version = "2.0.0" 4 | authors = ["Wesley Moore "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | chrono = { version = "0", features = ['serde'] } 9 | diesel = { version = "=1.4.3", features = ["postgres", "chrono", "uuidv07"] } 10 | dotenv = "0" 11 | egg-mode = { version = "0", default-features = false, features = ["hyper-rustls"], optional = true } 12 | env_logger = "0" 13 | getopts = "0" 14 | log = "0" 15 | elefren = { version = "0.21.0", default-features = false, features = ["rustls-tls"] } 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_json = "1.0" 18 | signal-hook = "0" 19 | tokio = "0.1" 20 | url = "2.0" 21 | uuid = { version = "0.7.0", features = ['v4', 'serde'] } # version needs to match diesel 22 | 23 | [features] 24 | twitter = ["egg-mode"] -------------------------------------------------------------------------------- /crystal/spec/sanitise_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Sanitise do 4 | context "strip_tags" do 5 | it "strips tags" do 6 | stripped = Sanitise.strip_tags("

This is a simple test.

") 7 | stripped.should eq "This is a simple test." 8 | end 9 | 10 | it "puts whitespace around inline tags" do 11 | stripped = Sanitise.strip_tags("

Paragraph

Next to paragraph.") 12 | stripped.should eq "Paragraph Next to paragraph." 13 | end 14 | 15 | it "strips content from script, style, etc. tags" do 16 | stripped = Sanitise.strip_tags("Do not want.