├── .gitattributes ├── .gitignore ├── .gitlab └── issue_templates │ └── Bug Report.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Vagrantfile ├── ansible ├── common.yml ├── group_vars │ ├── dev.yml │ └── prod.yml ├── playbook.yml ├── requirements.yml ├── roles │ ├── boussole │ │ ├── defaults │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── boussole.service.jinja2 │ ├── cmark-gfm │ │ └── tasks │ │ │ └── main.yml │ ├── common │ │ └── tasks │ │ │ └── main.yml │ ├── consumers │ │ ├── defaults │ │ │ └── main.yml │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── consumer.service.jinja2 │ ├── cronjobs │ │ └── tasks │ │ │ └── main.yml │ ├── development │ │ ├── files │ │ │ └── ipython_config.py │ │ └── tasks │ │ │ └── main.yml │ ├── gunicorn │ │ ├── files │ │ │ ├── gunicorn_reloader.path │ │ │ └── gunicorn_reloader.service │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ ├── gunicorn.conf.jinja2 │ │ │ ├── gunicorn.service.jinja2 │ │ │ └── gunicorn.socket.jinja2 │ ├── ipv6_networking │ │ ├── defaults │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── java │ │ └── tasks │ │ │ └── main.yml │ ├── nginx │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ └── logrotate │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── nginx.conf.jinja2 │ ├── nginx_prod_config │ │ ├── defaults │ │ │ └── main.yml │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ ├── tildes-shortener.conf.jinja2 │ │ │ └── tildes-static-sites.conf.jinja2 │ ├── nginx_site_config │ │ ├── defaults │ │ │ └── main.yml │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── tildes.conf.jinja2 │ ├── nodejs │ │ └── tasks │ │ │ └── main.yml │ ├── pgbouncer │ │ ├── handlers │ │ │ └── main.yml │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── pgbouncer.ini.jinja2 │ ├── postgresql │ │ ├── defaults │ │ │ └── main.yml │ │ ├── handlers │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── postgresql_plpython │ │ ├── meta │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── postgresql_redis_bridge │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── postgresql_redis_bridge.service.jinja2 │ ├── postgresql_tildes_dbs │ │ ├── defaults │ │ │ └── main.yml │ │ ├── meta │ │ │ └── main.yml │ │ └── tasks │ │ │ ├── database.yml │ │ │ └── main.yml │ ├── prometheus │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ └── prometheus.service │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── prometheus.yml.jinja2 │ ├── prometheus_node_exporter │ │ ├── files │ │ │ └── prometheus_node_exporter.service │ │ └── tasks │ │ │ └── main.yml │ ├── prometheus_postgres_exporter │ │ ├── files │ │ │ ├── prometheus_postgres_exporter.service │ │ │ └── queries.yaml │ │ ├── meta │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── prometheus_redis_exporter │ │ ├── files │ │ │ └── prometheus_redis_exporter.service │ │ ├── meta │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── pts_lbsearch │ │ └── tasks │ │ │ └── main.yml │ ├── python │ │ ├── defaults │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── redis │ │ ├── defaults │ │ │ └── main.yml │ │ ├── files │ │ │ ├── redis.service │ │ │ └── transparent_hugepage.service │ │ ├── handlers │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── redis.conf.jinja2 │ ├── redis_module_cell │ │ ├── meta │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── scripts │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── activate.sh.jinja2 │ ├── self_signed_ssl_cert │ │ ├── meta │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── webassets │ │ ├── meta │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── main.yml │ │ └── templates │ │ │ └── webassets.service.jinja2 │ └── wiki_repo │ │ └── tasks │ │ └── main.yml ├── tasks │ └── prometheus_user.yml └── vars.yml ├── git_hooks ├── pre-commit └── pre-push └── tildes ├── alembic.ini ├── alembic ├── env.py ├── script.py.mako └── versions │ ├── 0435c46f64d8_topic_schedule_add_only_new_top_level_.py │ ├── 04fd898de0db_add_collapse_old_comments_setting.py │ ├── 054aaef690cd_move_user_permissions_to_their_own_table.py │ ├── 09cfb27cc90e_add_scraper_results_table.py │ ├── 19400b1efe8b_add_visit_time_to_topic_visits_primary_.py │ ├── 1996feae620d_change_troll_and_flame_comment_tags_to_.py │ ├── 1ade2bf86efc_comment_tags_add_reason_column.py │ ├── 20b5f07e5f80_add_topic_schedule_table.py │ ├── 22a8ed36a3c9_send_rabbitmq_message_on_new_scraper_.py │ ├── 24014adda7c3_add_topic_link_edit_to_logeventtype.py │ ├── 2512581c91b3_add_setting_to_open_links_in_new_tab.py │ ├── 28d7ce2c4825_use_enum_for_user_permissions.py │ ├── 34753d8124b4_prevent_inserting_subsequent_topic_.py │ ├── 347859b0355e_added_account_default_theme_setting.py │ ├── 380a76d4a722_user_add_banned_time.py │ ├── 3f83028d1673_add_user_bio_column.py │ ├── 3fbddcba0e3b_add_comment_remove_and_comment_unremove_.py │ ├── 4241b0202fd4_add_setting_to_open_group_and_user_.py │ ├── 468cf81f4a6b_topic_schedule_add_latest_topic_id.py │ ├── 4d352e61a468_add_is_voting_closed_to_comments_.py │ ├── 4d86b372a8db_user_add_ban_expiry_time.py │ ├── 4e101aae77cd_add_topic_ignores.py │ ├── 4ebc3ca32b48_send_rabbitmq_message_on_link_edit.py │ ├── 4fb2c786c7a0_add_new_notify_triggers.py │ ├── 50c251c4a19c_add_search_column_index_for_topics.py │ ├── 51a1012f4f63_add_comment_sort_order_account_setting.py │ ├── 53567981cdf4_add_topic_and_comment_bookmark_tables.py │ ├── 53f81a72f076_group_add_common_topic_tags.py │ ├── 55f4c1f951d5_add_group_scripts_table.py │ ├── 5a7dc1032efc_add_tags_to_topic_search_vector.py │ ├── 5cd2db18b722_rename_comment_tags_to_labels.py │ ├── 61f43e57679a_add_youtube_scraper_result.py │ ├── 679090fd4977_add_search_column_index_for_comments.py │ ├── 67e332481a6e_add_two_factor_authentication.py │ ├── 6a635773de8f_add_comment_post_to_logeventtype.py │ ├── 6c840340ab86_drop_track_comment_visits_column_on_.py │ ├── 6ede05a0ea23_topic_add_original_url_column.py │ ├── 7ac1aad64144_group_add_is_user_treated_as_topic_.py │ ├── 82e9801eb2d6_update_post_topic_permission_to_topic_.py │ ├── 8326f8cc5ddd_topic_drop_original_url_column.py │ ├── 84dc19f6e876_rename_column_for_restricted_posting_.py │ ├── 879588c5729d_add_financials_table.py │ ├── 8e54f422541c_user_track_last_usage_of_exemplary_label.py │ ├── 9148909b78e9_add_group_stats_table.py │ ├── 9b7a7b906956_shorten_topic_re_visit_grace_period.py │ ├── 9b88cb0a7b2c_add_groupwikipage.py │ ├── 9fc0033a2b61_topic_add_schedule_id_column.py │ ├── a0e0b6206146_users_add_is_deleted_deleted_time.py │ ├── a1708d376252_drop_topics_removed_time_column.py │ ├── a195ddbb4be6_add_solarized_prefix_to_default_themes.py │ ├── a2fda5d4e058_add_logeventtype_values_for_voting.py │ ├── afa3128a9b54_add_exemplary_comment_tag.py │ ├── b3be50625592_add_log_comments_table.py │ ├── b424479202f9_drop_removed_time_column_from_comments.py │ ├── b761d0185ca0_groups_add_important_topic_tags.py │ ├── b825165870d9_add_weights_to_comment_tags.py │ ├── b9d9ae4c2286_add_comment_excerpt.py │ ├── bcf1406bb6c5_add_admin_tool_for_removing_topics.py │ ├── beaa57144e49_comment_labels_send_rabbitmq_message_on_.py │ ├── cc12ea6c616d_drop_rabbitmq_functions_triggers.py │ ├── cddd7d7ed0ea_add_interesting_activity_topic_sorting.py │ ├── d33fb803a153_switch_to_general_permissions_column.py │ ├── d56e71257a86_add_tag_related_user_settings.py │ ├── de83b8750123_add_setting_to_open_text_links_in_new_.py │ ├── e9bbc2929d9c_group_add_sidebar_markdown_html.py │ ├── f1ecbf24c212_added_user_tag_type_comment_notification.py │ ├── f20ce28b1d5c_rename_group_wiki_pages_slug_to_path.py │ ├── f4e1ef359307_extend_topic_indexes_for_keyset_.py │ ├── fa14e9f5ebe5_add_user_rate_limit_table.py │ ├── fab922a8bb04_update_comment_triggers_for_removals.py │ ├── fe91222503ef_financials_drop_is_approximate_column.py │ └── fef2c9c9a186_user_add_interact_mark_notifications_.py ├── boussole.yaml ├── consumers ├── comment_user_mentions_generator.py ├── post_processing_script_runner.py ├── site_icon_downloader.py ├── topic_embedly_extractor.py ├── topic_interesting_activity_updater.py ├── topic_metadata_generator.py └── topic_youtube_scraper.py ├── development.ini ├── gunicorn_config.py ├── lua └── sandbox.lua ├── mypy.ini ├── package-lock.json ├── package.json ├── production.ini.example ├── prospector.yaml ├── pyproject.toml ├── pytest.ini ├── requirements-dev.in ├── requirements-dev.txt ├── requirements.in ├── requirements.txt ├── scripts ├── __init__.py ├── backup_database.py ├── clean_private_data.py ├── close_voting_on_old_posts.py ├── generate_group_stats_for_yesterday.py ├── generate_site_icons_css.py ├── initialize_db.py ├── lift_expired_temporary_bans.py ├── post_scheduled_topics.py ├── postgresql_redis_bridge.py └── update_groups_common_topic_tags.py ├── scss ├── _base.scss ├── _functions.scss ├── _layout.scss ├── _mixins.scss ├── _placeholders.scss ├── _spectre_variables.scss ├── _variables.scss ├── modules │ ├── _breadcrumbs.scss │ ├── _btn.scss │ ├── _chip.scss │ ├── _comment.scss │ ├── _divider.scss │ ├── _donation.scss │ ├── _dropdown.scss │ ├── _empty.scss │ ├── _form.scss │ ├── _group.scss │ ├── _heading.scss │ ├── _input.scss │ ├── _label.scss │ ├── _link.scss │ ├── _listing.scss │ ├── _logged-in-user.scss │ ├── _menu.scss │ ├── _message.scss │ ├── _nav.scss │ ├── _pagination.scss │ ├── _post.scss │ ├── _settings.scss │ ├── _sidebar.scss │ ├── _site-footer.scss │ ├── _site-header.scss │ ├── _static-site.scss │ ├── _syntax-highlighting.scss │ ├── _tab.scss │ ├── _table.scss │ ├── _text.scss │ ├── _theme-preview.scss │ ├── _time.scss │ ├── _toast.scss │ └── _topic.scss ├── spectre-0.5.1 │ ├── _accordions.scss │ ├── _animations.scss │ ├── _asian.scss │ ├── _autocomplete.scss │ ├── _avatars.scss │ ├── _badges.scss │ ├── _bars.scss │ ├── _base.scss │ ├── _breadcrumbs.scss │ ├── _buttons.scss │ ├── _calendars.scss │ ├── _cards.scss │ ├── _carousels.scss │ ├── _chips.scss │ ├── _codes.scss │ ├── _comparison-sliders.scss │ ├── _dropdowns.scss │ ├── _empty.scss │ ├── _filters.scss │ ├── _forms.scss │ ├── _icons.scss │ ├── _labels.scss │ ├── _layout.scss │ ├── _media.scss │ ├── _menus.scss │ ├── _meters.scss │ ├── _mixins.scss │ ├── _modals.scss │ ├── _navbar.scss │ ├── _navs.scss │ ├── _normalize.scss │ ├── _off-canvas.scss │ ├── _pagination.scss │ ├── _panels.scss │ ├── _parallax.scss │ ├── _popovers.scss │ ├── _progress.scss │ ├── _sliders.scss │ ├── _steps.scss │ ├── _tables.scss │ ├── _tabs.scss │ ├── _tiles.scss │ ├── _timelines.scss │ ├── _toasts.scss │ ├── _tooltips.scss │ ├── _typography.scss │ ├── _utilities.scss │ ├── _variables.scss │ ├── icons │ │ ├── _icons-action.scss │ │ ├── _icons-core.scss │ │ ├── _icons-navigation.scss │ │ └── _icons-object.scss │ ├── mixins │ │ ├── _avatar.scss │ │ ├── _button.scss │ │ ├── _clearfix.scss │ │ ├── _color.scss │ │ ├── _label.scss │ │ ├── _position.scss │ │ ├── _shadow.scss │ │ ├── _text.scss │ │ ├── _toast.scss │ │ └── _transition.scss │ ├── spectre-exp.scss │ ├── spectre-icons.scss │ ├── spectre.scss │ └── utilities │ │ ├── _colors.scss │ │ ├── _cursors.scss │ │ ├── _display.scss │ │ ├── _divider.scss │ │ ├── _loading.scss │ │ ├── _position.scss │ │ ├── _shapes.scss │ │ └── _text.scss ├── styles.scss └── themes │ ├── _atom_one_dark.scss │ ├── _black.scss │ ├── _default.scss │ ├── _dracula.scss │ ├── _gruvbox.scss │ ├── _love.scss │ ├── _solarized.scss │ ├── _theme_mixins.scss │ └── _zenburn.scss ├── setup.py ├── sql └── init │ ├── functions │ ├── event_stream.sql │ └── utils.sql │ ├── insert_base_data.sql │ └── triggers │ ├── comment_labels │ ├── event_stream.sql │ └── users.sql │ ├── comment_notifications │ ├── topic_visits.sql │ └── users.sql │ ├── comment_votes │ └── comments.sql │ ├── comments │ ├── comment_notifications.sql │ ├── comments.sql │ ├── event_stream.sql │ ├── topic_visits.sql │ └── topics.sql │ ├── group_subscriptions │ └── groups.sql │ ├── message_conversations │ └── users.sql │ ├── message_replies │ └── message_conversations.sql │ ├── scraper_results │ └── event_stream.sql │ ├── topic_visits │ └── topic_visits.sql │ ├── topic_votes │ └── topics.sql │ ├── topics │ ├── event_stream.sql │ ├── topic_schedule.sql │ └── topics.sql │ └── users │ └── users.sql ├── static ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── images │ ├── site-icons │ │ └── README │ └── tildes-logo-144x144.png ├── js │ ├── behaviors │ │ ├── auto-focus.js │ │ ├── autocomplete-chip-clear.js │ │ ├── autocomplete-input.js │ │ ├── autocomplete-menu-item.js │ │ ├── autocomplete-menu.js │ │ ├── autoselect-input.js │ │ ├── autosubmit-on-change.js │ │ ├── cancel-button.js │ │ ├── comment-collapse-all-button.js │ │ ├── comment-collapse-button.js │ │ ├── comment-collapse-read-button.js │ │ ├── comment-expand-all-button.js │ │ ├── comment-label-button.js │ │ ├── comment-parent-button.js │ │ ├── comment-reply-form.js │ │ ├── confirm-leave-page-unsaved.js │ │ ├── copy-button.js │ │ ├── ctrl-enter-submit-form.js │ │ ├── dropdown-toggle.js │ │ ├── external-links-new-tabs.js │ │ ├── fadeout-parent-on-success.js │ │ ├── group-links-new-tabs.js │ │ ├── hide-sidebar-if-open.js │ │ ├── hide-sidebar-no-preventdefault.js │ │ ├── markdown-edit-tab.js │ │ ├── markdown-preview-tab.js │ │ ├── prevent-double-submit.js │ │ ├── remove-on-click.js │ │ ├── remove-on-success.js │ │ ├── sidebar-toggle.js │ │ ├── stripe-checkout.js │ │ ├── stripe-donate-form.js │ │ ├── tab.js │ │ ├── theme-preview.js │ │ ├── theme-selector.js │ │ ├── time-period-select.js │ │ └── user-links-new-tabs.js │ ├── scripts.js │ └── third_party │ │ ├── areyousure-1.9.0.js │ │ ├── intercooler-1.0.3.min.js │ │ ├── jquery-3.1.1.min.js │ │ └── onmount-1.3.0.js ├── manifest.json ├── mstile-150x150.png ├── robots.txt └── safari-pinned-tab.svg ├── tasks.py ├── tests ├── __init__.py ├── conftest.py ├── fixtures.py ├── test_comment.py ├── test_comment_user_mentions.py ├── test_datetime.py ├── test_group.py ├── test_hash.py ├── test_html.py ├── test_id.py ├── test_markdown.py ├── test_markdown_field.py ├── test_messages.py ├── test_metrics.py ├── test_ratelimit.py ├── test_scraper.py ├── test_simplestring_field.py ├── test_string.py ├── test_title.py ├── test_topic.py ├── test_topic_permissions.py ├── test_topic_tags.py ├── test_triggers_comments.py ├── test_url.py ├── test_url_transform.py ├── test_user.py ├── test_username.py ├── test_webassets.py └── webtests │ ├── test_login.py │ ├── test_user_settings.py │ └── test_w3_validator.py ├── tildes ├── __init__.py ├── api.py ├── auth.py ├── database.py ├── database_models.py ├── enums.py ├── jinja.py ├── json.py ├── lib │ ├── __init__.py │ ├── auth.py │ ├── cmark.py │ ├── database.py │ ├── datetime.py │ ├── event_stream.py │ ├── hash.py │ ├── html.py │ ├── id.py │ ├── lua.py │ ├── markdown.py │ ├── password.py │ ├── ratelimit.py │ ├── site_info.py │ ├── string.py │ ├── url.py │ └── url_transform.py ├── metrics.py ├── models │ ├── __init__.py │ ├── comment │ │ ├── __init__.py │ │ ├── comment.py │ │ ├── comment_bookmark.py │ │ ├── comment_label.py │ │ ├── comment_notification.py │ │ ├── comment_notification_query.py │ │ ├── comment_query.py │ │ ├── comment_tree.py │ │ └── comment_vote.py │ ├── database_model.py │ ├── financials.py │ ├── group │ │ ├── __init__.py │ │ ├── group.py │ │ ├── group_query.py │ │ ├── group_script.py │ │ ├── group_stat.py │ │ ├── group_subscription.py │ │ └── group_wiki_page.py │ ├── log │ │ ├── __init__.py │ │ └── log.py │ ├── message │ │ ├── __init__.py │ │ └── message.py │ ├── model_query.py │ ├── pagination.py │ ├── scraper │ │ ├── __init__.py │ │ └── scraper_result.py │ ├── scripting.py │ ├── topic │ │ ├── __init__.py │ │ ├── topic.py │ │ ├── topic_bookmark.py │ │ ├── topic_ignore.py │ │ ├── topic_query.py │ │ ├── topic_schedule.py │ │ ├── topic_visit.py │ │ └── topic_vote.py │ └── user │ │ ├── __init__.py │ │ ├── user.py │ │ ├── user_group_settings.py │ │ ├── user_invite_code.py │ │ ├── user_permissions.py │ │ └── user_rate_limit.py ├── request_methods.py ├── resources │ ├── __init__.py │ ├── comment.py │ ├── group.py │ ├── message.py │ ├── topic.py │ └── user.py ├── routes.py ├── schemas │ ├── __init__.py │ ├── comment.py │ ├── fields.py │ ├── group.py │ ├── group_wiki_page.py │ ├── listing.py │ ├── message.py │ ├── topic.py │ └── user.py ├── scrapers │ ├── __init__.py │ ├── embedly_scraper.py │ ├── exceptions.py │ └── youtube_scraper.py ├── settings.py ├── templates │ ├── base.atom.jinja2 │ ├── base.jinja2 │ ├── base.rss.jinja2 │ ├── base_no_sidebar.jinja2 │ ├── base_settings.jinja2 │ ├── base_user_menu.jinja2 │ ├── bookmarks.jinja2 │ ├── donate_stripe.jinja2 │ ├── donate_stripe_redirect.jinja2 │ ├── donate_success.jinja2 │ ├── error_group_not_found.jinja2 │ ├── error_page.jinja2 │ ├── financials.jinja2 │ ├── group_wiki.jinja2 │ ├── group_wiki_edit_page.jinja2 │ ├── group_wiki_new_page.jinja2 │ ├── group_wiki_page.jinja2 │ ├── groups.jinja2 │ ├── home.atom.jinja2 │ ├── home.jinja2 │ ├── home.rss.jinja2 │ ├── ignored_topics.jinja2 │ ├── includes │ │ ├── new_topic_form.jinja2 │ │ ├── password_restrictions.jinja2 │ │ ├── topic_tags.jinja2 │ │ └── wiki_editing_notes.jinja2 │ ├── intercooler │ │ ├── comment_contents.jinja2 │ │ ├── comment_edit.jinja2 │ │ ├── comment_reply.jinja2 │ │ ├── group_subscription_box.jinja2 │ │ ├── invite_code.jinja2 │ │ ├── login_two_factor.jinja2 │ │ ├── markdown_preview.jinja2 │ │ ├── markdown_source.jinja2 │ │ ├── post_action_toggle_button.jinja2 │ │ ├── single_comment.jinja2 │ │ ├── single_message.jinja2 │ │ ├── topic_contents.jinja2 │ │ ├── topic_edit.jinja2 │ │ ├── topic_group_edit.jinja2 │ │ ├── topic_link_edit.jinja2 │ │ ├── topic_tags.jinja2 │ │ ├── topic_tags_edit.jinja2 │ │ ├── topic_title_edit.jinja2 │ │ ├── topic_voting.jinja2 │ │ ├── two_factor_backup_codes.jinja2 │ │ ├── two_factor_disabled.jinja2 │ │ └── two_factor_enabled.jinja2 │ ├── invite.jinja2 │ ├── login.jinja2 │ ├── macros │ │ ├── buttons.jinja2 │ │ ├── comments.jinja2 │ │ ├── datetime.jinja2 │ │ ├── donation_goal.jinja2 │ │ ├── forms.jinja2 │ │ ├── groups.jinja2 │ │ ├── links.jinja2 │ │ ├── messages.jinja2 │ │ ├── topics.jinja2 │ │ ├── user.jinja2 │ │ ├── user_menu.jinja2 │ │ └── utils.jinja2 │ ├── message_conversation.jinja2 │ ├── messages.jinja2 │ ├── messages_sent.jinja2 │ ├── messages_unread.jinja2 │ ├── new_message.jinja2 │ ├── new_topic.jinja2 │ ├── notifications.jinja2 │ ├── notifications_unread.jinja2 │ ├── register.jinja2 │ ├── search.jinja2 │ ├── settings.jinja2 │ ├── settings_account_recovery.jinja2 │ ├── settings_bio.jinja2 │ ├── settings_filters.jinja2 │ ├── settings_password_change.jinja2 │ ├── settings_theme_previews.jinja2 │ ├── settings_two_factor.jinja2 │ ├── topic.jinja2 │ ├── topic_listing.atom.jinja2 │ ├── topic_listing.jinja2 │ ├── topic_listing.rss.jinja2 │ ├── user.jinja2 │ ├── user_search.jinja2 │ └── votes.jinja2 ├── tweens.py ├── typing.py └── views │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── v0 │ │ ├── __init__.py │ │ ├── group.py │ │ ├── topic.py │ │ └── user.py │ └── web │ │ ├── __init__.py │ │ ├── comment.py │ │ ├── exceptions.py │ │ ├── group.py │ │ ├── markdown_preview.py │ │ ├── message.py │ │ ├── topic.py │ │ └── user.py │ ├── bookmarks.py │ ├── decorators.py │ ├── donate.py │ ├── exceptions.py │ ├── financials.py │ ├── group.py │ ├── group_wiki_page.py │ ├── ignored_topics.py │ ├── login.py │ ├── message.py │ ├── metrics.py │ ├── notifications.py │ ├── register.py │ ├── settings.py │ ├── shortener.py │ ├── topic.py │ ├── user.py │ └── votes.py └── webassets.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | Customfile 3 | 4 | __pycache__ 5 | *.py[co] 6 | 7 | pip-wheel-metadata/ 8 | *.egg-info/ 9 | 10 | .cache/ 11 | .mypy_cache/ 12 | .webassets-cache/ 13 | .webassets-manifest 14 | 15 | *.gz 16 | *.log 17 | 18 | # don't track the built versions of CSS and JS 19 | tildes/static/css/* 20 | tildes/static/js/third_party.js 21 | tildes/static/js/tildes.js 22 | 23 | # don't track site icon files 24 | tildes/static/images/site-icons/*.png 25 | 26 | # NodeJS dependencies 27 | tildes/node_modules/ 28 | -------------------------------------------------------------------------------- /ansible/common.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | become: true 4 | 5 | vars_files: 6 | - vars.yml 7 | 8 | roles: 9 | - common 10 | -------------------------------------------------------------------------------- /ansible/group_vars/dev.yml: -------------------------------------------------------------------------------- 1 | --- 2 | app_username: vagrant 3 | pip_requirements_filename: requirements-dev.txt 4 | ini_file: development.ini 5 | 6 | gunicorn_args: --reload 7 | 8 | # have to disable sendfile for vagrant due to a virtualbox bug 9 | nginx_enable_sendfile: false 10 | nginx_worker_processes: 1 11 | 12 | nginx_enable_hsts: false 13 | nginx_enable_csp: false 14 | 15 | postgresql_tildes_databases: 16 | - tildes 17 | - tildes_test 18 | postgresql_tildes_user_flags: "SUPERUSER" 19 | tildes_database_insert_dev_data: true 20 | 21 | hsts_max_age: 60 22 | 23 | site_hostname: localhost 24 | 25 | ssl_cert_dir: /etc/pki/tls/certs 26 | ssl_cert_path: "{{ ssl_cert_dir }}/localhost.crt" 27 | ssl_private_key_path: "{{ ssl_cert_dir }}/localhost.key" 28 | 29 | ansible_python_interpreter: /usr/bin/python3 30 | 31 | # Workaround for some Ansible permissions issues when becoming an unprivileged user 32 | # (this has some risks, but should be fine for our use) 33 | ansible_shell_allow_world_readable_temp: true 34 | -------------------------------------------------------------------------------- /ansible/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | become: true 4 | vars_files: 5 | - vars.yml 6 | roles: 7 | - common 8 | 9 | - hosts: app_server 10 | become: true 11 | vars_files: 12 | - vars.yml 13 | roles: 14 | - cmark-gfm 15 | - pts_lbsearch 16 | - python 17 | - gunicorn 18 | - nginx 19 | - nginx_site_config 20 | - postgresql 21 | - postgresql_plpython 22 | - postgresql_tildes_dbs 23 | - pgbouncer 24 | - redis 25 | - redis_module_cell 26 | - postgresql_redis_bridge 27 | - boussole 28 | - webassets 29 | - scripts 30 | - prometheus_node_exporter 31 | - prometheus_postgres_exporter 32 | - prometheus_redis_exporter 33 | - consumers 34 | - cronjobs 35 | - wiki_repo 36 | 37 | - hosts: dev 38 | become: true 39 | vars_files: 40 | - vars.yml 41 | roles: 42 | - self_signed_ssl_cert 43 | - prometheus 44 | - java 45 | - nodejs 46 | - development 47 | 48 | - hosts: prod 49 | become: true 50 | vars_files: 51 | - vars.yml 52 | roles: 53 | - nginx_prod_config 54 | - ipv6_networking 55 | -------------------------------------------------------------------------------- /ansible/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - community.general 4 | -------------------------------------------------------------------------------- /ansible/roles/boussole/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | boussole_venv_dir: /opt/venvs/boussole 3 | -------------------------------------------------------------------------------- /ansible/roles/boussole/templates/boussole.service.jinja2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Boussole - auto-compile SCSS files on change 3 | 4 | [Service] 5 | User={{ app_username }} 6 | Group={{ app_username }} 7 | WorkingDirectory={{ app_dir }} 8 | Environment="LC_ALL=C.UTF-8" "LANG=C.UTF-8" 9 | ExecStart={{ boussole_venv_dir }}/bin/boussole watch --backend=yaml --config=boussole.yaml --poll 10 | Restart=always 11 | RestartSec=5 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ansible/roles/common/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set time zone to UTC 3 | community.general.timezone: 4 | name: Etc/UTC 5 | 6 | - name: Create group for app user 7 | group: 8 | name: "{{ app_username }}" 9 | 10 | - name: Create app user 11 | user: 12 | name: "{{ app_username }}" 13 | group: "{{ app_username }}" 14 | -------------------------------------------------------------------------------- /ansible/roles/consumers/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | consumers: 3 | - comment_user_mentions_generator 4 | - post_processing_script_runner 5 | - topic_interesting_activity_updater 6 | - topic_metadata_generator 7 | -------------------------------------------------------------------------------- /ansible/roles/consumers/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: python 4 | - role: redis 5 | -------------------------------------------------------------------------------- /ansible/roles/consumers/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Set up service files for background consumers 3 | template: 4 | src: "consumer.service.jinja2" 5 | dest: /etc/systemd/system/consumer-{{ item }}.service 6 | owner: root 7 | group: root 8 | mode: 0644 9 | loop: "{{ consumers }}" 10 | 11 | - name: Start and enable all consumer services 12 | service: 13 | name: consumer-{{ item }} 14 | state: started 15 | enabled: true 16 | loop: "{{ consumers }}" 17 | -------------------------------------------------------------------------------- /ansible/roles/consumers/templates/consumer.service.jinja2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description={{ item.replace("_", " ").title() }} (Queue Consumer) 3 | Requires=redis.service 4 | After=redis.service 5 | PartOf=redis.service 6 | 7 | [Service] 8 | User={{ app_username }} 9 | Group={{ app_username }} 10 | WorkingDirectory={{ app_dir }}/consumers 11 | Environment="INI_FILE={{ app_dir }}/{{ ini_file }}" 12 | ExecStart={{ bin_dir }}/python {{ item }}.py 13 | Restart=always 14 | RestartSec=5 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /ansible/roles/development/files/ipython_config.py: -------------------------------------------------------------------------------- 1 | c.InteractiveShellApp.extensions = ['autoreload'] 2 | c.InteractiveShellApp.exec_lines = ['%autoreload 2'] 3 | -------------------------------------------------------------------------------- /ansible/roles/development/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create IPython profile 3 | become_user: "{{ app_username }}" 4 | command: 5 | cmd: "{{ bin_dir }}/ipython profile create" 6 | creates: /home/{{ app_username }}/.ipython/profile_default 7 | 8 | - name: Create IPython config file 9 | copy: 10 | src: ipython_config.py 11 | dest: /home/{{ app_username }}/.ipython/profile_default/ipython_config.py 12 | owner: "{{ app_username }}" 13 | group: "{{ app_username }}" 14 | mode: 0744 15 | 16 | - name: Automatically activate venv on login and in new shells 17 | lineinfile: 18 | path: /home/{{ app_username }}/.bashrc 19 | line: source activate 20 | owner: "{{ app_username }}" 21 | group: "{{ app_username }}" 22 | 23 | - name: Add invoke's tab-completion script to support completing invoke task names 24 | lineinfile: 25 | path: /home/{{ app_username }}/.bashrc 26 | line: source <(invoke --print-completion-script bash) 27 | -------------------------------------------------------------------------------- /ansible/roles/gunicorn/files/gunicorn_reloader.path: -------------------------------------------------------------------------------- 1 | [Path] 2 | PathChanged=/opt/tildes/static/css/site-icons.css 3 | 4 | [Install] 5 | WantedBy=multi-user.target 6 | -------------------------------------------------------------------------------- /ansible/roles/gunicorn/files/gunicorn_reloader.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=gunicorn reloader 3 | After=network.target 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=/bin/systemctl reload gunicorn.service 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /ansible/roles/gunicorn/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: python 4 | -------------------------------------------------------------------------------- /ansible/roles/gunicorn/templates/gunicorn.conf.jinja2: -------------------------------------------------------------------------------- 1 | d /run/gunicorn 0755 {{ app_username }} {{ app_username }} - 2 | -------------------------------------------------------------------------------- /ansible/roles/gunicorn/templates/gunicorn.service.jinja2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=gunicorn daemon 3 | Requires=gunicorn.socket 4 | After=network.target 5 | 6 | [Service] 7 | PIDFile=/run/gunicorn/pid 8 | User={{ app_username }} 9 | Group={{ app_username }} 10 | RuntimeDirectory=gunicorn 11 | WorkingDirectory={{ app_dir }} 12 | ExecStart={{ bin_dir }}/gunicorn --paste {{ ini_file }} --config {{ app_dir }}/gunicorn_config.py --bind unix:/run/gunicorn/socket --pid /run/gunicorn/pid {{ gunicorn_args }} 13 | ExecReload=/bin/kill -s HUP $MAINPID 14 | ExecStop=/bin/kill -s TERM $MAINPID 15 | PrivateTmp=true 16 | Environment=PROMETHEUS_MULTIPROC_DIR=/tmp 17 | Restart=always 18 | RestartSec=30 19 | 20 | [Install] 21 | WantedBy=multi-user.target 22 | -------------------------------------------------------------------------------- /ansible/roles/gunicorn/templates/gunicorn.socket.jinja2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=gunicorn socket 3 | PartOf=gunicorn.service 4 | 5 | [Socket] 6 | ListenStream=/run/gunicorn/socket 7 | SocketUser={{ app_username }} 8 | SocketGroup={{ app_username }} 9 | 10 | [Install] 11 | WantedBy=sockets.target 12 | -------------------------------------------------------------------------------- /ansible/roles/ipv6_networking/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ipv6_device: eth0 3 | -------------------------------------------------------------------------------- /ansible/roles/ipv6_networking/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Enable IPv6 networking 3 | blockinfile: 4 | path: /etc/network/interfaces 5 | block: | 6 | iface {{ ipv6_device }} inet6 static 7 | address {{ ipv6_address }} 8 | netmask 64 9 | 10 | post-up sleep 5; /sbin/ip -family inet6 route add {{ ipv6_gateway }} dev {{ ipv6_device }} 11 | post-up sleep 5; /sbin/ip -family inet6 route add default via {{ ipv6_gateway }} 12 | pre-down /sbin/ip -family inet6 route del default via {{ ipv6_gateway }} 13 | pre-down /sbin/ip -family inet6 route del {{ ipv6_gateway }} dev {{ ipv6_device }} 14 | 15 | # apt seems to hang a lot when using IPv6 16 | - name: Force apt not to use IPv6 17 | lineinfile: 18 | path: /etc/apt/apt.conf.d/99force-ipv4 19 | line: Acquire::ForceIPv4 "true"; 20 | create: true 21 | owner: root 22 | group: root 23 | mode: 0644 24 | -------------------------------------------------------------------------------- /ansible/roles/java/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install OpenJDK Java runtime 3 | apt: 4 | name: openjdk-11-jre 5 | -------------------------------------------------------------------------------- /ansible/roles/nginx/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nginx_enable_sendfile: true 3 | nginx_worker_processes: auto 4 | 5 | ssl_cert_path: /etc/letsencrypt/live/{{ site_hostname }}/fullchain.pem 6 | ssl_private_key_path: /etc/letsencrypt/live/{{ site_hostname }}/privkey.pem 7 | -------------------------------------------------------------------------------- /ansible/roles/nginx/files/logrotate: -------------------------------------------------------------------------------- 1 | # rotate nginx log files daily and delete after 30 days 2 | /var/log/nginx/*.log { 3 | daily 4 | missingok 5 | rotate 30 6 | compress 7 | delaycompress 8 | notifempty 9 | create 640 nginx adm 10 | sharedscripts 11 | postrotate 12 | if [ -f /var/run/nginx.pid ]; then 13 | kill -USR1 `cat /var/run/nginx.pid` 14 | fi 15 | endscript 16 | } 17 | -------------------------------------------------------------------------------- /ansible/roles/nginx/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reload nginx 3 | service: 4 | name: nginx 5 | state: reloaded 6 | -------------------------------------------------------------------------------- /ansible/roles/nginx_prod_config/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hsts_max_age: 63072000 3 | static_sites_dir: /opt/tildes-static-sites 4 | -------------------------------------------------------------------------------- /ansible/roles/nginx_prod_config/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: nginx 4 | - role: gunicorn 5 | -------------------------------------------------------------------------------- /ansible/roles/nginx_prod_config/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add shortener config file 3 | template: 4 | src: tildes-shortener.conf.jinja2 5 | dest: /etc/nginx/sites-available/tildes-shortener.conf 6 | owner: root 7 | group: root 8 | mode: 0644 9 | 10 | - name: Enable shortener in nginx 11 | file: 12 | path: /etc/nginx/sites-enabled/tildes-shortener.conf 13 | src: /etc/nginx/sites-available/tildes-shortener.conf 14 | state: link 15 | owner: root 16 | group: root 17 | mode: 0644 18 | notify: 19 | - Reload nginx 20 | 21 | - name: Add static sites config file 22 | template: 23 | src: tildes-static-sites.conf.jinja2 24 | dest: /etc/nginx/sites-available/tildes-static-sites.conf 25 | owner: root 26 | group: root 27 | mode: 0644 28 | 29 | - name: Enable static sites in nginx 30 | file: 31 | path: /etc/nginx/sites-enabled/tildes-static-sites.conf 32 | src: /etc/nginx/sites-available/tildes-static-sites.conf 33 | state: link 34 | owner: root 35 | group: root 36 | mode: 0644 37 | notify: 38 | - Reload nginx 39 | -------------------------------------------------------------------------------- /ansible/roles/nginx_prod_config/templates/tildes-shortener.conf.jinja2: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl http2; 3 | listen [::]:443 ssl http2; 4 | 5 | server_name tild.es; 6 | 7 | keepalive_timeout 5; 8 | 9 | add_header Strict-Transport-Security "max-age={{ hsts_max_age }}; includeSubDomains; preload" always; 10 | 11 | # Are these security headers unnecessary when we're just redirecting? 12 | add_header X-Content-Type-Options "nosniff" always; 13 | add_header X-Frame-Options "SAMEORIGIN" always; 14 | add_header X-Xss-Protection "1; mode=block" always; 15 | add_header Referrer-Policy "same-origin" always; 16 | 17 | # Exact location match to redirect the root url to tildes.net 18 | location = / { 19 | return 301 https://tildes.net; 20 | } 21 | 22 | # Serve the same robots.txt file as on the site itself 23 | location = /robots.txt { 24 | root {{ app_dir }}/static; 25 | } 26 | 27 | # Will match all addresses *except* exact matches above 28 | location / { 29 | # Strip any trailing slash while redirecting 30 | rewrite ^/(.*)/?$ https://tildes.net/shortener/$1 permanent; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ansible/roles/nginx_site_config/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nginx_enable_hsts: true 3 | nginx_enable_csp: true 4 | nginx_enable_ratelimiting: true 5 | nginx_redirect_www: true 6 | 7 | prometheus_ips: 8 | - 127.0.0.1 9 | - ::1 10 | 11 | hsts_max_age: 63072000 12 | -------------------------------------------------------------------------------- /ansible/roles/nginx_site_config/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: nginx 4 | - role: gunicorn 5 | -------------------------------------------------------------------------------- /ansible/roles/nginx_site_config/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add site config file 3 | template: 4 | src: tildes.conf.jinja2 5 | dest: /etc/nginx/sites-available/tildes.conf 6 | owner: root 7 | group: root 8 | mode: 0644 9 | notify: 10 | - Reload nginx 11 | 12 | - name: Enable site in nginx 13 | file: 14 | path: /etc/nginx/sites-enabled/tildes.conf 15 | src: /etc/nginx/sites-available/tildes.conf 16 | state: link 17 | owner: root 18 | group: root 19 | mode: 0644 20 | notify: 21 | - Reload nginx 22 | -------------------------------------------------------------------------------- /ansible/roles/nodejs/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add APT key for NodeSource Node.js repository 3 | apt_key: 4 | url: https://deb.nodesource.com/gpgkey/nodesource.gpg.key 5 | 6 | - name: Add NodeSource Node.js APT repository 7 | apt_repository: 8 | repo: deb https://deb.nodesource.com/node_14.x buster main 9 | 10 | - name: Install Node.js 11 | apt: 12 | name: nodejs 13 | 14 | - name: Install npm packages defined in package.json 15 | become_user: "{{ app_username }}" 16 | community.general.npm: 17 | path: "{{ app_dir }}" 18 | # --no-bin-links option is needed to prevent npm from creating symlinks in the .bin 19 | # directory, which doesn't work inside Vagrant on Windows 20 | no_bin_links: true 21 | -------------------------------------------------------------------------------- /ansible/roles/pgbouncer/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reload pgbouncer 3 | service: 4 | name: pgbouncer 5 | state: reloaded 6 | -------------------------------------------------------------------------------- /ansible/roles/pgbouncer/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: postgresql 4 | -------------------------------------------------------------------------------- /ansible/roles/pgbouncer/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install pgbouncer 3 | apt: 4 | name: pgbouncer 5 | 6 | - name: Add pgbouncer.ini 7 | template: 8 | src: pgbouncer.ini.jinja2 9 | dest: /etc/pgbouncer/pgbouncer.ini 10 | owner: postgres 11 | group: postgres 12 | mode: 0640 13 | notify: 14 | - Reload pgbouncer 15 | 16 | - name: Add user to pgbouncer userlist 17 | lineinfile: 18 | path: /etc/pgbouncer/userlist.txt 19 | line: '"tildes" ""' 20 | create: true 21 | owner: postgres 22 | group: postgres 23 | mode: 0640 24 | notify: 25 | - Reload pgbouncer 26 | 27 | - name: Start and enable pgbouncer service 28 | service: 29 | name: pgbouncer 30 | state: started 31 | enabled: true 32 | -------------------------------------------------------------------------------- /ansible/roles/pgbouncer/templates/pgbouncer.ini.jinja2: -------------------------------------------------------------------------------- 1 | [databases] 2 | 3 | [pgbouncer] 4 | logfile = /var/log/postgresql/pgbouncer.log 5 | log_connections = 0 6 | log_disconnections = 0 7 | 8 | pidfile = /var/run/postgresql/pgbouncer.pid 9 | 10 | listen_port = 6432 11 | 12 | unix_socket_dir = /var/run/postgresql 13 | 14 | auth_type = hba 15 | auth_file = /etc/pgbouncer/userlist.txt 16 | auth_hba_file = /etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf 17 | 18 | pool_mode = transaction 19 | -------------------------------------------------------------------------------- /ansible/roles/postgresql/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | postgresql_version: 13 3 | 4 | # Users of this role can define postgresql_settings, which will be merged with 5 | # this base _postgresql_settings 6 | _postgresql_settings: 7 | lock_timeout: 5000 8 | statement_timeout: 5000 9 | idle_in_transaction_session_timeout: 600000 10 | timezone: "'UTC'" 11 | shared_preload_libraries: "'pg_stat_statements'" 12 | postgresql_settings: {} 13 | -------------------------------------------------------------------------------- /ansible/roles/postgresql/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart postgresql 3 | service: 4 | name: postgresql 5 | state: restarted 6 | 7 | - name: Reload postgresql 8 | service: 9 | name: postgresql 10 | state: reloaded 11 | -------------------------------------------------------------------------------- /ansible/roles/postgresql/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add APT key for PostgreSQL repository 3 | apt_key: 4 | url: https://www.postgresql.org/media/keys/ACCC4CF8.asc 5 | 6 | - name: Add PostgreSQL APT repository 7 | apt_repository: 8 | repo: deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main 9 | 10 | - name: Install PostgreSQL 11 | apt: 12 | name: postgresql-{{ postgresql_version }} 13 | 14 | - name: Start and enable PostgreSQL service 15 | service: 16 | name: postgresql 17 | state: started 18 | enabled: true 19 | 20 | - name: Set configuration options in postgresql.conf 21 | lineinfile: 22 | path: /etc/postgresql/{{ postgresql_version }}/main/postgresql.conf 23 | regexp: "^#?{{ item.key }} ?=" 24 | line: "{{ item.key }} = {{ item.value }}" 25 | loop: "{{ _postgresql_settings | combine(postgresql_settings) | dict2items }}" 26 | notify: 27 | - Restart postgresql 28 | -------------------------------------------------------------------------------- /ansible/roles/postgresql_plpython/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: postgresql 4 | -------------------------------------------------------------------------------- /ansible/roles/postgresql_plpython/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install PL/Python3 procedural language for PostgreSQL 3 | apt: 4 | name: postgresql-plpython3-{{ postgresql_version }} 5 | 6 | - name: Set PYTHONPATH env var for PostgreSQL 7 | lineinfile: 8 | path: /etc/postgresql/{{ postgresql_version }}/main/environment 9 | regexp: "^PYTHONPATH=" 10 | line: "PYTHONPATH='{{ venv_dir }}/lib/python{{ python_version }}/site-packages:{{ app_dir }}'" 11 | notify: 12 | - Restart postgresql 13 | -------------------------------------------------------------------------------- /ansible/roles/postgresql_redis_bridge/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: postgresql 4 | - role: redis 5 | -------------------------------------------------------------------------------- /ansible/roles/postgresql_redis_bridge/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Create postgresql_redis_bridge service file 2 | template: 3 | src: postgresql_redis_bridge.service.jinja2 4 | dest: /etc/systemd/system/postgresql_redis_bridge.service 5 | owner: root 6 | group: root 7 | mode: 0644 8 | 9 | - name: Start and enable postgresql_redis_bridge service 10 | service: 11 | name: postgresql_redis_bridge 12 | state: started 13 | enabled: true 14 | -------------------------------------------------------------------------------- /ansible/roles/postgresql_redis_bridge/templates/postgresql_redis_bridge.service.jinja2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=postgresql_redis_bridge - convert NOTIFY to Redis streams 3 | Requires=redis.service 4 | After=redis.service 5 | PartOf=redis.service 6 | 7 | [Service] 8 | User={{ app_username }} 9 | Group={{ app_username }} 10 | WorkingDirectory={{ app_dir }}/scripts 11 | Environment="INI_FILE={{ app_dir }}/{{ ini_file }}" 12 | ExecStart={{ bin_dir }}/python postgresql_redis_bridge.py 13 | Restart=always 14 | RestartSec=5 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /ansible/roles/postgresql_tildes_dbs/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | postgresql_tildes_databases: 3 | - tildes 4 | postgresql_tildes_user_flags: "" 5 | 6 | tildes_database_insert_dev_data: false 7 | -------------------------------------------------------------------------------- /ansible/roles/postgresql_tildes_dbs/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: postgresql 4 | - role: pgbouncer 5 | 6 | # needed to be able to run the db init scripts 7 | - role: python 8 | - role: cmark-gfm 9 | -------------------------------------------------------------------------------- /ansible/roles/prometheus/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | prometheus_version: 2.0.0 3 | 4 | prometheus_consumer_scrape_targets: 5 | comment_user_mentions_generator: 25010 6 | post_processing_script_runner: 25016 7 | topic_interesting_activity_updater: 25013 8 | topic_metadata_generator: 25014 9 | -------------------------------------------------------------------------------- /ansible/roles/prometheus/files/prometheus.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Prometheus Server 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=simple 7 | RemainAfterExit=no 8 | WorkingDirectory=/opt/prometheus 9 | User=prometheus 10 | Group=prometheus 11 | ExecStart=/opt/prometheus/prometheus --config.file=/opt/prometheus/prometheus.yml --log.level info 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ansible/roles/prometheus/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart prometheus 3 | service: 4 | name: prometheus 5 | state: restarted 6 | -------------------------------------------------------------------------------- /ansible/roles/prometheus_node_exporter/files/prometheus_node_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Prometheus Node Exporter 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=simple 7 | RemainAfterExit=no 8 | WorkingDirectory=/opt/prometheus_node_exporter 9 | User=prometheus 10 | Group=prometheus 11 | ExecStart=/opt/prometheus_node_exporter/node_exporter -log.level info 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ansible/roles/prometheus_postgres_exporter/files/prometheus_postgres_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Prometheus Postgres Exporter 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=simple 7 | RemainAfterExit=no 8 | WorkingDirectory=/opt/prometheus_postgres_exporter 9 | User=postgres 10 | Group=postgres 11 | Environment="DATA_SOURCE_NAME=user=postgres host=/run/postgresql/ sslmode=disable" 12 | Environment="PG_EXPORTER_EXTEND_QUERY_PATH=/opt/prometheus_postgres_exporter/queries.yaml" 13 | ExecStart=/opt/prometheus_postgres_exporter/postgres_exporter 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /ansible/roles/prometheus_postgres_exporter/files/queries.yaml: -------------------------------------------------------------------------------- 1 | pg_txid: 2 | query: "SELECT max(age(datfrozenxid)) AS max_txid_age from pg_database" 3 | metrics: 4 | - max_txid_age: 5 | usage: "GAUGE" 6 | description: "Highest transaction ID age (wraparound at 2 billion)" 7 | -------------------------------------------------------------------------------- /ansible/roles/prometheus_postgres_exporter/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: postgresql 4 | -------------------------------------------------------------------------------- /ansible/roles/prometheus_redis_exporter/files/prometheus_redis_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Prometheus Redis Exporter 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=simple 7 | RemainAfterExit=no 8 | WorkingDirectory=/opt/prometheus_redis_exporter 9 | User=prometheus 10 | Group=prometheus 11 | Environment="REDIS_ADDR=unix:///run/redis/socket" 12 | ExecStart=/opt/prometheus_redis_exporter/redis_exporter 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /ansible/roles/prometheus_redis_exporter/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: redis 4 | -------------------------------------------------------------------------------- /ansible/roles/pts_lbsearch/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Download pts_lbsearch code from GitHub 3 | get_url: 4 | dest: /tmp/pts_lbsearch.c 5 | url: https://raw.githubusercontent.com/pts/pts-line-bisect/2ecd9f59246cfa28cb1aeac7cd8d98a8eea2914f/pts_lbsearch.c 6 | checksum: sha256:ef79efc2f1ecde504b6074f9c89bdc71259a833fa2a2dda4538ed5ea3e04aea1 7 | 8 | - name: Compile pts_lbsearch 9 | command: 10 | chdir: /tmp 11 | # compilation command taken from the top of the source file 12 | cmd: gcc -ansi -W -Wall -Wextra -Werror=missing-declarations -s -O2 -DNDEBUG -o /usr/local/bin/pts_lbsearch pts_lbsearch.c 13 | creates: /usr/local/bin/pts_lbsearch 14 | -------------------------------------------------------------------------------- /ansible/roles/python/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pip_requirements_filename: requirements.txt 3 | -------------------------------------------------------------------------------- /ansible/roles/redis/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | redis_version: 6.2.4 3 | -------------------------------------------------------------------------------- /ansible/roles/redis/files/redis.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=redis daemon 3 | After=network.target 4 | 5 | [Service] 6 | PIDFile=/run/redis/pid 7 | User=redis 8 | Group=redis 9 | RuntimeDirectory=redis 10 | ExecStart=/usr/local/bin/redis-server /etc/redis.conf 11 | Restart=always 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /ansible/roles/redis/files/transparent_hugepage.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Disable transparent hugepage for redis 3 | Before=redis.service 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=/bin/bash -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' 8 | ExecStart=/bin/bash -c 'echo never > /sys/kernel/mm/transparent_hugepage/defrag' 9 | 10 | [Install] 11 | RequiredBy=redis.service 12 | -------------------------------------------------------------------------------- /ansible/roles/redis/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Restart redis 3 | service: 4 | name: redis 5 | state: restarted 6 | -------------------------------------------------------------------------------- /ansible/roles/redis_module_cell/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: redis 4 | -------------------------------------------------------------------------------- /ansible/roles/redis_module_cell/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Download redis-cell Redis module from GitHub 3 | get_url: 4 | dest: /tmp/redis-cell.tar.gz 5 | url: https://github.com/brandur/redis-cell/releases/download/v0.2.1/redis-cell-v0.2.1-x86_64-unknown-linux-gnu.tar.gz 6 | checksum: sha256:9427fb100f4cada817f30f854ead7f233de32948a0ec644f15988c275a2ed1cb 7 | 8 | - name: Create /opt/redis-cell 9 | file: 10 | path: /opt/redis-cell 11 | state: directory 12 | owner: redis 13 | group: redis 14 | mode: 0755 15 | 16 | - name: Extract redis-cell 17 | unarchive: 18 | remote_src: true 19 | src: /tmp/redis-cell.tar.gz 20 | dest: /opt/redis-cell 21 | 22 | - name: Load redis-cell module in Redis configuration 23 | lineinfile: 24 | path: /etc/redis.conf 25 | line: loadmodule /opt/redis-cell/libredis_cell.so 26 | notify: 27 | - Restart redis 28 | -------------------------------------------------------------------------------- /ansible/roles/scripts/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install the activate script 3 | template: 4 | src: activate.sh.jinja2 5 | dest: /usr/local/bin/activate 6 | owner: root 7 | group: root 8 | mode: 0755 9 | -------------------------------------------------------------------------------- /ansible/roles/scripts/templates/activate.sh.jinja2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Simple convenience script to activate the venv and switch to the app dir 4 | # (Needs to be run by sourcing or it won't do anything) 5 | cd {{ app_dir }} 6 | source {{ bin_dir }}/activate 7 | -------------------------------------------------------------------------------- /ansible/roles/self_signed_ssl_cert/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: nginx 4 | -------------------------------------------------------------------------------- /ansible/roles/self_signed_ssl_cert/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install packages needed by Ansible community plugins 3 | pip: 4 | executable: pip3 5 | name: cryptography 6 | 7 | - name: Create directory for certificate 8 | file: 9 | path: "{{ ssl_cert_dir }}" 10 | state: directory 11 | mode: 0755 12 | 13 | - name: Create a private key 14 | community.crypto.openssl_privatekey: 15 | path: "{{ ssl_private_key_path }}" 16 | 17 | - name: Create a self-signed certificate 18 | community.crypto.x509_certificate: 19 | path: "{{ ssl_cert_path }}" 20 | privatekey_path: "{{ ssl_private_key_path }}" 21 | provider: selfsigned 22 | notify: 23 | - Reload nginx 24 | -------------------------------------------------------------------------------- /ansible/roles/webassets/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: python 4 | -------------------------------------------------------------------------------- /ansible/roles/webassets/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if site-icons.css file exists 3 | stat: 4 | path: "{{ app_dir }}/static/css/site-icons.css" 5 | register: site_icons_css_file 6 | 7 | # webassets will crash the site if this file doesn't exist 8 | - name: Create site-icons.css file 9 | file: 10 | path: "{{ app_dir }}/static/css/site-icons.css" 11 | state: touch 12 | owner: "{{ app_username }}" 13 | group: "{{ app_username }}" 14 | mode: 0644 15 | when: not site_icons_css_file.stat.exists 16 | 17 | - name: Create systemd service file 18 | template: 19 | src: webassets.service.jinja2 20 | dest: /etc/systemd/system/webassets.service 21 | owner: root 22 | group: root 23 | mode: 0644 24 | 25 | - name: Start and enable webassets service 26 | service: 27 | name: webassets 28 | state: started 29 | enabled: true 30 | -------------------------------------------------------------------------------- /ansible/roles/webassets/templates/webassets.service.jinja2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Webassets - auto-compile JS files on change 3 | 4 | [Service] 5 | User={{ app_username }} 6 | Group={{ app_username }} 7 | WorkingDirectory={{ app_dir }} 8 | ExecStart={{ bin_dir }}/webassets -c webassets.yaml watch 9 | Restart=always 10 | RestartSec=5 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /ansible/roles/wiki_repo/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create the base repo directory 3 | file: 4 | path: /var/lib/tildes-wiki 5 | state: directory 6 | owner: "{{ app_username }}" 7 | group: "{{ app_username }}" 8 | mode: 0775 9 | 10 | - name: Check if a wiki git repo exists 11 | stat: 12 | path: /var/lib/tildes-wiki/.git 13 | register: wiki_repo 14 | 15 | - name: Create a git repo and initial commit 16 | become_user: "{{ app_username }}" 17 | shell: 18 | cmd: | 19 | git init 20 | git config user.name "Tildes" 21 | git config user.email "Tildes" 22 | git commit --allow-empty -m "Initial commit" 23 | chdir: /var/lib/tildes-wiki 24 | when: not wiki_repo.stat.exists 25 | -------------------------------------------------------------------------------- /ansible/tasks/prometheus_user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create group for prometheus user 3 | group: 4 | name: prometheus 5 | 6 | - name: Create prometheus user 7 | user: 8 | name: prometheus 9 | group: prometheus 10 | create_home: false 11 | -------------------------------------------------------------------------------- /ansible/vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | app_dir: /opt/tildes 3 | venv_dir: /opt/venvs/tildes 4 | bin_dir: "{{ venv_dir }}/bin" 5 | 6 | static_sites_dir: /opt/tildes-static-sites 7 | 8 | python_full_version: 3.9.5 9 | python_version: "{{ python_full_version.rpartition('.')[0] }}" 10 | -------------------------------------------------------------------------------- /git_hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Pre-commit hook script that ensures type-checking, tests, and fast style checks pass 4 | 5 | vagrant ssh -c ". activate \ 6 | && invoke type-check test --quiet code-style-check" 7 | -------------------------------------------------------------------------------- /git_hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Pre-push hook script that ensures all tests and code checks pass 4 | 5 | vagrant ssh -c ". activate \ 6 | && invoke type-check test --quiet --full code-style-check --full" 7 | -------------------------------------------------------------------------------- /tildes/alembic.ini: -------------------------------------------------------------------------------- 1 | [alembic] 2 | script_location = alembic 3 | sqlalchemy.url = postgresql+psycopg2://tildes:@/tildes 4 | 5 | [post_write_hooks] 6 | hooks=black 7 | black.type=console_scripts 8 | black.entrypoint=black 9 | 10 | # Logging configuration 11 | [loggers] 12 | keys = root,sqlalchemy,alembic 13 | 14 | [handlers] 15 | keys = console 16 | 17 | [formatters] 18 | keys = generic 19 | 20 | [logger_root] 21 | level = WARN 22 | handlers = console 23 | qualname = 24 | 25 | [logger_sqlalchemy] 26 | level = WARN 27 | handlers = 28 | qualname = sqlalchemy.engine 29 | 30 | [logger_alembic] 31 | level = INFO 32 | handlers = 33 | qualname = alembic 34 | 35 | [handler_console] 36 | class = StreamHandler 37 | args = (sys.stderr,) 38 | level = NOTSET 39 | formatter = generic 40 | 41 | [formatter_generic] 42 | format = %(levelname)-5.5s [%(name)s] %(message)s 43 | datefmt = %H:%M:%S 44 | -------------------------------------------------------------------------------- /tildes/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises:${" " + down_revision if down_revision else "" | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /tildes/alembic/versions/0435c46f64d8_topic_schedule_add_only_new_top_level_.py: -------------------------------------------------------------------------------- 1 | """topic_schedule: add only_new_top_level_comments_in_latest 2 | 3 | Revision ID: 0435c46f64d8 4 | Revises: 468cf81f4a6b 5 | Create Date: 2020-07-05 19:33:17.746617 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "0435c46f64d8" 14 | down_revision = "468cf81f4a6b" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "topic_schedule", 22 | sa.Column( 23 | "only_new_top_level_comments_in_latest", 24 | sa.Boolean(), 25 | server_default="true", 26 | nullable=False, 27 | ), 28 | ) 29 | 30 | 31 | def downgrade(): 32 | op.drop_column("topic_schedule", "only_new_top_level_comments_in_latest") 33 | -------------------------------------------------------------------------------- /tildes/alembic/versions/04fd898de0db_add_collapse_old_comments_setting.py: -------------------------------------------------------------------------------- 1 | """Add collapse_old_comments setting 2 | 3 | Revision ID: 04fd898de0db 4 | Revises: b9d9ae4c2286 5 | Create Date: 2018-08-29 03:07:10.278549 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "04fd898de0db" 14 | down_revision = "b9d9ae4c2286" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "users", 22 | sa.Column( 23 | "collapse_old_comments", sa.Boolean(), server_default="true", nullable=False 24 | ), 25 | ) 26 | 27 | 28 | def downgrade(): 29 | op.drop_column("users", "collapse_old_comments") 30 | -------------------------------------------------------------------------------- /tildes/alembic/versions/1ade2bf86efc_comment_tags_add_reason_column.py: -------------------------------------------------------------------------------- 1 | """comment_tags: add reason column 2 | 3 | Revision ID: 1ade2bf86efc 4 | Revises: 1996feae620d 5 | Create Date: 2018-09-18 20:44:19.357105 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "1ade2bf86efc" 14 | down_revision = "1996feae620d" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column("comment_tags", sa.Column("reason", sa.Text(), nullable=True)) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column("comment_tags", "reason") 25 | -------------------------------------------------------------------------------- /tildes/alembic/versions/24014adda7c3_add_topic_link_edit_to_logeventtype.py: -------------------------------------------------------------------------------- 1 | """Add TOPIC_LINK_EDIT to logeventtype 2 | 3 | Revision ID: 24014adda7c3 4 | Revises: 7ac1aad64144 5 | Create Date: 2019-03-14 21:57:27.057187 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "24014adda7c3" 14 | down_revision = "7ac1aad64144" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ALTER TYPE doesn't work from inside a transaction, disable it 21 | connection = None 22 | if not op.get_context().as_sql: 23 | connection = op.get_bind() 24 | connection.execution_options(isolation_level="AUTOCOMMIT") 25 | 26 | op.execute("ALTER TYPE logeventtype ADD VALUE IF NOT EXISTS 'TOPIC_LINK_EDIT'") 27 | 28 | # re-activate the transaction for any future migrations 29 | if connection is not None: 30 | connection.execution_options(isolation_level="READ_COMMITTED") 31 | 32 | 33 | def downgrade(): 34 | # can't remove from enums, do nothing 35 | pass 36 | -------------------------------------------------------------------------------- /tildes/alembic/versions/2512581c91b3_add_setting_to_open_links_in_new_tab.py: -------------------------------------------------------------------------------- 1 | """Add setting to open links in new tab 2 | 3 | Revision ID: 2512581c91b3 4 | Revises: 5 | Create Date: 2018-07-21 22:23:49.563318 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "2512581c91b3" 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "users", 22 | sa.Column( 23 | "open_new_tab_external", 24 | sa.Boolean(), 25 | server_default="false", 26 | nullable=False, 27 | ), 28 | ) 29 | op.add_column( 30 | "users", 31 | sa.Column( 32 | "open_new_tab_internal", 33 | sa.Boolean(), 34 | server_default="false", 35 | nullable=False, 36 | ), 37 | ) 38 | 39 | 40 | def downgrade(): 41 | op.drop_column("users", "open_new_tab_internal") 42 | op.drop_column("users", "open_new_tab_external") 43 | -------------------------------------------------------------------------------- /tildes/alembic/versions/347859b0355e_added_account_default_theme_setting.py: -------------------------------------------------------------------------------- 1 | """Added account default theme setting 2 | 3 | Revision ID: 347859b0355e 4 | Revises: 3fbddcba0e3b 5 | Create Date: 2018-08-11 16:23:13.297883 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "347859b0355e" 14 | down_revision = "3fbddcba0e3b" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column("users", sa.Column("theme_default", sa.Text())) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column("users", "theme_default") 25 | -------------------------------------------------------------------------------- /tildes/alembic/versions/3f83028d1673_add_user_bio_column.py: -------------------------------------------------------------------------------- 1 | """Add user bio column 2 | 3 | Revision ID: 3f83028d1673 4 | Revises: 4ebc3ca32b48 5 | Create Date: 2019-02-20 08:17:49.636855 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "3f83028d1673" 14 | down_revision = "4ebc3ca32b48" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column("users", sa.Column("bio_markdown", sa.Text(), nullable=True)) 21 | op.add_column("users", sa.Column("bio_rendered_html", sa.Text(), nullable=True)) 22 | op.create_check_constraint( 23 | "bio_markdown_length", "users", "LENGTH(bio_markdown) <= 2000" 24 | ) 25 | 26 | 27 | def downgrade(): 28 | op.drop_constraint("ck_users_bio_markdown_length", "users") 29 | op.drop_column("users", "bio_rendered_html") 30 | op.drop_column("users", "bio_markdown") 31 | -------------------------------------------------------------------------------- /tildes/alembic/versions/4241b0202fd4_add_setting_to_open_group_and_user_.py: -------------------------------------------------------------------------------- 1 | """Add setting to open group and user links in new tab 2 | 3 | Revision ID: 4241b0202fd4 4 | Revises: 34753d8124b4 5 | Create Date: 2020-02-06 16:59:10.720154 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "4241b0202fd4" 14 | down_revision = "34753d8124b4" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "users", 22 | sa.Column( 23 | "open_new_tab_group", sa.Boolean(), server_default="false", nullable=False 24 | ), 25 | ) 26 | op.add_column( 27 | "users", 28 | sa.Column( 29 | "open_new_tab_user", sa.Boolean(), server_default="false", nullable=False 30 | ), 31 | ) 32 | 33 | 34 | def downgrade(): 35 | op.drop_column("users", "open_new_tab_user") 36 | op.drop_column("users", "open_new_tab_group") 37 | -------------------------------------------------------------------------------- /tildes/alembic/versions/4d86b372a8db_user_add_ban_expiry_time.py: -------------------------------------------------------------------------------- 1 | """User: add ban_expiry_time 2 | 3 | Revision ID: 4d86b372a8db 4 | Revises: 9148909b78e9 5 | Create Date: 2020-05-09 20:05:30.503634 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "4d86b372a8db" 14 | down_revision = "9148909b78e9" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "users", 22 | sa.Column("ban_expiry_time", sa.TIMESTAMP(timezone=True), nullable=True), 23 | ) 24 | 25 | 26 | def downgrade(): 27 | op.drop_column("users", "ban_expiry_time") 28 | -------------------------------------------------------------------------------- /tildes/alembic/versions/4ebc3ca32b48_send_rabbitmq_message_on_link_edit.py: -------------------------------------------------------------------------------- 1 | """Send rabbitmq message on link edit 2 | 3 | Revision ID: 4ebc3ca32b48 4 | Revises: 24014adda7c3 5 | Create Date: 2019-03-15 00:59:57.713065 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "4ebc3ca32b48" 14 | down_revision = "24014adda7c3" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.execute( 21 | """ 22 | CREATE TRIGGER send_rabbitmq_message_for_topic_link_edit 23 | AFTER UPDATE ON topics 24 | FOR EACH ROW 25 | WHEN (OLD.link IS DISTINCT FROM NEW.link) 26 | EXECUTE PROCEDURE send_rabbitmq_message_for_topic('link_edited'); 27 | """ 28 | ) 29 | 30 | 31 | def downgrade(): 32 | op.execute("DROP TRIGGER send_rabbitmq_message_for_topic_link_edit ON topics") 33 | -------------------------------------------------------------------------------- /tildes/alembic/versions/51a1012f4f63_add_comment_sort_order_account_setting.py: -------------------------------------------------------------------------------- 1 | """Add comment sort order account setting 2 | 3 | Revision ID: 51a1012f4f63 4 | Revises: 9b7a7b906956 5 | Create Date: 2020-02-07 22:38:08.826608 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "51a1012f4f63" 14 | down_revision = "9b7a7b906956" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.execute( 21 | "create type commenttreesortoption as enum('VOTES', 'NEWEST', 'POSTED', 'RELEVANCE')" 22 | ) 23 | op.add_column( 24 | "users", 25 | sa.Column( 26 | "comment_sort_order_default", 27 | postgresql.ENUM( 28 | "VOTES", "NEWEST", "POSTED", "RELEVANCE", name="commenttreesortoption" 29 | ), 30 | nullable=True, 31 | ), 32 | ) 33 | 34 | 35 | def downgrade(): 36 | op.drop_column("users", "comment_sort_order_default") 37 | op.execute("drop type commenttreesortoption") 38 | -------------------------------------------------------------------------------- /tildes/alembic/versions/53f81a72f076_group_add_common_topic_tags.py: -------------------------------------------------------------------------------- 1 | """Group: add common_topic_tags 2 | 3 | Revision ID: 53f81a72f076 4 | Revises: fef2c9c9a186 5 | Create Date: 2019-04-24 17:50:24.360780 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from tildes.lib.database import ArrayOfLtree 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = "53f81a72f076" 16 | down_revision = "fef2c9c9a186" 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | op.add_column( 23 | "groups", 24 | sa.Column( 25 | "common_topic_tags", ArrayOfLtree(), server_default="{}", nullable=False 26 | ), 27 | ) 28 | 29 | 30 | def downgrade(): 31 | op.drop_column("groups", "common_topic_tags") 32 | -------------------------------------------------------------------------------- /tildes/alembic/versions/55f4c1f951d5_add_group_scripts_table.py: -------------------------------------------------------------------------------- 1 | """Add group_scripts table 2 | 3 | Revision ID: 55f4c1f951d5 4 | Revises: 28d7ce2c4825 5 | Create Date: 2020-11-30 19:54:30.731335 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "55f4c1f951d5" 14 | down_revision = "28d7ce2c4825" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table( 21 | "group_scripts", 22 | sa.Column("script_id", sa.Integer(), nullable=False), 23 | sa.Column("group_id", sa.Integer(), nullable=True), 24 | sa.Column("code", sa.Text(), nullable=False), 25 | sa.ForeignKeyConstraint( 26 | ["group_id"], 27 | ["groups.group_id"], 28 | name=op.f("fk_group_scripts_group_id_groups"), 29 | ), 30 | sa.PrimaryKeyConstraint("script_id", name=op.f("pk_group_scripts")), 31 | ) 32 | 33 | 34 | def downgrade(): 35 | op.drop_table("group_scripts") 36 | -------------------------------------------------------------------------------- /tildes/alembic/versions/61f43e57679a_add_youtube_scraper_result.py: -------------------------------------------------------------------------------- 1 | """Add youtube scraper result 2 | 3 | Revision ID: 61f43e57679a 4 | Revises: a0e0b6206146 5 | Create Date: 2019-01-26 20:02:27.642583 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "61f43e57679a" 14 | down_revision = "a0e0b6206146" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ALTER TYPE doesn't work from inside a transaction, disable it 21 | connection = None 22 | if not op.get_context().as_sql: 23 | connection = op.get_bind() 24 | connection.execution_options(isolation_level="AUTOCOMMIT") 25 | 26 | op.execute("ALTER TYPE scrapertype ADD VALUE IF NOT EXISTS 'YOUTUBE'") 27 | 28 | # re-activate the transaction for any future migrations 29 | if connection is not None: 30 | connection.execution_options(isolation_level="READ_COMMITTED") 31 | 32 | 33 | def downgrade(): 34 | # can't remove from enums, do nothing 35 | pass 36 | -------------------------------------------------------------------------------- /tildes/alembic/versions/67e332481a6e_add_two_factor_authentication.py: -------------------------------------------------------------------------------- 1 | """Add two-factor authentication 2 | 3 | Revision ID: 67e332481a6e 4 | Revises: fab922a8bb04 5 | Create Date: 2018-07-31 02:53:50.182862 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "67e332481a6e" 14 | down_revision = "fab922a8bb04" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "users", 22 | sa.Column( 23 | "two_factor_backup_codes", postgresql.ARRAY(sa.Text()), nullable=True 24 | ), 25 | ) 26 | op.add_column( 27 | "users", 28 | sa.Column( 29 | "two_factor_enabled", sa.Boolean(), server_default="false", nullable=False 30 | ), 31 | ) 32 | op.add_column("users", sa.Column("two_factor_secret", sa.Text(), nullable=True)) 33 | 34 | 35 | def downgrade(): 36 | op.drop_column("users", "two_factor_secret") 37 | op.drop_column("users", "two_factor_enabled") 38 | op.drop_column("users", "two_factor_backup_codes") 39 | -------------------------------------------------------------------------------- /tildes/alembic/versions/6a635773de8f_add_comment_post_to_logeventtype.py: -------------------------------------------------------------------------------- 1 | """Add COMMENT_POST to logeventtype 2 | 3 | Revision ID: 6a635773de8f 4 | Revises: b3be50625592 5 | Create Date: 2018-08-26 01:56:13.511360 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "6a635773de8f" 14 | down_revision = "b3be50625592" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ALTER TYPE doesn't work from inside a transaction, disable it 21 | connection = None 22 | if not op.get_context().as_sql: 23 | connection = op.get_bind() 24 | connection.execution_options(isolation_level="AUTOCOMMIT") 25 | 26 | op.execute("ALTER TYPE logeventtype ADD VALUE IF NOT EXISTS 'COMMENT_POST'") 27 | 28 | # re-activate the transaction for any future migrations 29 | if connection is not None: 30 | connection.execution_options(isolation_level="READ_COMMITTED") 31 | 32 | 33 | def downgrade(): 34 | # can't remove from enums, do nothing 35 | pass 36 | -------------------------------------------------------------------------------- /tildes/alembic/versions/6c840340ab86_drop_track_comment_visits_column_on_.py: -------------------------------------------------------------------------------- 1 | """Drop track_comment_visits column on users 2 | 3 | Revision ID: 6c840340ab86 4 | Revises: cc12ea6c616d 5 | Create Date: 2020-01-27 21:42:25.565355 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "6c840340ab86" 14 | down_revision = "cc12ea6c616d" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.drop_column("users", "track_comment_visits") 21 | 22 | 23 | def downgrade(): 24 | op.add_column( 25 | "users", 26 | sa.Column( 27 | "track_comment_visits", 28 | sa.BOOLEAN(), 29 | server_default=sa.text("false"), 30 | nullable=False, 31 | ), 32 | ) 33 | -------------------------------------------------------------------------------- /tildes/alembic/versions/6ede05a0ea23_topic_add_original_url_column.py: -------------------------------------------------------------------------------- 1 | """Topic: add original_url column 2 | 3 | Revision ID: 6ede05a0ea23 4 | Revises: 09cfb27cc90e 5 | Create Date: 2018-09-12 18:45:44.768561 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from tildes.models.topic import Topic 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = "6ede05a0ea23" 16 | down_revision = "09cfb27cc90e" 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | op.add_column("topics", sa.Column("original_url", sa.Text(), nullable=True)) 23 | 24 | session = sa.orm.Session(bind=op.get_bind()) 25 | session.query(Topic).update({"original_url": Topic.link}, synchronize_session=False) 26 | session.commit() 27 | 28 | 29 | def downgrade(): 30 | op.drop_column("topics", "original_url") 31 | -------------------------------------------------------------------------------- /tildes/alembic/versions/7ac1aad64144_group_add_is_user_treated_as_topic_.py: -------------------------------------------------------------------------------- 1 | """Group: add is_user_treated_as_topic_source 2 | 3 | Revision ID: 7ac1aad64144 4 | Revises: 61f43e57679a 5 | Create Date: 2019-03-08 23:02:33.848382 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "7ac1aad64144" 14 | down_revision = "61f43e57679a" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column( 22 | "groups", 23 | sa.Column( 24 | "is_user_treated_as_topic_source", 25 | sa.Boolean(), 26 | server_default="false", 27 | nullable=False, 28 | ), 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_column("groups", "is_user_treated_as_topic_source") 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /tildes/alembic/versions/82e9801eb2d6_update_post_topic_permission_to_topic_.py: -------------------------------------------------------------------------------- 1 | """Update post_topic permission to topic.post 2 | 3 | Revision ID: 82e9801eb2d6 4 | Revises: 0435c46f64d8 5 | Create Date: 2020-08-05 00:05:46.690188 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "82e9801eb2d6" 14 | down_revision = "0435c46f64d8" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.execute( 21 | "update user_permissions set permission = 'topic.post' where permission = 'post_topic'" 22 | ) 23 | 24 | 25 | def downgrade(): 26 | op.execute( 27 | "update user_permissions set permission = 'post_topic' where permission = 'topic.post'" 28 | ) 29 | -------------------------------------------------------------------------------- /tildes/alembic/versions/8326f8cc5ddd_topic_drop_original_url_column.py: -------------------------------------------------------------------------------- 1 | """Topic: drop original_url column 2 | 3 | Revision ID: 8326f8cc5ddd 4 | Revises: 20b5f07e5f80 5 | Create Date: 2019-10-05 00:52:20.515858 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "8326f8cc5ddd" 14 | down_revision = "20b5f07e5f80" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.drop_column("topics", "original_url") 21 | 22 | 23 | def downgrade(): 24 | op.add_column("topics", sa.Column("original_url", sa.Text(), nullable=True)) 25 | -------------------------------------------------------------------------------- /tildes/alembic/versions/84dc19f6e876_rename_column_for_restricted_posting_.py: -------------------------------------------------------------------------------- 1 | """Rename column for restricted-posting groups and wiki permission 2 | 3 | Revision ID: 84dc19f6e876 4 | Revises: 054aaef690cd 5 | Create Date: 2020-02-29 03:03:31.968814 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "84dc19f6e876" 14 | down_revision = "054aaef690cd" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.alter_column( 21 | "groups", 22 | "is_admin_posting_only", 23 | new_column_name="requires_permission_to_post_topics", 24 | ) 25 | 26 | op.execute( 27 | "update user_permissions set permission = 'wiki.edit' where permission = 'wiki'" 28 | ) 29 | 30 | 31 | def downgrade(): 32 | op.alter_column( 33 | "groups", 34 | "requires_permission_to_post_topics", 35 | new_column_name="is_admin_posting_only", 36 | ) 37 | 38 | op.execute( 39 | "update user_permissions set permission = 'wiki' where permission = 'wiki.edit'" 40 | ) 41 | -------------------------------------------------------------------------------- /tildes/alembic/versions/9fc0033a2b61_topic_add_schedule_id_column.py: -------------------------------------------------------------------------------- 1 | """Topic: add schedule_id column 2 | 3 | Revision ID: 9fc0033a2b61 4 | Revises: 8326f8cc5ddd 5 | Create Date: 2019-10-12 01:51:26.045258 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "9fc0033a2b61" 14 | down_revision = "8326f8cc5ddd" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column("topics", sa.Column("schedule_id", sa.Integer(), nullable=True)) 21 | op.create_index( 22 | op.f("ix_topics_schedule_id"), "topics", ["schedule_id"], unique=False 23 | ) 24 | op.create_foreign_key( 25 | op.f("fk_topics_schedule_id_topic_schedule"), 26 | "topics", 27 | "topic_schedule", 28 | ["schedule_id"], 29 | ["schedule_id"], 30 | ) 31 | 32 | 33 | def downgrade(): 34 | op.drop_constraint( 35 | op.f("fk_topics_schedule_id_topic_schedule"), "topics", type_="foreignkey" 36 | ) 37 | op.drop_index(op.f("ix_topics_schedule_id"), table_name="topics") 38 | op.drop_column("topics", "schedule_id") 39 | -------------------------------------------------------------------------------- /tildes/alembic/versions/a1708d376252_drop_topics_removed_time_column.py: -------------------------------------------------------------------------------- 1 | """Drop topics.removed_time column 2 | 3 | Revision ID: a1708d376252 4 | Revises: bcf1406bb6c5 5 | Create Date: 2018-08-23 00:29:41.024890 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "a1708d376252" 14 | down_revision = "bcf1406bb6c5" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.drop_column("topics", "removed_time") 21 | 22 | 23 | def downgrade(): 24 | op.add_column( 25 | "topics", 26 | sa.Column( 27 | "removed_time", 28 | postgresql.TIMESTAMP(timezone=True), 29 | autoincrement=False, 30 | nullable=True, 31 | ), 32 | ) 33 | -------------------------------------------------------------------------------- /tildes/alembic/versions/a195ddbb4be6_add_solarized_prefix_to_default_themes.py: -------------------------------------------------------------------------------- 1 | """Add solarized- prefix to default themes 2 | 3 | Revision ID: a195ddbb4be6 4 | Revises: f20ce28b1d5c 5 | Create Date: 2019-09-10 04:13:50.950487 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "a195ddbb4be6" 14 | down_revision = "f20ce28b1d5c" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.execute( 21 | "UPDATE users SET theme_default = 'solarized-dark' WHERE theme_default = 'dark'" 22 | ) 23 | op.execute( 24 | "UPDATE users SET theme_default = 'solarized-light' WHERE theme_default = 'light'" 25 | ) 26 | 27 | 28 | def downgrade(): 29 | op.execute( 30 | "UPDATE users SET theme_default = 'dark' WHERE theme_default = 'solarized-dark'" 31 | ) 32 | op.execute( 33 | "UPDATE users SET theme_default = 'light' WHERE theme_default = 'solarized-light'" 34 | ) 35 | -------------------------------------------------------------------------------- /tildes/alembic/versions/afa3128a9b54_add_exemplary_comment_tag.py: -------------------------------------------------------------------------------- 1 | """Add Exemplary comment tag 2 | 3 | Revision ID: afa3128a9b54 4 | Revises: 1ade2bf86efc 5 | Create Date: 2018-09-18 22:17:39.619439 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "afa3128a9b54" 14 | down_revision = "1ade2bf86efc" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ALTER TYPE doesn't work from inside a transaction, disable it 21 | connection = None 22 | if not op.get_context().as_sql: 23 | connection = op.get_bind() 24 | connection.execution_options(isolation_level="AUTOCOMMIT") 25 | 26 | op.execute("ALTER TYPE commenttagoption ADD VALUE IF NOT EXISTS 'EXEMPLARY'") 27 | 28 | # re-activate the transaction for any future migrations 29 | if connection is not None: 30 | connection.execution_options(isolation_level="READ_COMMITTED") 31 | 32 | 33 | def downgrade(): 34 | pass 35 | -------------------------------------------------------------------------------- /tildes/alembic/versions/b761d0185ca0_groups_add_important_topic_tags.py: -------------------------------------------------------------------------------- 1 | """Groups: add important_topic_tags 2 | 3 | Revision ID: b761d0185ca0 4 | Revises: 679090fd4977 5 | Create Date: 2019-10-26 01:51:21.231463 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from tildes.lib.database import TagList 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = "b761d0185ca0" 16 | down_revision = "679090fd4977" 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | op.add_column( 23 | "groups", 24 | sa.Column( 25 | "important_topic_tags", TagList(), server_default="{}", nullable=False 26 | ), 27 | ) 28 | 29 | 30 | def downgrade(): 31 | op.drop_column("groups", "important_topic_tags") 32 | -------------------------------------------------------------------------------- /tildes/alembic/versions/b825165870d9_add_weights_to_comment_tags.py: -------------------------------------------------------------------------------- 1 | """Add weights to comment tags 2 | 3 | Revision ID: b825165870d9 4 | Revises: 6ede05a0ea23 5 | Create Date: 2018-09-14 03:06:51.144073 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "b825165870d9" 14 | down_revision = "6ede05a0ea23" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "comment_tags", 22 | sa.Column("weight", sa.REAL(), server_default=sa.text("1.0"), nullable=False), 23 | ) 24 | op.add_column("users", sa.Column("comment_tag_weight", sa.REAL(), nullable=True)) 25 | 26 | 27 | def downgrade(): 28 | op.drop_column("users", "comment_tag_weight") 29 | op.drop_column("comment_tags", "weight") 30 | -------------------------------------------------------------------------------- /tildes/alembic/versions/d33fb803a153_switch_to_general_permissions_column.py: -------------------------------------------------------------------------------- 1 | """Switch to general permissions column 2 | 3 | Revision ID: d33fb803a153 4 | Revises: 67e332481a6e 5 | Create Date: 2018-08-16 23:07:07.643208 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "d33fb803a153" 14 | down_revision = "67e332481a6e" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "users", 22 | sa.Column( 23 | "permissions", postgresql.JSONB(astext_type=sa.Text()), nullable=True 24 | ), 25 | ) 26 | op.drop_column("users", "is_admin") 27 | 28 | 29 | def downgrade(): 30 | op.add_column( 31 | "users", 32 | sa.Column( 33 | "is_admin", 34 | sa.BOOLEAN(), 35 | server_default=sa.text("false"), 36 | autoincrement=False, 37 | nullable=False, 38 | ), 39 | ) 40 | op.drop_column("users", "permissions") 41 | -------------------------------------------------------------------------------- /tildes/alembic/versions/de83b8750123_add_setting_to_open_text_links_in_new_.py: -------------------------------------------------------------------------------- 1 | """Add setting to open text links in new tab 2 | 3 | Revision ID: de83b8750123 4 | Revises: 2512581c91b3 5 | Create Date: 2018-07-24 03:10:59.485645 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "de83b8750123" 14 | down_revision = "2512581c91b3" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "users", 22 | sa.Column( 23 | "open_new_tab_text", sa.Boolean(), server_default="false", nullable=False 24 | ), 25 | ) 26 | 27 | 28 | def downgrade(): 29 | op.drop_column("users", "open_new_tab_text") 30 | -------------------------------------------------------------------------------- /tildes/alembic/versions/e9bbc2929d9c_group_add_sidebar_markdown_html.py: -------------------------------------------------------------------------------- 1 | """Group: add sidebar markdown/html 2 | 3 | Revision ID: e9bbc2929d9c 4 | Revises: 9b88cb0a7b2c 5 | Create Date: 2019-05-31 00:18:07.179045 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "e9bbc2929d9c" 14 | down_revision = "9b88cb0a7b2c" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column("groups", sa.Column("sidebar_markdown", sa.Text(), nullable=True)) 21 | op.add_column( 22 | "groups", sa.Column("sidebar_rendered_html", sa.Text(), nullable=True) 23 | ) 24 | 25 | 26 | def downgrade(): 27 | op.drop_column("groups", "sidebar_rendered_html") 28 | op.drop_column("groups", "sidebar_markdown") 29 | -------------------------------------------------------------------------------- /tildes/alembic/versions/f20ce28b1d5c_rename_group_wiki_pages_slug_to_path.py: -------------------------------------------------------------------------------- 1 | """Rename group_wiki_pages.slug to path 2 | 3 | Revision ID: f20ce28b1d5c 4 | Revises: cddd7d7ed0ea 5 | Create Date: 2019-08-10 04:40:04.657360 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "f20ce28b1d5c" 14 | down_revision = "cddd7d7ed0ea" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.alter_column("group_wiki_pages", "slug", new_column_name="path") 21 | 22 | 23 | def downgrade(): 24 | op.alter_column("group_wiki_pages", "path", new_column_name="slug") 25 | -------------------------------------------------------------------------------- /tildes/alembic/versions/fa14e9f5ebe5_add_user_rate_limit_table.py: -------------------------------------------------------------------------------- 1 | """Add user_rate_limit table 2 | 3 | Revision ID: fa14e9f5ebe5 4 | Revises: b761d0185ca0 5 | Create Date: 2019-11-05 18:11:34.303355 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "fa14e9f5ebe5" 14 | down_revision = "b761d0185ca0" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.create_table( 21 | "user_rate_limit", 22 | sa.Column("user_id", sa.Integer(), nullable=False), 23 | sa.Column("action", sa.Text(), nullable=False), 24 | sa.Column("period", sa.Interval(), nullable=False), 25 | sa.Column("limit", sa.Integer(), nullable=False), 26 | sa.ForeignKeyConstraint( 27 | ["user_id"], 28 | ["users.user_id"], 29 | name=op.f("fk_user_rate_limit_user_id_users"), 30 | ), 31 | sa.PrimaryKeyConstraint("user_id", "action", name=op.f("pk_user_rate_limit")), 32 | ) 33 | 34 | 35 | def downgrade(): 36 | op.drop_table("user_rate_limit") 37 | -------------------------------------------------------------------------------- /tildes/alembic/versions/fe91222503ef_financials_drop_is_approximate_column.py: -------------------------------------------------------------------------------- 1 | """Financials: Drop is_approximate column 2 | 3 | Revision ID: fe91222503ef 4 | Revises: 84dc19f6e876 5 | Create Date: 2020-03-04 22:38:15.528403 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "fe91222503ef" 14 | down_revision = "84dc19f6e876" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.drop_column("financials", "is_approximate") 21 | 22 | 23 | def downgrade(): 24 | op.add_column( 25 | "financials", 26 | sa.Column( 27 | "is_approximate", 28 | sa.BOOLEAN(), 29 | server_default=sa.text("false"), 30 | autoincrement=False, 31 | nullable=False, 32 | ), 33 | ) 34 | -------------------------------------------------------------------------------- /tildes/alembic/versions/fef2c9c9a186_user_add_interact_mark_notifications_.py: -------------------------------------------------------------------------------- 1 | """User: add interact_mark_notifications_read 2 | 3 | Revision ID: fef2c9c9a186 4 | Revises: beaa57144e49 5 | Create Date: 2019-04-01 13:21:38.441021 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "fef2c9c9a186" 14 | down_revision = "beaa57144e49" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column( 21 | "users", 22 | sa.Column( 23 | "interact_mark_notifications_read", 24 | sa.Boolean(), 25 | server_default="true", 26 | nullable=False, 27 | ), 28 | ) 29 | 30 | 31 | def downgrade(): 32 | op.drop_column("users", "interact_mark_notifications_read") 33 | -------------------------------------------------------------------------------- /tildes/boussole.yaml: -------------------------------------------------------------------------------- 1 | EXCLUDES: [] 2 | LIBRARY_PATHS: [] 3 | OUTPUT_STYLES: nested 4 | SOURCES_PATH: scss/ 5 | SOURCE_COMMENTS: false 6 | TARGET_PATH: static/css/ 7 | -------------------------------------------------------------------------------- /tildes/gunicorn_config.py: -------------------------------------------------------------------------------- 1 | """Configuration file for gunicorn.""" 2 | 3 | from prometheus_client import multiprocess 4 | 5 | 6 | def child_exit(server, worker): # type: ignore 7 | """Mark worker processes as dead for Prometheus when the worker exits. 8 | 9 | Note that this uses the child_exit hook instead of worker_exit so that it's handled 10 | by the master process (and will still be called if a worker crashes). 11 | """ 12 | # pylint: disable=unused-argument 13 | multiprocess.mark_process_dead(worker.pid) 14 | -------------------------------------------------------------------------------- /tildes/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | mypy_path = /opt/tildes/stubs/ 3 | exclude = ^(tests|alembic)/ 4 | disallow_untyped_defs = true 5 | ignore_missing_imports = true 6 | no_implicit_optional = true 7 | pretty = true 8 | show_error_codes = true 9 | show_error_context = true 10 | warn_redundant_casts = true 11 | warn_unused_ignores = true 12 | 13 | # invoke crashes if task functions use type annotations, so we can't use them there 14 | [mypy-tasks] 15 | disallow_untyped_defs = false 16 | 17 | -------------------------------------------------------------------------------- /tildes/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | exclude = "/(\\.mypy_cache|node_modules|scss|sql|static)/" 3 | 4 | [tool.isort] 5 | skip = "alembic" 6 | line_length = 88 7 | multi_line_output = 3 # "Vertical Hanging Indent" style 8 | include_trailing_comma = true 9 | lines_after_imports = 2 10 | known_third_party = "alembic" 11 | order_by_type = false 12 | -------------------------------------------------------------------------------- /tildes/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | addopts = -p no:cacheprovider --strict-markers 4 | filterwarnings = 5 | ignore::DeprecationWarning 6 | ignore::PendingDeprecationWarning 7 | ignore::yaml.YAMLLoadWarning 8 | markers = 9 | html_validation: mark a test as one that validates HTML using the Nu HTML Checker (very slow) 10 | webtest: mark a test as one that uses the WebTest library, which goes through the actual WSGI app and involves using HTTP/HTML (more of a "functional test" than "unit test") 11 | -------------------------------------------------------------------------------- /tildes/requirements-dev.in: -------------------------------------------------------------------------------- 1 | -r requirements.in 2 | black 3 | freezegun 4 | html5validator 5 | mypy 6 | prospector @ git+https://github.com/Deimos/prospector.git#egg=prospector 7 | pyramid-debugtoolbar 8 | pytest 9 | pytest-mock 10 | testing.redis 11 | types-bleach 12 | types-python-dateutil 13 | types-redis 14 | types-requests 15 | webtest 16 | -------------------------------------------------------------------------------- /tildes/requirements.in: -------------------------------------------------------------------------------- 1 | ago 2 | alembic 3 | argon2_cffi 4 | beautifulsoup4 5 | bleach 6 | click 7 | cornice 8 | gunicorn 9 | html5lib 10 | invoke 11 | ipython 12 | lupa 13 | marshmallow 14 | Pillow 15 | pip-tools 16 | prometheus-client 17 | psycopg2 18 | publicsuffix2==2.20160818 19 | pygit2 20 | Pygments 21 | pyotp 22 | pyramid<2.0 23 | pyramid-ipython 24 | pyramid-jinja2 25 | pyramid-session-redis==1.5.0 # 1.5.1 has a change that will invalidate current sessions 26 | pyramid-tm 27 | pyramid-webassets 28 | python-dateutil 29 | PyYAML # needs to be installed separately for webassets 30 | qrcode 31 | pip-tools 32 | redis 33 | requests 34 | sentry-sdk 35 | SQLAlchemy<1.4 36 | SQLAlchemy-Utils 37 | stripe 38 | titlecase 39 | webargs 40 | wrapt 41 | zope.sqlalchemy 42 | -------------------------------------------------------------------------------- /tildes/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains standalone scripts that exist outside the app.""" 2 | -------------------------------------------------------------------------------- /tildes/scripts/lift_expired_temporary_bans.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Simple script to lift any temporary bans that have expired. 5 | 6 | This script should be set up to run regularly (such as every hour). 7 | """ 8 | 9 | from tildes.lib.database import get_session_from_config 10 | from tildes.lib.datetime import utc_now 11 | from tildes.models.user import User 12 | 13 | 14 | def lift_expired_temporary_bans(config_path: str) -> None: 15 | """Lift temporary bans that have expired.""" 16 | db_session = get_session_from_config(config_path) 17 | 18 | db_session.query(User).filter( 19 | User.ban_expiry_time < utc_now(), # type: ignore 20 | User.is_banned == True, # noqa 21 | ).update({"is_banned": False, "ban_expiry_time": None}, synchronize_session=False) 22 | 23 | db_session.commit() 24 | -------------------------------------------------------------------------------- /tildes/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | // shrinks a font size by 0.1rem on mobile screen sizes 5 | @mixin font-shrink-on-mobile($base-size) { 6 | font-size: $base-size - 0.1rem; 7 | 8 | @media (min-width: $size-md) { 9 | font-size: $base-size; 10 | } 11 | } 12 | 13 | // makes sure the element is the "minimum touch size" on mobile 14 | @mixin min-touch-size() { 15 | min-width: $min-touch-size; 16 | min-height: $min-touch-size; 17 | 18 | @media (min-width: $size-md) { 19 | min-width: 0; 20 | min-height: 0; 21 | } 22 | } 23 | 24 | // Forcibly wrap text in the element if it can't be done "naturally", necessary for 25 | // handling long "words" in places like titles that might otherwise mess up the layout 26 | @mixin force-text-wrap-if-needed() { 27 | overflow-wrap: anywhere; 28 | 29 | @supports not (overflow-wrap: anywhere) { 30 | // Only Firefox supports overflow-wrap: anywhere so far, these two rules should be 31 | // fairly similar for other browsers 32 | overflow-wrap: break-word; 33 | word-break: break-word; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tildes/scss/_spectre_variables.scss: -------------------------------------------------------------------------------- 1 | // This file should be imported at the top of Spectre's _variables.scss 2 | // Since Spectre uses !default declarations, these values won't be overwritten 3 | 4 | @import "variables"; 5 | 6 | $primary-color: #268bd2; // Solarized 7 | 8 | // Remove the rounded corners on various elements 9 | $border-radius: 0; 10 | 11 | // Responsive breakpoints 12 | $size-xs: 480px; 13 | $size-sm: 600px; 14 | $size-md: $show-sidebar-width; 15 | $size-lg: 960px; 16 | $size-xl: 1200px; 17 | $size-2x: 1500px; // stylelint-disable scss/dollar-variable-pattern 18 | -------------------------------------------------------------------------------- /tildes/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $sidebar-width: 300px; 2 | 3 | // Viewport width that the sidebar is shown by default 4 | $show-sidebar-width: 840px; 5 | 6 | // Minimum size of buttons on small screens 7 | $min-touch-size: 26px; 8 | 9 | // Maximum width of the
element 10 | $main-max-width: 1400px; 11 | 12 | // Maximum width to allow on "paragraph-like" text 13 | $paragraph-max-width: 40rem; 14 | 15 | // The approximate size where the site's width "maxes out" (larger just adds margins) 16 | $size-max: $main-max-width + $sidebar-width; 17 | -------------------------------------------------------------------------------- /tildes/scss/modules/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .breadcrumb .breadcrumb-item { 5 | color: var(--foreground-secondary-color); 6 | 7 | &:not(:last-child) { 8 | a { 9 | color: var(--foreground-secondary-color); 10 | } 11 | } 12 | 13 | &:not(:first-child) { 14 | &::before { 15 | color: var(--foreground-secondary-color); 16 | } 17 | } 18 | 19 | &:last-child { 20 | a { 21 | color: var(--link-color); 22 | } 23 | } 24 | } 25 | 26 | ol.breadcrumb, 27 | ul.breadcrumb { 28 | margin-left: 0; 29 | font-size: 0.6rem; 30 | line-height: 0.8rem; 31 | 32 | a { 33 | text-decoration: none; 34 | 35 | &:hover { 36 | text-decoration: underline; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tildes/scss/modules/_chip.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .chip { 5 | border-radius: 0; 6 | 7 | background-color: var(--background-secondary-color); 8 | color: var(--foreground-highlight-color); 9 | 10 | &.active { 11 | background-color: var(--button-color); 12 | color: var(--button-by-brightness-color); 13 | 14 | .btn { 15 | color: var(--button-by-brightness-color); 16 | } 17 | } 18 | 19 | &.error { 20 | background-color: var(--error-color); 21 | 22 | color: var(--error-by-brightness-color); 23 | 24 | .btn { 25 | color: var(--error-by-brightness-color); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tildes/scss/modules/_divider.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .divider, 5 | .divider[data-content] { 6 | margin: 1rem; 7 | border-color: var(--border-color); 8 | } 9 | 10 | .divider[data-content]::after { 11 | color: var(--foreground-primary-color); 12 | background-color: var(--background-primary-color); 13 | } 14 | -------------------------------------------------------------------------------- /tildes/scss/modules/_dropdown.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .dropdown { 5 | .menu { 6 | animation: none; 7 | 8 | .btn-post-action { 9 | justify-content: left; 10 | width: 100%; 11 | 12 | &:hover { 13 | background-color: var(--background-secondary-color); 14 | } 15 | } 16 | } 17 | 18 | &.dropdown-bottom { 19 | .menu { 20 | top: auto; 21 | bottom: 100%; 22 | } 23 | } 24 | 25 | &-toggle.btn-post-action { 26 | height: auto; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tildes/scss/modules/_empty.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .empty { 5 | background: inherit; 6 | color: inherit; 7 | } 8 | 9 | .empty-subtitle { 10 | color: var(--foreground-secondary-color); 11 | } 12 | -------------------------------------------------------------------------------- /tildes/scss/modules/_group.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .group-list { 5 | margin: 1rem 0; 6 | 7 | ol { 8 | margin-left: 1rem; 9 | } 10 | 11 | li { 12 | text-indent: -1rem; 13 | margin-left: 1rem; 14 | } 15 | 16 | li + li { 17 | margin-top: 1rem; 18 | } 19 | 20 | .link-group { 21 | font-weight: bold; 22 | } 23 | } 24 | 25 | .group-list-activity { 26 | font-style: italic; 27 | font-size: 0.6rem; 28 | line-height: 0.8rem; 29 | } 30 | 31 | .group-list-item-not-subscribed { 32 | a.link-group { 33 | color: var(--warning-color); 34 | } 35 | } 36 | 37 | .group-sidebar-text { 38 | margin-top: 1rem; 39 | } 40 | 41 | .group-subscription { 42 | display: flex; 43 | align-items: center; 44 | margin: 1rem 0; 45 | 46 | .group-subscription-count, 47 | button { 48 | flex: 1; // makes the two elements equal width 49 | } 50 | 51 | .btn-used { 52 | border: 0; 53 | } 54 | } 55 | 56 | .group-subscription-count { 57 | white-space: nowrap; 58 | font-size: 0.6rem; 59 | text-align: center; 60 | margin-right: 0.2rem; 61 | } 62 | -------------------------------------------------------------------------------- /tildes/scss/modules/_heading.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .heading-main { 5 | font-weight: bold; 6 | } 7 | 8 | .heading-post-listing { 9 | font-size: 0.6rem; 10 | } 11 | 12 | .heading-notification { 13 | font-size: 0.8rem; 14 | line-height: 1rem; 15 | margin-bottom: 0.2rem; 16 | } 17 | -------------------------------------------------------------------------------- /tildes/scss/modules/_input.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .input-group .input-group-addon { 5 | border-color: inherit; 6 | background-color: var(--background-secondary-color); 7 | color: var(--foreground-highlight-color); 8 | } 9 | 10 | .input-invite-code { 11 | font-size: 0.6rem; 12 | margin-top: 0.4rem; 13 | } 14 | -------------------------------------------------------------------------------- /tildes/scss/modules/_link.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | a.link-user, 5 | a.link-group { 6 | white-space: nowrap; 7 | text-decoration: none; 8 | 9 | &:hover { 10 | text-decoration: underline; 11 | } 12 | 13 | &:visited { 14 | color: var(--link-color); 15 | } 16 | } 17 | 18 | .link-no-visited-color:visited { 19 | color: var(--link-color); 20 | } 21 | 22 | a.logged-in-user-alert { 23 | color: var(--alert-color); 24 | 25 | &:visited { 26 | color: var(--alert-color); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tildes/scss/modules/_listing.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .listing-options { 5 | display: flex; 6 | flex-wrap: wrap; 7 | align-items: center; 8 | margin-bottom: 0.4rem; 9 | 10 | // right-align the period dropdown at small sizes, left-align at larger 11 | justify-content: flex-end; 12 | 13 | @media (min-width: $size-md) { 14 | justify-content: flex-start; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tildes/scss/modules/_logged-in-user.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .logged-in-user-info { 5 | flex-direction: row-reverse; 6 | align-items: center; 7 | 8 | font-size: 0.8rem; 9 | 10 | a { 11 | @include min-touch-size; 12 | 13 | display: flex; 14 | align-items: center; 15 | } 16 | } 17 | 18 | .logged-in-user-alert { 19 | font-weight: bold; 20 | font-size: 0.5rem; 21 | } 22 | 23 | .logged-in-user-username, 24 | .logged-in-user-username:visited { 25 | color: var(--foreground-primary-color); 26 | } 27 | -------------------------------------------------------------------------------- /tildes/scss/modules/_menu.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .menu { 5 | box-shadow: none; 6 | border: 1px outset var(--border-color); 7 | 8 | background-color: var(--background-primary-color); 9 | 10 | .menu-item { 11 | > a:hover, 12 | > a:focus { 13 | background-color: transparent; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tildes/scss/modules/_message.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .message { 5 | margin-bottom: 0.4rem; 6 | border: 1px solid; 7 | border-color: inherit; 8 | 9 | header { 10 | display: flex; 11 | align-items: center; 12 | padding: 0.2rem; 13 | font-size: 0.7rem; 14 | line-height: 0.9rem; 15 | 16 | color: var(--foreground-highlight-color); 17 | background-color: var(--background-secondary-color); 18 | 19 | .link-user { 20 | margin-right: 0.2rem; 21 | } 22 | 23 | time { 24 | margin-left: 0.4rem; 25 | font-size: 0.6rem; 26 | } 27 | } 28 | } 29 | 30 | .message-text { 31 | @extend %text-container; 32 | 33 | margin-left: 0.2rem; 34 | overflow: auto; 35 | padding: 0.2rem; 36 | } 37 | 38 | .message-list-unread { 39 | .message-list-subject { 40 | font-weight: bold; 41 | } 42 | } 43 | 44 | .is-message-mine { 45 | margin-left: -2px; 46 | border-left: 3px solid var(--stripe-mine-color); 47 | } 48 | -------------------------------------------------------------------------------- /tildes/scss/modules/_pagination.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .page-item { 5 | font-size: 0.6rem; 6 | height: auto; 7 | line-height: normal; 8 | 9 | & + & { 10 | margin-left: 0.4rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tildes/scss/modules/_post.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .post-listing { 5 | > li { 6 | margin-bottom: 1rem; 7 | } 8 | 9 | .is-topic-mine, 10 | .is-topic-official { 11 | margin-left: -1px; 12 | } 13 | } 14 | 15 | .post-listing-notifications { 16 | .comment { 17 | margin-bottom: 0.4rem; 18 | } 19 | 20 | .btn-link-minimal { 21 | margin-left: 0.4rem; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tildes/scss/modules/_settings.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .settings-list { 5 | a:visited { 6 | color: var(--link-color); 7 | } 8 | 9 | li { 10 | margin-bottom: 1rem; 11 | } 12 | 13 | // Nested settings list 14 | ul { 15 | margin-top: 0; 16 | 17 | li { 18 | margin-bottom: unset; 19 | margin-top: 0; 20 | } 21 | } 22 | } 23 | 24 | .settings-section { 25 | .settings-list { 26 | margin-left: 1rem; 27 | } 28 | 29 | h2 { 30 | font-size: 1rem; 31 | border-bottom: 1px solid; 32 | border-color: inherit; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tildes/scss/modules/_site-footer.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | #site-footer { 5 | padding: 0.2rem; 6 | 7 | @media (min-width: $size-md) { 8 | padding-bottom: 1rem; 9 | } 10 | 11 | font-size: 0.6rem; 12 | text-align: center; 13 | 14 | a:visited { 15 | color: var(--link-color); 16 | } 17 | } 18 | 19 | .site-footer-links { 20 | display: flex; 21 | flex-wrap: wrap; 22 | justify-content: space-around; 23 | 24 | list-style-type: none; 25 | margin: 0 auto; 26 | max-width: 50rem; 27 | } 28 | 29 | .site-footer-link { 30 | @include min-touch-size; 31 | 32 | margin: 0; 33 | padding: 0 0.4rem; 34 | white-space: nowrap; 35 | } 36 | 37 | .site-footer-theme-selection { 38 | font-style: normal; 39 | margin-bottom: 1rem; 40 | 41 | select { 42 | width: auto; 43 | font-size: 0.6rem; 44 | height: 1.4rem; 45 | padding: 0 0 0 0.2rem; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tildes/scss/modules/_static-site.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | // Rules in this file are used by the Tildes static sites (Blog, Docs), not Tildes 5 | // itself. In general, rules in here should be kept to the absolute minimum, and only 6 | // used when necessary to transfer over styling to the static sites where it's not 7 | // feasible to do that through the separate CSS file in the repo for those sites. 8 | 9 | body.static-site { 10 | main { 11 | @extend %text-container; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tildes/scss/modules/_table.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .table td { 5 | border-bottom-color: var(--border-color); 6 | } 7 | 8 | .table th { 9 | border-bottom-color: var(--foreground-highlight-color); 10 | } 11 | 12 | .table-financials { 13 | max-width: $paragraph-max-width; 14 | margin-top: 1rem; 15 | margin-bottom: 2rem; 16 | 17 | .td-money { 18 | text-align: right; 19 | white-space: nowrap; 20 | } 21 | 22 | .tr-summary { 23 | font-weight: bold; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tildes/scss/modules/_theme-preview.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .theme-preview-blocks { 5 | display: flex; 6 | flex-wrap: wrap; 7 | max-width: $paragraph-max-width; 8 | } 9 | 10 | .theme-preview-block { 11 | min-width: 6rem; 12 | margin: 0.4rem; 13 | padding: 1rem; 14 | 15 | text-align: center; 16 | font-weight: bold; 17 | white-space: nowrap; 18 | } 19 | 20 | .theme-preview-fake-posts { 21 | // Disables all click events (and hover) on the fake posts so links/buttons don't work 22 | pointer-events: none; 23 | 24 | // Set a max width on the fake topics so the vote button isn't way off to the right 25 | .topic { 26 | max-width: $paragraph-max-width; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tildes/scss/modules/_time.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | // abbreviated timestamp - only displays on small screens 5 | .time-responsive::after { 6 | content: attr(data-abbreviated); 7 | 8 | @media (min-width: $size-lg) { 9 | display: none; 10 | } 11 | } 12 | 13 | // full timestamp - hidden on small screens 14 | .time-responsive-full { 15 | display: none; 16 | 17 | @media (min-width: $size-lg) { 18 | display: inline; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tildes/scss/modules/_toast.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | .toast { 5 | @extend %text-container; 6 | 7 | margin: 1rem 0; 8 | font-weight: bold; 9 | 10 | color: var(--foreground-highlight-color); 11 | background-color: var(--background-secondary-color); 12 | border-color: var(--border-color); 13 | 14 | a { 15 | color: var(--link-color); 16 | } 17 | 18 | ul { 19 | margin-bottom: 1rem; 20 | } 21 | } 22 | 23 | .toast-minor { 24 | font-size: 0.6rem; 25 | line-height: 0.9rem; 26 | font-weight: normal; 27 | margin: 0.4rem 0; 28 | 29 | h2 { 30 | font-size: 0.7rem; 31 | font-weight: bold; 32 | } 33 | } 34 | 35 | .toast.toast-warning { 36 | border-color: var(--warning-color); 37 | color: var(--warning-foreground-color); 38 | background-color: var(--warning-background-color); 39 | } 40 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_accordions.scss: -------------------------------------------------------------------------------- 1 | // Accordions 2 | .accordion { 3 | input:checked ~, 4 | &[open] { 5 | & .accordion-header { 6 | .icon { 7 | transform: rotate(90deg); 8 | } 9 | } 10 | 11 | & .accordion-body { 12 | max-height: 50rem; 13 | } 14 | } 15 | 16 | .accordion-header { 17 | display: block; 18 | padding: $unit-1 $unit-2; 19 | 20 | .icon { 21 | transition: all .2s ease; 22 | } 23 | } 24 | 25 | .accordion-body { 26 | margin-bottom: $layout-spacing; 27 | max-height: 0; 28 | overflow: hidden; 29 | transition: max-height .2s ease; 30 | } 31 | } 32 | 33 | // Remove default details marker in Webkit 34 | summary.accordion-header { 35 | &::-webkit-details-marker { 36 | display: none; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_animations.scss: -------------------------------------------------------------------------------- 1 | // Animations 2 | @keyframes loading { 3 | 0% { 4 | transform: rotate(0deg); 5 | } 6 | 100% { 7 | transform: rotate(360deg); 8 | } 9 | } 10 | 11 | @keyframes slide-down { 12 | 0% { 13 | opacity: 0; 14 | transform: translateY(-$unit-8); 15 | } 16 | 100% { 17 | opacity: 1; 18 | transform: translateY(0); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_asian.scss: -------------------------------------------------------------------------------- 1 | // Optimized for East Asian CJK 2 | :lang(zh) { 3 | font-family: $cjk-zh-font-family; 4 | } 5 | 6 | :lang(ja) { 7 | font-family: $cjk-jp-font-family; 8 | } 9 | 10 | :lang(ko) { 11 | font-family: $cjk-ko-font-family; 12 | } 13 | 14 | :lang(zh), 15 | :lang(ja), 16 | .cjk { 17 | ins, 18 | u { 19 | border-bottom: $border-width solid; 20 | text-decoration: none; 21 | } 22 | 23 | del + del, 24 | del + s, 25 | ins + ins, 26 | ins + u, 27 | s + del, 28 | s + s, 29 | u + ins, 30 | u + u { 31 | margin-left: .125em; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_autocomplete.scss: -------------------------------------------------------------------------------- 1 | // Autocomplete 2 | .form-autocomplete { 3 | position: relative; 4 | 5 | .form-autocomplete-input { 6 | align-content: flex-start; 7 | display: flex; 8 | flex-wrap: wrap; 9 | height: auto; 10 | min-height: $unit-8; 11 | padding: $unit-h; 12 | 13 | &.is-focused { 14 | @include control-shadow(); 15 | border-color: $primary-color; 16 | } 17 | 18 | .form-input { 19 | border-color: transparent; 20 | box-shadow: none; 21 | display: inline-block; 22 | flex: 1 0 auto; 23 | height: $unit-6; 24 | line-height: $unit-4; 25 | margin: $unit-h; 26 | width: auto; 27 | } 28 | } 29 | 30 | .menu { 31 | left: 0; 32 | position: absolute; 33 | top: 100%; 34 | width: 100%; 35 | } 36 | 37 | &.autocomplete-oneline { 38 | .form-autocomplete-input { 39 | flex-wrap: nowrap; 40 | overflow-x: auto; 41 | } 42 | 43 | .chip { 44 | flex: 1 0 auto; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_base.scss: -------------------------------------------------------------------------------- 1 | // Base 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: inherit; 6 | } 7 | 8 | html { 9 | box-sizing: border-box; 10 | font-size: $html-font-size; 11 | line-height: $html-line-height; 12 | -webkit-tap-highlight-color: transparent; 13 | } 14 | 15 | body { 16 | background: $body-bg; 17 | color: $body-font-color; 18 | font-family: $body-font-family; 19 | font-size: $font-size; 20 | overflow-x: hidden; 21 | text-rendering: optimizeLegibility; 22 | } 23 | 24 | a { 25 | color: $link-color; 26 | outline: none; 27 | text-decoration: none; 28 | 29 | &:focus { 30 | @include control-shadow(); 31 | } 32 | 33 | &:focus, 34 | &:hover, 35 | &:active, 36 | &.active { 37 | color: $link-color-dark; 38 | text-decoration: underline; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // Breadcrumbs 2 | .breadcrumb { 3 | list-style: none; 4 | margin: $unit-1 0; 5 | padding: $unit-1 0; 6 | 7 | .breadcrumb-item { 8 | color: $gray-color-dark; 9 | display: inline-block; 10 | margin: 0; 11 | padding: $unit-1 0; 12 | 13 | &:not(:last-child) { 14 | margin-right: $unit-1; 15 | 16 | a { 17 | color: $gray-color-dark; 18 | } 19 | } 20 | 21 | &:not(:first-child) { 22 | &::before { 23 | color: $gray-color-light; 24 | content: "/"; 25 | padding-right: $unit-2; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_cards.scss: -------------------------------------------------------------------------------- 1 | // Cards 2 | .card { 3 | background: $bg-color-light; 4 | border: $border-width solid $border-color; 5 | border-radius: $border-radius; 6 | display: flex; 7 | flex-direction: column; 8 | 9 | .card-header, 10 | .card-body, 11 | .card-footer { 12 | padding: $layout-spacing-lg; 13 | padding-bottom: 0; 14 | 15 | &:last-child { 16 | padding-bottom: $layout-spacing-lg; 17 | } 18 | } 19 | 20 | .card-image { 21 | padding-top: $layout-spacing-lg; 22 | 23 | &:first-child { 24 | padding-top: 0; 25 | 26 | img { 27 | border-top-left-radius: $border-radius; 28 | border-top-right-radius: $border-radius; 29 | } 30 | } 31 | 32 | &:last-child { 33 | img { 34 | border-bottom-left-radius: $border-radius; 35 | border-bottom-right-radius: $border-radius; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_chips.scss: -------------------------------------------------------------------------------- 1 | // Chips 2 | .chip { 3 | align-items: center; 4 | background: $bg-color-dark; 5 | border-radius: 5rem; 6 | color: $gray-color-dark; 7 | display: inline-flex; 8 | font-size: 90%; 9 | height: $unit-6; 10 | line-height: $unit-4; 11 | margin: $unit-h; 12 | max-width: 100%; 13 | padding: $unit-1 $unit-2; 14 | text-decoration: none; 15 | vertical-align: middle; 16 | 17 | &.active { 18 | background: $primary-color; 19 | color: $light-color; 20 | } 21 | 22 | .avatar { 23 | margin-left: -$unit-2; 24 | margin-right: $unit-1; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_codes.scss: -------------------------------------------------------------------------------- 1 | // Codes 2 | code { 3 | @include label-base(); 4 | @include label-variant($code-color, lighten($code-color, 33%)); 5 | font-size: 85%; 6 | } 7 | 8 | .code { 9 | border-radius: $border-radius; 10 | color: $body-font-color; 11 | position: relative; 12 | 13 | &::before { 14 | color: $gray-color; 15 | content: attr(data-lang); 16 | font-size: $font-size-sm; 17 | position: absolute; 18 | right: $layout-spacing; 19 | top: $unit-h; 20 | } 21 | 22 | code { 23 | background: $bg-color; 24 | color: inherit; 25 | display: block; 26 | line-height: 1.5; 27 | overflow-x: auto; 28 | padding: 1rem; 29 | width: 100%; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_dropdowns.scss: -------------------------------------------------------------------------------- 1 | // Dropdown 2 | .dropdown { 3 | display: inline-block; 4 | position: relative; 5 | 6 | .menu { 7 | animation: slide-down .15s ease 1; 8 | display: none; 9 | left: 0; 10 | max-height: 50vh; 11 | overflow-y: auto; 12 | position: absolute; 13 | top: 100%; 14 | } 15 | 16 | &.dropdown-right { 17 | .menu { 18 | left: auto; 19 | right: 0; 20 | } 21 | } 22 | 23 | &.active .menu, 24 | .dropdown-toggle:focus + .menu, 25 | .menu:hover { 26 | display: block; 27 | } 28 | 29 | // Fix dropdown-toggle border radius in button groups 30 | .btn-group { 31 | .dropdown-toggle:nth-last-child(2) { 32 | border-bottom-right-radius: $border-radius; 33 | border-top-right-radius: $border-radius; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_empty.scss: -------------------------------------------------------------------------------- 1 | // Empty states (or Blank slates) 2 | .empty { 3 | background: $bg-color; 4 | border-radius: $border-radius; 5 | color: $gray-color-dark; 6 | text-align: center; 7 | padding: $unit-16 $unit-8; 8 | 9 | .empty-icon { 10 | margin-bottom: $layout-spacing-lg; 11 | } 12 | 13 | .empty-title, 14 | .empty-subtitle { 15 | margin: $layout-spacing auto; 16 | } 17 | 18 | .empty-action { 19 | margin-top: $layout-spacing-lg; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_filters.scss: -------------------------------------------------------------------------------- 1 | // Filters 2 | // The number of filter options 3 | $filter-number: 8 !default; 4 | 5 | %filter-checked-nav { 6 | background: $primary-color; 7 | color: $light-color; 8 | } 9 | 10 | %filter-checked-body { 11 | display: none; 12 | } 13 | 14 | .filter { 15 | .filter-nav { 16 | margin: $layout-spacing 0; 17 | } 18 | 19 | .filter-body { 20 | display: flex; 21 | flex-wrap: wrap; 22 | } 23 | 24 | .filter-tag { 25 | @for $i from 0 through ($filter-number) { 26 | &#tag-#{$i}:checked ~ .filter-nav .chip[for="tag-#{$i}"] { 27 | @extend %filter-checked-nav; 28 | } 29 | } 30 | 31 | @for $i from 1 through ($filter-number) { 32 | &#tag-#{$i}:checked ~ .filter-body .filter-item:not([data-tag~="tag-#{$i}"]) { 33 | @extend %filter-checked-body; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_icons.scss: -------------------------------------------------------------------------------- 1 | // CSS Icons 2 | @import "icons/icons-core"; 3 | @import "icons/icons-navigation"; 4 | @import "icons/icons-action"; 5 | @import "icons/icons-object"; -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_labels.scss: -------------------------------------------------------------------------------- 1 | // Labels 2 | .label { 3 | @include label-base(); 4 | @include label-variant(lighten($body-font-color, 5%), $bg-color-dark); 5 | display: inline-block; 6 | 7 | // Label rounded 8 | &.label-rounded { 9 | border-radius: 5rem; 10 | padding-left: .4rem; 11 | padding-right: .4rem; 12 | } 13 | 14 | // Label colors 15 | &.label-primary { 16 | @include label-variant($light-color, $primary-color); 17 | } 18 | 19 | &.label-secondary { 20 | @include label-variant($primary-color, $secondary-color); 21 | } 22 | 23 | &.label-success { 24 | @include label-variant($light-color, $success-color); 25 | } 26 | 27 | &.label-warning { 28 | @include label-variant($light-color, $warning-color); 29 | } 30 | 31 | &.label-error { 32 | @include label-variant($light-color, $error-color); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | @import "mixins/avatar"; 3 | @import "mixins/button"; 4 | @import "mixins/clearfix"; 5 | @import "mixins/color"; 6 | @import "mixins/label"; 7 | @import "mixins/position"; 8 | @import "mixins/shadow"; 9 | @import "mixins/text"; 10 | @import "mixins/toast"; 11 | @import "mixins/transition"; 12 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_navbar.scss: -------------------------------------------------------------------------------- 1 | // Navbar 2 | .navbar { 3 | align-items: stretch; 4 | display: flex; 5 | flex-wrap: wrap; 6 | justify-content: space-between; 7 | 8 | .navbar-section { 9 | align-items: center; 10 | display: flex; 11 | flex: 1 0 0; 12 | 13 | &:not(:first-child):last-child { 14 | justify-content: flex-end; 15 | } 16 | } 17 | 18 | .navbar-center { 19 | align-items: center; 20 | display: flex; 21 | flex: 0 0 auto; 22 | } 23 | 24 | .navbar-brand { 25 | font-size: $font-size-lg; 26 | font-weight: 500; 27 | text-decoration: none; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_navs.scss: -------------------------------------------------------------------------------- 1 | // Navs 2 | .nav { 3 | display: flex; 4 | flex-direction: column; 5 | list-style: none; 6 | margin: $unit-1 0; 7 | 8 | .nav-item { 9 | a { 10 | color: $gray-color-dark; 11 | padding: $unit-1 $unit-2; 12 | text-decoration: none; 13 | &:focus, 14 | &:hover { 15 | color: $primary-color; 16 | } 17 | } 18 | &.active { 19 | & > a { 20 | color: darken($gray-color-dark, 10%); 21 | font-weight: bold; 22 | &:focus, 23 | &:hover { 24 | color: $primary-color; 25 | } 26 | } 27 | } 28 | } 29 | 30 | & .nav { 31 | margin-bottom: $unit-2; 32 | margin-left: $unit-4; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_panels.scss: -------------------------------------------------------------------------------- 1 | // Panels 2 | .panel { 3 | border: $border-width solid $border-color; 4 | border-radius: $border-radius; 5 | display: flex; 6 | flex-direction: column; 7 | 8 | .panel-header, 9 | .panel-footer { 10 | flex: 0 0 auto; 11 | padding: $layout-spacing-lg; 12 | } 13 | 14 | .panel-nav { 15 | flex: 0 0 auto; 16 | } 17 | 18 | .panel-body { 19 | flex: 1 1 auto; 20 | overflow-y: auto; 21 | padding: 0 $layout-spacing-lg; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_tables.scss: -------------------------------------------------------------------------------- 1 | // Tables 2 | .table { 3 | border-collapse: collapse; 4 | border-spacing: 0; 5 | width: 100%; 6 | @if $rtl == true { 7 | text-align: right; 8 | } @else { 9 | text-align: left; 10 | } 11 | 12 | &.table-striped { 13 | tbody { 14 | tr:nth-of-type(odd) { 15 | background: $bg-color; 16 | } 17 | } 18 | } 19 | 20 | &, 21 | &.table-striped { 22 | tbody { 23 | tr { 24 | &.active { 25 | background: $bg-color-dark; 26 | } 27 | } 28 | } 29 | } 30 | 31 | &.table-hover { 32 | tbody { 33 | tr { 34 | &:hover { 35 | background: $bg-color-dark; 36 | } 37 | } 38 | } 39 | } 40 | 41 | // Tables with horizontal scrollbar 42 | &.table-scroll { 43 | display: block; 44 | overflow-x: auto; 45 | padding-bottom: .75rem; 46 | white-space: nowrap; 47 | } 48 | 49 | td, 50 | th { 51 | border-bottom: $border-width solid $border-color; 52 | padding: $unit-3 $unit-2; 53 | } 54 | th { 55 | border-bottom-width: $border-width-lg; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_tiles.scss: -------------------------------------------------------------------------------- 1 | // Tiles 2 | .tile { 3 | align-content: space-between; 4 | align-items: flex-start; 5 | display: flex; 6 | 7 | .tile-icon, 8 | .tile-action { 9 | flex: 0 0 auto; 10 | } 11 | .tile-content { 12 | flex: 1 1 auto; 13 | &:not(:first-child) { 14 | padding-left: $unit-2; 15 | } 16 | &:not(:last-child) { 17 | padding-right: $unit-2; 18 | } 19 | } 20 | .tile-title, 21 | .tile-subtitle { 22 | line-height: $line-height; 23 | } 24 | 25 | &.tile-centered { 26 | align-items: center; 27 | 28 | .tile-content { 29 | overflow: hidden; 30 | } 31 | 32 | .tile-title, 33 | .tile-subtitle { 34 | @include text-ellipsis(); 35 | margin-bottom: 0; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_toasts.scss: -------------------------------------------------------------------------------- 1 | // Toasts 2 | .toast { 3 | @include toast-variant($dark-color); 4 | border: $border-width solid $dark-color; 5 | border-radius: $border-radius; 6 | color: $light-color; 7 | display: block; 8 | padding: $layout-spacing; 9 | width: 100%; 10 | 11 | &.toast-primary { 12 | @include toast-variant($primary-color); 13 | } 14 | 15 | &.toast-success { 16 | @include toast-variant($success-color); 17 | } 18 | 19 | &.toast-warning { 20 | @include toast-variant($warning-color); 21 | } 22 | 23 | &.toast-error { 24 | @include toast-variant($error-color); 25 | } 26 | 27 | a { 28 | color: $light-color; 29 | text-decoration: underline; 30 | 31 | &:focus, 32 | &:hover, 33 | &:active, 34 | &.active { 35 | opacity: .75; 36 | } 37 | } 38 | 39 | .btn-clear { 40 | margin: 4px -2px 4px 4px; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/_utilities.scss: -------------------------------------------------------------------------------- 1 | @import "utilities/colors"; 2 | @import "utilities/cursors"; 3 | @import "utilities/display"; 4 | @import "utilities/divider"; 5 | @import "utilities/loading"; 6 | @import "utilities/position"; 7 | @import "utilities/shapes"; 8 | @import "utilities/text"; 9 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/icons/_icons-core.scss: -------------------------------------------------------------------------------- 1 | // Icon variables 2 | $icon-border-width: $border-width-lg; 3 | $icon-prefix: "icon"; 4 | 5 | // Icon base style 6 | .#{$icon-prefix} { 7 | box-sizing: border-box; 8 | display: inline-block; 9 | font-size: inherit; 10 | font-style: normal; 11 | height: 1em; 12 | position: relative; 13 | text-indent: -9999px; 14 | vertical-align: middle; 15 | width: 1em; 16 | &::before, 17 | &::after { 18 | display: block; 19 | left: 50%; 20 | position: absolute; 21 | top: 50%; 22 | transform: translate(-50%, -50%); 23 | } 24 | 25 | // Icon sizes 26 | &.icon-2x { 27 | font-size: 1.6rem; 28 | } 29 | 30 | &.icon-3x { 31 | font-size: 2.4rem; 32 | } 33 | 34 | &.icon-4x { 35 | font-size: 3.2rem; 36 | } 37 | } 38 | 39 | // Component icon support 40 | .accordion, 41 | .btn, 42 | .toast, 43 | .menu { 44 | .#{$icon-prefix} { 45 | vertical-align: -10%; 46 | } 47 | } 48 | 49 | .btn-lg { 50 | .#{$icon-prefix} { 51 | vertical-align: -15%; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/mixins/_avatar.scss: -------------------------------------------------------------------------------- 1 | // Avatar mixin 2 | @mixin avatar-base($size: $unit-8) { 3 | font-size: $size / 2; 4 | height: $size; 5 | width: $size; 6 | } 7 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/mixins/_clearfix.scss: -------------------------------------------------------------------------------- 1 | // Clearfix mixin 2 | @mixin clearfix() { 3 | &::after { 4 | clear: both; 5 | content: ""; 6 | display: table; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/mixins/_color.scss: -------------------------------------------------------------------------------- 1 | // Background color utility mixin 2 | @mixin bg-color-variant($name: ".bg-primary", $color: $primary-color) { 3 | #{$name} { 4 | background: $color; 5 | 6 | @if (lightness($color) < 60) { 7 | color: $light-color; 8 | } 9 | } 10 | } 11 | 12 | // Text color utility mixin 13 | @mixin text-color-variant($name: ".text-primary", $color: $primary-color) { 14 | #{$name} { 15 | color: $color; 16 | } 17 | 18 | a#{$name} { 19 | &:focus, 20 | &:hover { 21 | color: darken($color, 5%); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/mixins/_label.scss: -------------------------------------------------------------------------------- 1 | // Label base style 2 | @mixin label-base() { 3 | border-radius: $border-radius; 4 | line-height: 1.2; 5 | padding: .1rem .15rem; 6 | } 7 | 8 | @mixin label-variant($color: $light-color, $bg-color: $primary-color) { 9 | background: $bg-color; 10 | color: $color; 11 | } 12 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/mixins/_shadow.scss: -------------------------------------------------------------------------------- 1 | // Component focus shadow 2 | @mixin control-shadow($color: $primary-color) { 3 | box-shadow: 0 0 0 .1rem rgba($color, .2); 4 | } 5 | 6 | // Shadow mixin 7 | @mixin shadow-variant($offset) { 8 | box-shadow: 0 $offset ($offset + .05rem) * 2 rgba($dark-color, .3); 9 | } 10 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/mixins/_text.scss: -------------------------------------------------------------------------------- 1 | // Text Ellipsis 2 | @mixin text-ellipsis() { 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | } 7 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/mixins/_toast.scss: -------------------------------------------------------------------------------- 1 | // Toast variant mixin 2 | @mixin toast-variant($color: $dark-color) { 3 | background: rgba($color, .9); 4 | border-color: $color; 5 | } 6 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/mixins/_transition.scss: -------------------------------------------------------------------------------- 1 | // Component transition 2 | @mixin control-transition() { 3 | transition: all .2s ease; 4 | } 5 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/spectre-exp.scss: -------------------------------------------------------------------------------- 1 | // Variables and mixins 2 | @import "variables"; 3 | @import "mixins"; 4 | 5 | /*! Spectre.css Experimentals v#{$version} | MIT License | github.com/picturepan2/spectre */ 6 | // Experimentals 7 | @import "autocomplete"; 8 | @import "calendars"; 9 | @import "carousels"; 10 | @import "comparison-sliders"; 11 | @import "filters"; 12 | @import "meters"; 13 | @import "off-canvas"; 14 | @import "parallax"; 15 | @import "progress"; 16 | @import "sliders"; 17 | @import "timelines"; 18 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/spectre-icons.scss: -------------------------------------------------------------------------------- 1 | // Variables and mixins 2 | @import "variables"; 3 | @import "mixins"; 4 | 5 | /*! Spectre.css Icons v#{$version} | MIT License | github.com/picturepan2/spectre */ 6 | // Icons 7 | @import "icons/icons-core"; 8 | @import "icons/icons-navigation"; 9 | @import "icons/icons-action"; 10 | @import "icons/icons-object"; 11 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/spectre.scss: -------------------------------------------------------------------------------- 1 | // Variables and mixins 2 | @import "variables"; 3 | @import "mixins"; 4 | 5 | /*! Spectre.css v#{$version} | MIT License | github.com/picturepan2/spectre */ 6 | // Reset and dependencies 7 | @import "normalize"; 8 | @import "base"; 9 | 10 | // Elements 11 | @import "typography"; 12 | @import "asian"; 13 | @import "tables"; 14 | @import "buttons"; 15 | @import "forms"; 16 | @import "labels"; 17 | @import "codes"; 18 | @import "media"; 19 | 20 | // Layout 21 | @import "layout"; 22 | @import "navbar"; 23 | 24 | // Components 25 | @import "accordions"; 26 | @import "avatars"; 27 | @import "badges"; 28 | @import "breadcrumbs"; 29 | @import "bars"; 30 | @import "cards"; 31 | @import "chips"; 32 | @import "dropdowns"; 33 | @import "empty"; 34 | @import "menus"; 35 | @import "modals"; 36 | @import "navs"; 37 | @import "pagination"; 38 | @import "panels"; 39 | @import "popovers"; 40 | @import "steps"; 41 | @import "tabs"; 42 | @import "tiles"; 43 | @import "toasts"; 44 | @import "tooltips"; 45 | 46 | // Utility classes 47 | @import "animations"; 48 | @import "autocomplete"; 49 | @import "utilities"; 50 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/utilities/_colors.scss: -------------------------------------------------------------------------------- 1 | // Text colors 2 | @include text-color-variant(".text-primary", $primary-color); 3 | 4 | @include text-color-variant(".text-secondary", $secondary-color-dark); 5 | 6 | @include text-color-variant(".text-gray", $gray-color); 7 | 8 | @include text-color-variant(".text-light", $light-color); 9 | 10 | @include text-color-variant(".text-success", $success-color); 11 | 12 | @include text-color-variant(".text-warning", $warning-color); 13 | 14 | @include text-color-variant(".text-error", $error-color); 15 | 16 | // Background colors 17 | @include bg-color-variant(".bg-primary", $primary-color); 18 | 19 | @include bg-color-variant(".bg-secondary", $secondary-color); 20 | 21 | @include bg-color-variant(".bg-dark", $dark-color); 22 | 23 | @include bg-color-variant(".bg-gray", $bg-color); 24 | 25 | @include bg-color-variant(".bg-success", $success-color); 26 | 27 | @include bg-color-variant(".bg-warning", $warning-color); 28 | 29 | @include bg-color-variant(".bg-error", $error-color); 30 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/utilities/_cursors.scss: -------------------------------------------------------------------------------- 1 | // Cursors 2 | .c-hand { 3 | cursor: pointer; 4 | } 5 | 6 | .c-move { 7 | cursor: move; 8 | } 9 | 10 | .c-zoom-in { 11 | cursor: zoom-in; 12 | } 13 | 14 | .c-zoom-out { 15 | cursor: zoom-out; 16 | } 17 | 18 | .c-not-allowed { 19 | cursor: not-allowed; 20 | } 21 | 22 | .c-auto { 23 | cursor: auto; 24 | } 25 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/utilities/_display.scss: -------------------------------------------------------------------------------- 1 | // Display 2 | .d-block { 3 | display: block; 4 | } 5 | .d-inline { 6 | display: inline; 7 | } 8 | .d-inline-block { 9 | display: inline-block; 10 | } 11 | .d-flex { 12 | display: flex; 13 | } 14 | .d-inline-flex { 15 | display: inline-flex; 16 | } 17 | .d-none, 18 | .d-hide { 19 | display: none !important; 20 | } 21 | .d-visible { 22 | visibility: visible; 23 | } 24 | .d-invisible { 25 | visibility: hidden; 26 | } 27 | .text-hide { 28 | background: transparent; 29 | border: 0; 30 | color: transparent; 31 | font-size: 0; 32 | line-height: 0; 33 | text-shadow: none; 34 | } 35 | .text-assistive { 36 | border: 0; 37 | clip: rect(0,0,0,0); 38 | height: 1px; 39 | margin: -1px; 40 | overflow: hidden; 41 | padding: 0; 42 | position: absolute; 43 | width: 1px; 44 | } 45 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/utilities/_divider.scss: -------------------------------------------------------------------------------- 1 | // Divider 2 | .divider, 3 | .divider-vert { 4 | display: block; 5 | position: relative; 6 | 7 | &[data-content]::after { 8 | background: $bg-color-light; 9 | color: $gray-color; 10 | content: attr(data-content); 11 | display: inline-block; 12 | font-size: $font-size-sm; 13 | padding: 0 $unit-2; 14 | transform: translateY(-$font-size-sm + $border-width); 15 | } 16 | } 17 | 18 | .divider { 19 | border-top: $border-width solid $border-color; 20 | height: $border-width; 21 | margin: $unit-2 0; 22 | 23 | &[data-content] { 24 | margin: $unit-4 0; 25 | } 26 | } 27 | 28 | .divider-vert { 29 | display: block; 30 | padding: $unit-4; 31 | 32 | &::before { 33 | border-left: $border-width solid $border-color; 34 | bottom: $unit-2; 35 | content: ""; 36 | display: block; 37 | left: 50%; 38 | position: absolute; 39 | top: $unit-2; 40 | transform: translateX(-50%); 41 | } 42 | 43 | &[data-content]::after { 44 | left: 50%; 45 | padding: $unit-1 0; 46 | position: absolute; 47 | top: 50%; 48 | transform: translate(-50%, -50%); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/utilities/_loading.scss: -------------------------------------------------------------------------------- 1 | // Loading 2 | .loading { 3 | color: transparent !important; 4 | min-height: $unit-4; 5 | pointer-events: none; 6 | position: relative; 7 | &::after { 8 | animation: loading 500ms infinite linear; 9 | border: $border-width-lg solid $primary-color; 10 | border-radius: 50%; 11 | border-right-color: transparent; 12 | border-top-color: transparent; 13 | content: ""; 14 | display: block; 15 | height: $unit-4; 16 | left: 50%; 17 | margin-left: -$unit-2; 18 | margin-top: -$unit-2; 19 | position: absolute; 20 | top: 50%; 21 | width: $unit-4; 22 | z-index: $zindex-0; 23 | } 24 | 25 | &.loading-lg { 26 | min-height: $unit-10; 27 | &::after { 28 | height: $unit-8; 29 | margin-left: -$unit-4; 30 | margin-top: -$unit-4; 31 | width: $unit-8; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/utilities/_position.scss: -------------------------------------------------------------------------------- 1 | // Position 2 | .clearfix { 3 | @include clearfix(); 4 | } 5 | 6 | .float-left { 7 | float: left !important; 8 | } 9 | 10 | .float-right { 11 | float: right !important; 12 | } 13 | 14 | .relative { 15 | position: relative; 16 | } 17 | 18 | .absolute { 19 | position: absolute; 20 | } 21 | 22 | .fixed { 23 | position: fixed; 24 | } 25 | 26 | .centered { 27 | display: block; 28 | float: none; 29 | margin-left: auto; 30 | margin-right: auto; 31 | } 32 | 33 | .flex-centered { 34 | align-items: center; 35 | display: flex; 36 | justify-content: center; 37 | } 38 | 39 | // Spacing 40 | @include margin-variant(0, 0); 41 | 42 | @include margin-variant(1, $unit-1); 43 | 44 | @include margin-variant(2, $unit-2); 45 | 46 | @include padding-variant(0, 0); 47 | 48 | @include padding-variant(1, $unit-1); 49 | 50 | @include padding-variant(2, $unit-2); 51 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/utilities/_shapes.scss: -------------------------------------------------------------------------------- 1 | // Shapes 2 | .rounded { 3 | border-radius: $border-radius; 4 | } 5 | 6 | .circle { 7 | border-radius: 50%; 8 | } 9 | -------------------------------------------------------------------------------- /tildes/scss/spectre-0.5.1/utilities/_text.scss: -------------------------------------------------------------------------------- 1 | // Text 2 | // Text alignment utilities 3 | .text-left { 4 | text-align: left; 5 | } 6 | 7 | .text-right { 8 | text-align: right; 9 | } 10 | 11 | .text-center { 12 | text-align: center; 13 | } 14 | 15 | .text-justify { 16 | text-align: justify; 17 | } 18 | 19 | // Text transform utilities 20 | .text-lowercase { 21 | text-transform: lowercase; 22 | } 23 | 24 | .text-uppercase { 25 | text-transform: uppercase; 26 | } 27 | 28 | .text-capitalize { 29 | text-transform: capitalize; 30 | } 31 | 32 | // Text style utilities 33 | .text-normal { 34 | font-weight: normal; 35 | } 36 | 37 | .text-bold { 38 | font-weight: bold; 39 | } 40 | 41 | .text-italic { 42 | font-style: italic; 43 | } 44 | 45 | .text-large { 46 | font-size: 1.2em; 47 | } 48 | 49 | // Text overflow utilities 50 | .text-ellipsis { 51 | @include text-ellipsis(); 52 | } 53 | 54 | .text-clip { 55 | overflow: hidden; 56 | text-overflow: clip; 57 | white-space: nowrap; 58 | } 59 | 60 | .text-break { 61 | hyphens: auto; 62 | word-break: break-word; 63 | word-wrap: break-word; 64 | } 65 | -------------------------------------------------------------------------------- /tildes/setup.py: -------------------------------------------------------------------------------- 1 | """Extremely minimal setup.py to support pip install -e.""" 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | setup( 7 | name="tildes", 8 | version="0.1", 9 | packages=find_packages(), 10 | entry_points=""" 11 | [paste.app_factory] 12 | main = tildes:main 13 | """, 14 | ) 15 | -------------------------------------------------------------------------------- /tildes/sql/init/functions/event_stream.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2020 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | create or replace function add_to_event_stream(stream_name_pieces text[], fields text[]) returns void as $$ 5 | select pg_notify( 6 | 'postgresql_events', 7 | array_to_string(stream_name_pieces, '.') || ':' || json_object(fields) 8 | ); 9 | $$ language sql; 10 | -------------------------------------------------------------------------------- /tildes/sql/init/functions/utils.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2019 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | CREATE OR REPLACE FUNCTION id36_to_id(id36 TEXT) RETURNS INTEGER AS $$ 5 | from tildes.lib.id import id36_to_id 6 | 7 | return id36_to_id(id36) 8 | $$ IMMUTABLE LANGUAGE plpython3u; 9 | -------------------------------------------------------------------------------- /tildes/sql/init/insert_base_data.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2018 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | -- add an "unknown user" for re-assigning deleted comments to after they're 5 | -- outside the retention period, and similar uses 6 | INSERT INTO users (user_id, username, password_hash) 7 | VALUES (0, 'unknown user', ''); 8 | 9 | -- add a generic "Tildes" user to attribute automatic actions to 10 | INSERT INTO users (user_id, username, password_hash) 11 | VALUES (-1, 'Tildes', ''); 12 | -------------------------------------------------------------------------------- /tildes/sql/init/triggers/comment_labels/event_stream.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2020 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | create or replace function comment_labels_events_trigger() returns trigger as $$ 5 | declare 6 | affected_row record := coalesce(NEW, OLD); 7 | stream_name_pieces text[] := array[TG_TABLE_NAME, lower(TG_OP)]::text[] || TG_ARGV; 8 | 9 | -- in general, only the below declaration of payload_fields should be edited 10 | payload_fields text[] := array[ 11 | 'comment_id', affected_row.comment_id, 12 | 'user_id', affected_row.user_id, 13 | 'label', affected_row.label 14 | ]::text[]; 15 | begin 16 | perform add_to_event_stream(stream_name_pieces, payload_fields); 17 | 18 | return null; 19 | end; 20 | $$ language plpgsql; 21 | 22 | create trigger comment_labels_events_insert_delete 23 | after insert or delete on comment_labels 24 | for each row 25 | execute function comment_labels_events_trigger(); 26 | -------------------------------------------------------------------------------- /tildes/sql/init/triggers/comment_labels/users.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2018 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | CREATE OR REPLACE FUNCTION update_user_last_exemplary_label_time() RETURNS TRIGGER AS $$ 5 | BEGIN 6 | UPDATE users 7 | SET last_exemplary_label_time = NOW() 8 | WHERE user_id = NEW.user_id; 9 | 10 | RETURN NULL; 11 | END 12 | $$ LANGUAGE plpgsql; 13 | 14 | 15 | CREATE TRIGGER update_user_last_exemplary_label_time 16 | AFTER INSERT ON comment_labels 17 | FOR EACH ROW 18 | WHEN (NEW.label = 'EXEMPLARY') 19 | EXECUTE PROCEDURE update_user_last_exemplary_label_time(); 20 | -------------------------------------------------------------------------------- /tildes/sql/init/triggers/comment_votes/comments.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2018 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | CREATE OR REPLACE FUNCTION update_comment_num_votes() RETURNS TRIGGER AS $$ 5 | BEGIN 6 | IF (TG_OP = 'INSERT') THEN 7 | UPDATE comments 8 | SET num_votes = num_votes + 1 9 | WHERE comment_id = NEW.comment_id; 10 | ELSIF (TG_OP = 'DELETE') THEN 11 | -- Exclude comments with closed voting from decrements so that individual vote 12 | -- records can be deleted while retaining the final vote total. 13 | UPDATE comments 14 | SET num_votes = num_votes - 1 15 | WHERE comment_id = OLD.comment_id 16 | AND is_voting_closed = FALSE; 17 | END IF; 18 | 19 | RETURN NULL; 20 | END 21 | $$ LANGUAGE plpgsql; 22 | 23 | 24 | CREATE TRIGGER update_comment_num_votes 25 | AFTER INSERT OR DELETE ON comment_votes 26 | FOR EACH ROW 27 | EXECUTE PROCEDURE update_comment_num_votes(); 28 | -------------------------------------------------------------------------------- /tildes/sql/init/triggers/comments/comment_notifications.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2018 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | -- delete any notifications related to a comment when it's deleted or removed 5 | CREATE OR REPLACE FUNCTION delete_comment_notifications() RETURNS TRIGGER AS $$ 6 | BEGIN 7 | DELETE FROM comment_notifications 8 | WHERE comment_id = OLD.comment_id; 9 | 10 | RETURN NULL; 11 | END; 12 | $$ LANGUAGE plpgsql; 13 | 14 | CREATE TRIGGER delete_comment_notifications_update 15 | AFTER UPDATE ON comments 16 | FOR EACH ROW 17 | WHEN ((OLD.is_deleted = false AND NEW.is_deleted = true) 18 | OR (OLD.is_removed = false AND NEW.is_removed = true)) 19 | EXECUTE PROCEDURE delete_comment_notifications(); 20 | -------------------------------------------------------------------------------- /tildes/sql/init/triggers/group_subscriptions/groups.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2018 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | CREATE OR REPLACE FUNCTION update_group_subscription_count() RETURNS TRIGGER AS $$ 5 | BEGIN 6 | IF (TG_OP = 'INSERT') THEN 7 | UPDATE groups 8 | SET num_subscriptions = num_subscriptions + 1 9 | WHERE group_id = NEW.group_id; 10 | ELSIF (TG_OP = 'DELETE') THEN 11 | UPDATE groups 12 | SET num_subscriptions = num_subscriptions - 1 13 | WHERE group_id = OLD.group_id; 14 | END IF; 15 | 16 | RETURN NULL; 17 | END 18 | $$ LANGUAGE plpgsql; 19 | 20 | 21 | CREATE TRIGGER update_group_subscription_count 22 | AFTER INSERT OR DELETE ON group_subscriptions 23 | FOR EACH ROW 24 | EXECUTE PROCEDURE update_group_subscription_count(); 25 | -------------------------------------------------------------------------------- /tildes/sql/init/triggers/scraper_results/event_stream.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2020 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | create or replace function scraper_results_events_trigger() returns trigger as $$ 5 | declare 6 | affected_row record := coalesce(NEW, OLD); 7 | stream_name_pieces text[] := array[TG_TABLE_NAME, lower(TG_OP)]::text[] || TG_ARGV; 8 | 9 | -- in general, only the below declaration of payload_fields should be edited 10 | payload_fields text[] := array[ 11 | 'result_id', affected_row.result_id 12 | ]::text[]; 13 | begin 14 | perform add_to_event_stream(stream_name_pieces, payload_fields); 15 | 16 | return null; 17 | end; 18 | $$ language plpgsql; 19 | 20 | create trigger scraper_results_events_insert_delete 21 | after insert or delete on scraper_results 22 | for each row 23 | execute function scraper_results_events_trigger(); 24 | -------------------------------------------------------------------------------- /tildes/sql/init/triggers/topic_visits/topic_visits.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2020 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | -- prevent inserting another visit immediately after an existing one 5 | create or replace function prevent_recent_repeat_visits() returns trigger as $$ 6 | begin 7 | perform * from topic_visits 8 | where user_id = NEW.user_id 9 | and topic_id = NEW.topic_id 10 | and visit_time >= now() - interval '30 seconds'; 11 | 12 | if (FOUND) then 13 | return null; 14 | else 15 | return NEW; 16 | end if; 17 | end; 18 | $$ language plpgsql; 19 | 20 | 21 | create trigger prevent_recent_repeat_visits_insert 22 | before insert on topic_visits 23 | for each row 24 | execute procedure prevent_recent_repeat_visits(); 25 | -------------------------------------------------------------------------------- /tildes/sql/init/triggers/topic_votes/topics.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2018 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | CREATE OR REPLACE FUNCTION update_topic_num_votes() RETURNS TRIGGER AS $$ 5 | BEGIN 6 | IF (TG_OP = 'INSERT') THEN 7 | UPDATE topics 8 | SET num_votes = num_votes + 1 9 | WHERE topic_id = NEW.topic_id; 10 | ELSIF (TG_OP = 'DELETE') THEN 11 | -- Exclude topics with closed voting from decrements so that individual vote 12 | -- records can be deleted while retaining the final vote total. 13 | UPDATE topics 14 | SET num_votes = num_votes - 1 15 | WHERE topic_id = OLD.topic_id 16 | AND is_voting_closed = FALSE; 17 | END IF; 18 | 19 | RETURN NULL; 20 | END 21 | $$ LANGUAGE plpgsql; 22 | 23 | 24 | CREATE TRIGGER update_topic_num_votes 25 | AFTER INSERT OR DELETE ON topic_votes 26 | FOR EACH ROW 27 | EXECUTE PROCEDURE update_topic_num_votes(); 28 | -------------------------------------------------------------------------------- /tildes/sql/init/triggers/users/users.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2018 Tildes contributors 2 | -- SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | -- set user.deleted_time when it's deleted 5 | CREATE OR REPLACE FUNCTION set_user_deleted_time() RETURNS TRIGGER AS $$ 6 | BEGIN 7 | NEW.deleted_time := current_timestamp; 8 | 9 | RETURN NEW; 10 | END; 11 | $$ LANGUAGE plpgsql; 12 | 13 | CREATE TRIGGER delete_user_set_deleted_time_update 14 | BEFORE UPDATE ON users 15 | FOR EACH ROW 16 | WHEN (OLD.is_deleted = false AND NEW.is_deleted = true) 17 | EXECUTE PROCEDURE set_user_deleted_time(); 18 | 19 | 20 | -- set user.banned_time when it's banned 21 | CREATE OR REPLACE FUNCTION set_user_banned_time() RETURNS TRIGGER AS $$ 22 | BEGIN 23 | NEW.banned_time := current_timestamp; 24 | 25 | RETURN NEW; 26 | END; 27 | $$ LANGUAGE plpgsql; 28 | 29 | CREATE TRIGGER ban_user_set_banned_time_update 30 | BEFORE UPDATE ON users 31 | FOR EACH ROW 32 | WHEN (OLD.is_banned = false AND NEW.is_banned = true) 33 | EXECUTE PROCEDURE set_user_banned_time(); 34 | -------------------------------------------------------------------------------- /tildes/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectria/tildes/436c3f24f0409b5ee6811daf73df7d0bd72fb894/tildes/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /tildes/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectria/tildes/436c3f24f0409b5ee6811daf73df7d0bd72fb894/tildes/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /tildes/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectria/tildes/436c3f24f0409b5ee6811daf73df7d0bd72fb894/tildes/static/apple-touch-icon.png -------------------------------------------------------------------------------- /tildes/static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #002b36 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tildes/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectria/tildes/436c3f24f0409b5ee6811daf73df7d0bd72fb894/tildes/static/favicon-16x16.png -------------------------------------------------------------------------------- /tildes/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectria/tildes/436c3f24f0409b5ee6811daf73df7d0bd72fb894/tildes/static/favicon-32x32.png -------------------------------------------------------------------------------- /tildes/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectria/tildes/436c3f24f0409b5ee6811daf73df7d0bd72fb894/tildes/static/favicon.ico -------------------------------------------------------------------------------- /tildes/static/images/site-icons/README: -------------------------------------------------------------------------------- 1 | This folder holds the site-icons (favicons) downloaded by the "site_icon_downloader" consumer. 2 | -------------------------------------------------------------------------------- /tildes/static/images/tildes-logo-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectria/tildes/436c3f24f0409b5ee6811daf73df7d0bd72fb894/tildes/static/images/tildes-logo-144x144.png -------------------------------------------------------------------------------- /tildes/static/js/behaviors/auto-focus.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-auto-focus]", function() { 5 | var $input = $(this); 6 | 7 | // just calling .focus() will place the cursor at the start of the field, 8 | // so un-setting and re-setting the value moves the cursor to the end 9 | var original_val = $input.val(); 10 | $input 11 | .focus() 12 | .val("") 13 | .val(original_val); 14 | }); 15 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/autocomplete-menu.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-autocomplete-menu]", function() { 5 | var $autocompleteContainer = $(this) 6 | .parents("[data-js-autocomplete-container]") 7 | .first(); 8 | var $chips = $autocompleteContainer.find("[data-js-autocomplete-chips]").first(); 9 | 10 | $(this) 11 | .children("[data-js-autocomplete-menu-item]") 12 | .each(function(index, $menuItem) { 13 | $menuItem.setAttribute("tabindex", $chips.children().length + index); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/autoselect-input.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-autoselect-input]", function() { 5 | $(this).click(function() { 6 | $(this).select(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/autosubmit-on-change.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-autosubmit-on-change]", function() { 5 | $(this).change(function() { 6 | $(this) 7 | .closest("form") 8 | .submit(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/cancel-button.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-cancel-button]", function() { 5 | $(this).click(function() { 6 | var $parentForm = $(this).closest("form"); 7 | 8 | var shouldRemove = true; 9 | 10 | // confirm removal if the form specifies to 11 | var confirmPrompt = $parentForm.attr("data-js-confirm-cancel"); 12 | if (confirmPrompt) { 13 | // only prompt if any of the inputs aren't empty 14 | var $nonEmptyFields = $parentForm.find("input,textarea").filter(function() { 15 | return $(this).val(); 16 | }); 17 | 18 | if ($nonEmptyFields.length > 0) { 19 | shouldRemove = window.confirm(confirmPrompt); 20 | } else { 21 | shouldRemove = true; 22 | } 23 | } 24 | 25 | if (shouldRemove) { 26 | $(this) 27 | .closest("form") 28 | .remove(); 29 | } 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/comment-collapse-all-button.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-comment-collapse-all-button]", function() { 5 | $(this).click(function() { 6 | // first uncollapse any individually collapsed comments 7 | $(".is-comment-collapsed-individual").each(function(idx, child) { 8 | $(child) 9 | .find("[data-js-comment-collapse-button]:first") 10 | .trigger("click"); 11 | }); 12 | 13 | // then collapse all first-level replies 14 | $('.comment[data-comment-depth="1"]:not(.is-comment-collapsed)').each(function( 15 | idx, 16 | child 17 | ) { 18 | $(child) 19 | .find("[data-js-comment-collapse-button]:first") 20 | .trigger("click"); 21 | }); 22 | 23 | $(this).blur(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/comment-collapse-button.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-comment-collapse-button]", function() { 5 | $(this).click(function() { 6 | var $this = $(this); 7 | var $comment = $this.closest(".comment"); 8 | 9 | // if the comment is individually collapsed, just remove that class, 10 | // otherwise toggle the collapsed state 11 | if ($comment.hasClass("is-comment-collapsed-individual")) { 12 | $comment.removeClass("is-comment-collapsed-individual"); 13 | } else { 14 | $comment.toggleClass("is-comment-collapsed"); 15 | } 16 | 17 | $this.blur(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/comment-expand-all-button.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-comment-expand-all-button]", function() { 5 | $(this).click(function() { 6 | $(".is-comment-collapsed, .is-comment-collapsed-individual").each(function( 7 | idx, 8 | child 9 | ) { 10 | $(child) 11 | .find("[data-js-comment-collapse-button]:first") 12 | .trigger("click"); 13 | }); 14 | 15 | $(this).blur(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/comment-parent-button.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-comment-parent-button]", function() { 5 | $(this).click(function() { 6 | var $comment = $(this) 7 | .parents(".comment") 8 | .first(); 9 | var $parentComment = $comment.parents(".comment").first(); 10 | 11 | var backButton = document.createElement("a"); 12 | backButton.setAttribute( 13 | "href", 14 | "#comment-" + $comment.attr("data-comment-id36") 15 | ); 16 | backButton.setAttribute("class", "comment-nav-link"); 17 | backButton.setAttribute("data-js-comment-back-button", ""); 18 | backButton.setAttribute("data-js-remove-on-click", ""); 19 | backButton.innerHTML = "[Back]"; 20 | 21 | var $parentHeader = $parentComment.find("header").first(); 22 | 23 | // remove any existing back button 24 | $parentHeader.find("[data-js-comment-back-button]").remove(); 25 | 26 | $parentHeader.append(backButton); 27 | $.onmount(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/confirm-leave-page-unsaved.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-confirm-leave-page-unsaved]", function() { 5 | var $form = $(this); 6 | $form.areYouSure(); 7 | 8 | // Fixes a strange interaction between Intercooler and AreYouSure, where 9 | // submitting a form by using the keyboard to push the submit button would 10 | // trigger a confirmation prompt before leaving the page. 11 | $form.on("success.ic", function() { 12 | $form.removeClass("dirty"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/ctrl-enter-submit-form.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-ctrl-enter-submit-form]", function() { 5 | $(this).keydown(function(event) { 6 | if ( 7 | (event.ctrlKey || event.metaKey) && 8 | (event.keyCode == 13 || event.keyCode == 10) 9 | ) { 10 | $(this) 11 | .closest("form") 12 | .submit(); 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/external-links-new-tabs.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-external-links-new-tabs]", function() { 5 | // Open external links in topic, comment, and message text in new tabs 6 | $(this) 7 | .find("a") 8 | .each(function() { 9 | if (this.host !== window.location.host) { 10 | $(this).attr("target", "_blank"); 11 | $(this).attr("rel", "noopener"); 12 | } 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/fadeout-parent-on-success.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-fadeout-parent-on-success]", function() { 5 | $(this).on("after.success.ic", function() { 6 | $(this) 7 | .parent() 8 | .fadeOut("fast"); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/group-links-new-tabs.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-group-links-new-tabs]", function() { 5 | // Open links to groups on Tildes in new tabs 6 | $(this) 7 | .find(".link-group") 8 | .each(function() { 9 | $(this).attr("target", "_blank"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/hide-sidebar-if-open.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-hide-sidebar-if-open]", function() { 5 | $(this).on("click", function(event) { 6 | if ($("#sidebar").hasClass("is-sidebar-displayed")) { 7 | event.preventDefault(); 8 | event.stopPropagation(); 9 | $("#sidebar").removeClass("is-sidebar-displayed"); 10 | } 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/hide-sidebar-no-preventdefault.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-hide-sidebar-no-preventdefault]", function() { 5 | $(this).on("click", function() { 6 | $("#sidebar").removeClass("is-sidebar-displayed"); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/markdown-edit-tab.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-markdown-edit-tab]", function() { 5 | $(this).click(function() { 6 | var $editTextarea = $(this) 7 | .closest("form") 8 | .find('[name="markdown"]'); 9 | var $previewDiv = $(this) 10 | .closest("form") 11 | .find(".form-markdown-preview"); 12 | 13 | $editTextarea.removeClass("d-none"); 14 | $previewDiv.addClass("d-none"); 15 | $previewDiv.empty(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/markdown-preview-tab.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-markdown-preview-tab]", function() { 5 | $(this).click(function() { 6 | var $editTextarea = $(this) 7 | .closest("form") 8 | .find('[name="markdown"]'); 9 | var $previewDiv = $(this) 10 | .closest("form") 11 | .find(".form-markdown-preview"); 12 | var $previewErrors = $(this) 13 | .closest("form") 14 | .find(".text-status-message.text-error"); 15 | 16 | $editTextarea.addClass("d-none"); 17 | $previewDiv.removeClass("d-none"); 18 | $previewErrors.remove(); 19 | }); 20 | 21 | $(this).on("after.success.ic success.ic", function(event) { 22 | // Stop intercooler event from bubbling up past this button. This 23 | // prevents behaviors on parent elements from mistaking a successful 24 | // "preview" from a successful "submit". 25 | event.stopPropagation(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/prevent-double-submit.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-prevent-double-submit]", function() { 5 | /* eslint-disable-next-line no-unused-vars */ 6 | $(this).on("beforeSend.ic", function(evt, elt, data, settings, xhr, requestId) { 7 | var $form = $(this); 8 | 9 | if ($form.attr("data-js-submitting") !== undefined) { 10 | xhr.abort(); 11 | return false; 12 | } else { 13 | $form.attr("data-js-submitting", true); 14 | } 15 | }); 16 | 17 | $(this).on("complete.ic", function() { 18 | $(this).removeAttr("data-js-submitting"); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/remove-on-click.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-remove-on-click]", function() { 5 | $(this).on("click", function() { 6 | $(this).remove(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/remove-on-success.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-remove-on-success]", function() { 5 | $(this).on("after.success.ic", function() { 6 | $(this).remove(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/sidebar-toggle.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-sidebar-toggle]", function() { 5 | $(this).click(function(event) { 6 | event.preventDefault(); 7 | event.stopPropagation(); 8 | 9 | $("#sidebar").toggleClass("is-sidebar-displayed"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/stripe-checkout.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-stripe-checkout]", function() { 5 | /* eslint-disable-next-line no-undef */ 6 | var stripe = Stripe($(this).attr("data-js-stripe-checkout")); 7 | stripe.redirectToCheckout({ 8 | sessionId: $(this).attr("data-js-stripe-checkout-session") 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/stripe-donate-form.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-stripe-donate-form]", function() { 5 | $(this).on("submit", function(event) { 6 | var $amountInput = $(this).find("#amount"); 7 | var amount = $amountInput.val(); 8 | 9 | var $errorDiv = $(this).find(".text-status-message"); 10 | 11 | // remove dollar sign and/or comma, then parse into float 12 | amount = amount.replace(/[$,]/g, ""); 13 | amount = parseFloat(amount); 14 | 15 | if (isNaN(amount)) { 16 | $errorDiv.text("Please enter a valid dollar amount."); 17 | event.preventDefault(); 18 | return; 19 | } else if (amount < 1.0) { 20 | $errorDiv.text("Donation amount must be at least $1."); 21 | event.preventDefault(); 22 | return; 23 | } 24 | 25 | // set the value in case any of the replacements happened 26 | $amountInput.val(amount); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/tab.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-tab]", function() { 5 | $(this).click(function() { 6 | $(this) 7 | .siblings() 8 | .removeClass("active"); 9 | $(this).addClass("active"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/theme-preview.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-theme-preview]", function() { 5 | $(this).click(function() { 6 | var newTheme = $(this).attr("data-js-theme-preview"); 7 | 8 | Tildes.changeTheme(newTheme); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tildes/static/js/behaviors/user-links-new-tabs.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Tildes contributors 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | $.onmount("[data-js-user-links-new-tabs]", function() { 5 | // Open links to users on Tildes in new tabs 6 | $(this) 7 | .find(".link-user") 8 | .each(function() { 9 | $(this).attr("target", "_blank"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tildes/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tildes", 3 | "short_name": "Tildes", 4 | "start_url": "/", 5 | "display": "browser", 6 | "icons": [ 7 | { 8 | "src": "/android-chrome-192x192.png", 9 | "sizes": "192x192", 10 | "type": "image/png" 11 | }, 12 | { 13 | "src": "/android-chrome-512x512.png", 14 | "sizes": "512x512", 15 | "type": "image/png" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tildes/static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectria/tildes/436c3f24f0409b5ee6811daf73df7d0bd72fb894/tildes/static/mstile-150x150.png -------------------------------------------------------------------------------- /tildes/static/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 16 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tildes/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains the app's tests (run with pytest).""" 2 | -------------------------------------------------------------------------------- /tildes/tests/test_hash.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | from tildes.lib.hash import hash_string, is_match_for_hash 5 | 6 | 7 | def test_same_string_verifies(): 8 | """Ensure that the same string will match the hashed result.""" 9 | string = "hunter2" 10 | hashed = hash_string(string) 11 | assert is_match_for_hash(string, hashed) 12 | 13 | 14 | def test_different_string_fails(): 15 | """Ensure that a different string doesn't match the hash.""" 16 | string = "correct horse battery staple" 17 | wrong_string = "incorrect horse battery staple" 18 | 19 | hashed = hash_string(string) 20 | assert not is_match_for_hash(wrong_string, hashed) 21 | -------------------------------------------------------------------------------- /tildes/tests/test_metrics.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | from tildes.metrics import _COUNTERS, _HISTOGRAMS, _SUMMARIES 5 | 6 | 7 | def test_all_metric_names_prefixed(): 8 | """Ensure all metric names have the 'tildes_' prefix.""" 9 | for metric_dict in (_COUNTERS, _HISTOGRAMS, _SUMMARIES): 10 | metrics = metric_dict.values() 11 | for metric in metrics: 12 | # this is ugly, but seems to be the "generic" way to get the name 13 | metric_name = metric.describe()[0].name 14 | 15 | assert metric_name.startswith("tildes_") 16 | -------------------------------------------------------------------------------- /tildes/tests/test_webassets.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | from webassets.loaders import YAMLLoader 5 | 6 | 7 | WEBASSETS_ENV = YAMLLoader("webassets.yaml").load_environment() 8 | 9 | 10 | def test_scripts_file_first_in_bundle(): 11 | """Ensure that the main scripts.js file will be at the top.""" 12 | js_bundle = WEBASSETS_ENV["javascript"] 13 | 14 | first_filename = js_bundle.resolve_contents()[0][0] 15 | 16 | assert first_filename == "js/scripts.js" 17 | 18 | 19 | def test_styles_file_last_in_bundle(): 20 | """Ensure that the main styles.css file will be at the bottom.""" 21 | css_bundle = WEBASSETS_ENV["css"] 22 | 23 | last_filename = css_bundle.resolve_contents()[-1][0] 24 | 25 | assert last_filename == "css/styles.css" 26 | -------------------------------------------------------------------------------- /tildes/tests/webtests/test_login.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | 5 | def test_login_no_external_redirect(webtest_loggedout): 6 | """Ensure that the login page won't redirect to an external site.""" 7 | login_page = webtest_loggedout.get( 8 | "/login", params={"from_url": "http://example.com"} 9 | ) 10 | login_page.form["username"] = "SessionUser" 11 | login_page.form["password"] = "session user password" 12 | response = login_page.form.submit() 13 | 14 | assert response.status_int == 302 15 | assert "example.com" not in response.headers["Location"] 16 | -------------------------------------------------------------------------------- /tildes/tests/webtests/test_user_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | 5 | def test_render_theme_options(webtest): 6 | """Test that theme settings are being rendered.""" 7 | settings = webtest.get("/settings") 8 | assert settings.status_int == 200 9 | assert settings.text.count("(site and account default)") == 1 10 | assert "(site default)" not in settings.text 11 | assert "(account default)" not in settings.text 12 | -------------------------------------------------------------------------------- /tildes/tests/webtests/test_w3_validator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | import subprocess 5 | 6 | from pytest import mark 7 | 8 | 9 | # marks all tests in this module with "html_validation" marker 10 | pytestmark = mark.html_validation 11 | 12 | 13 | def test_homepage_html_loggedout(webtest_loggedout): 14 | """Validate HTML5 on the Tildes homepage, logged out.""" 15 | homepage = webtest_loggedout.get("/") 16 | _run_html5validator(homepage.body) 17 | 18 | 19 | def test_homepage_html_loggedin(webtest): 20 | """Validate HTML5 on the Tildes homepage, logged in.""" 21 | homepage = webtest.get("/") 22 | _run_html5validator(homepage.body) 23 | 24 | 25 | def _run_html5validator(html): 26 | """Raises CalledProcessError on validation error.""" 27 | result = subprocess.run(["html5validator", "-"], input=html) 28 | result.check_returncode() 29 | -------------------------------------------------------------------------------- /tildes/tildes/database_models.py: -------------------------------------------------------------------------------- 1 | """Module that imports all DatabaseModel subclasses. 2 | 3 | This module shouldn't really be used for anything directly. It's for convenience so that 4 | both Alembic and the script for initializing the database can simply import * from here. 5 | """ 6 | # pylint: disable=unused-import 7 | 8 | from tildes.models.comment import ( 9 | Comment, 10 | CommentBookmark, 11 | CommentLabel, 12 | CommentNotification, 13 | CommentVote, 14 | ) 15 | from tildes.models.financials import Financials 16 | from tildes.models.group import Group, GroupScript, GroupStat, GroupSubscription 17 | from tildes.models.log import Log 18 | from tildes.models.message import MessageConversation, MessageReply 19 | from tildes.models.scraper import ScraperResult 20 | from tildes.models.topic import ( 21 | Topic, 22 | TopicBookmark, 23 | TopicIgnore, 24 | TopicSchedule, 25 | TopicVisit, 26 | TopicVote, 27 | ) 28 | from tildes.models.user import User, UserGroupSettings, UserInviteCode, UserRateLimit 29 | -------------------------------------------------------------------------------- /tildes/tildes/lib/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains the overall "library" for the application. 2 | 3 | Defining constants, behavior, etc. inside modules here (as opposed to other locations 4 | such as in models) is encouraged, since it often makes it simpler to import elsewhere 5 | for tests, when only a specific constant value is needed, etc. 6 | 7 | Modules here should *never* import anything from models, to avoid circular dependencies. 8 | """ 9 | -------------------------------------------------------------------------------- /tildes/tildes/lib/url.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Functions related to URLs.""" 5 | 6 | from urllib.parse import urlparse 7 | 8 | 9 | def get_domain_from_url(url: str, strip_www: bool = True) -> str: 10 | """Return the domain name from a url.""" 11 | domain = urlparse(url).netloc 12 | 13 | if not domain: 14 | raise ValueError("Invalid url or domain could not be determined") 15 | 16 | if strip_www and domain.startswith("www."): 17 | domain = domain[4:] 18 | 19 | return domain 20 | -------------------------------------------------------------------------------- /tildes/tildes/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains classes for the application's models.""" 2 | 3 | from .database_model import DatabaseModel 4 | from .model_query import ModelQuery 5 | -------------------------------------------------------------------------------- /tildes/tildes/models/comment/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains models related to comments.""" 2 | 3 | from .comment import Comment, EDIT_GRACE_PERIOD, VOTING_PERIOD 4 | from .comment_bookmark import CommentBookmark 5 | from .comment_label import CommentLabel 6 | from .comment_notification import CommentNotification 7 | from .comment_notification_query import CommentNotificationQuery 8 | from .comment_query import CommentQuery 9 | from .comment_tree import CommentInTree, CommentTree 10 | from .comment_vote import CommentVote 11 | -------------------------------------------------------------------------------- /tildes/tildes/models/group/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains models related to groups.""" 2 | 3 | from .group import Group 4 | from .group_query import GroupQuery 5 | from .group_script import GroupScript 6 | from .group_stat import GroupStat 7 | from .group_subscription import GroupSubscription 8 | from .group_wiki_page import GroupWikiPage 9 | -------------------------------------------------------------------------------- /tildes/tildes/models/log/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains models related to logs.""" 2 | 3 | from .log import Log, LogComment, LogTopic 4 | -------------------------------------------------------------------------------- /tildes/tildes/models/message/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains models related to messages.""" 2 | 3 | from .message import MessageConversation, MessageReply 4 | -------------------------------------------------------------------------------- /tildes/tildes/models/scraper/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains models related to scrapers.""" 2 | 3 | from .scraper_result import ScraperResult 4 | -------------------------------------------------------------------------------- /tildes/tildes/models/topic/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains models related to topics.""" 2 | 3 | from .topic import EDIT_GRACE_PERIOD, Topic, VOTING_PERIOD 4 | from .topic_bookmark import TopicBookmark 5 | from .topic_ignore import TopicIgnore 6 | from .topic_query import TopicQuery 7 | from .topic_schedule import TopicSchedule 8 | from .topic_visit import TopicVisit 9 | from .topic_vote import TopicVote 10 | -------------------------------------------------------------------------------- /tildes/tildes/models/user/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains models related to users.""" 2 | 3 | from .user import User 4 | from .user_group_settings import UserGroupSettings 5 | from .user_invite_code import UserInviteCode 6 | from .user_permissions import UserPermissions 7 | from .user_rate_limit import UserRateLimit 8 | -------------------------------------------------------------------------------- /tildes/tildes/resources/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Contains Pyramid "resource" related code, such as root factories.""" 5 | 6 | from pyramid.httpexceptions import HTTPForbidden, HTTPNotFound 7 | from pyramid.request import Request 8 | 9 | from tildes.models import DatabaseModel, ModelQuery 10 | 11 | 12 | def get_resource(request: Request, base_query: ModelQuery) -> DatabaseModel: 13 | """Prepare and execute base query from a root factory, returning result.""" 14 | # pylint: disable=unused-argument 15 | query = ( 16 | base_query.lock_based_on_request_method() 17 | .join_all_relationships() 18 | .undefer_all_columns() 19 | ) 20 | 21 | resource = query.one_or_none() 22 | 23 | if not resource: 24 | raise HTTPNotFound 25 | 26 | return resource 27 | -------------------------------------------------------------------------------- /tildes/tildes/resources/message.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Root factories for messages.""" 5 | 6 | from pyramid.request import Request 7 | 8 | from tildes.lib.id import id36_to_id 9 | from tildes.models.message import MessageConversation 10 | from tildes.resources import get_resource 11 | from tildes.schemas.message import MessageConversationSchema 12 | from tildes.views.decorators import use_kwargs 13 | 14 | 15 | @use_kwargs( 16 | MessageConversationSchema(only=("conversation_id36",)), location="matchdict" 17 | ) 18 | def message_conversation_by_id36( 19 | request: Request, conversation_id36: str 20 | ) -> MessageConversation: 21 | """Get a conversation specified by {conversation_id36} in the route.""" 22 | query = request.query(MessageConversation).filter_by( 23 | conversation_id=id36_to_id(conversation_id36) 24 | ) 25 | 26 | return get_resource(request, query) 27 | -------------------------------------------------------------------------------- /tildes/tildes/resources/user.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Root factories for users.""" 5 | 6 | from pyramid.request import Request 7 | 8 | from tildes.models.user import User 9 | from tildes.resources import get_resource 10 | from tildes.schemas.user import UserSchema 11 | from tildes.views.decorators import use_kwargs 12 | 13 | 14 | @use_kwargs(UserSchema(only=("username",)), location="matchdict") 15 | def user_by_username(request: Request, username: str) -> User: 16 | """Get a user specified by {username} in the route or 404 if not found.""" 17 | query = request.query(User).include_deleted().filter(User.username == username) 18 | 19 | return get_resource(request, query) 20 | -------------------------------------------------------------------------------- /tildes/tildes/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains schemas, which define data (e.g. for models) in more depth. 2 | 3 | These schemas are currently being used for several purposes: 4 | 5 | - Validation of data for models, such as checking the lengths of strings, ensuring 6 | that they match a particular regex pattern, etc. Specific errors can be generated 7 | for any data that is invalid. 8 | 9 | - Similarly, the webargs library uses the schemas to validate pieces of data coming in 10 | via urls, POST data, etc. It can produce errors if the data is not valid for the 11 | purpose it's intended for. 12 | 13 | - Serialization of data, which the Pyramid JSON renderer uses to produce data for the 14 | JSON API endpoints. 15 | """ 16 | -------------------------------------------------------------------------------- /tildes/tildes/schemas/comment.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Validation/dumping schema for comments.""" 5 | 6 | from marshmallow import Schema 7 | 8 | from tildes.enums import CommentLabelOption 9 | from tildes.schemas.fields import Enum, ID36, Markdown, SimpleString 10 | 11 | 12 | class CommentSchema(Schema): 13 | """Marshmallow schema for comments.""" 14 | 15 | comment_id36 = ID36() 16 | markdown = Markdown() 17 | parent_comment_id36 = ID36() 18 | 19 | 20 | class CommentLabelSchema(Schema): 21 | """Marshmallow schema for comment labels.""" 22 | 23 | name = Enum(CommentLabelOption) 24 | reason = SimpleString(max_length=1000, missing=None) 25 | -------------------------------------------------------------------------------- /tildes/tildes/schemas/group_wiki_page.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Validation/dumping schema for group wiki pages.""" 5 | 6 | from marshmallow import Schema 7 | 8 | from tildes.schemas.fields import Markdown, SimpleString 9 | 10 | 11 | PAGE_NAME_MAX_LENGTH = 40 12 | 13 | 14 | class GroupWikiPageSchema(Schema): 15 | """Marshmallow schema for group wiki pages.""" 16 | 17 | page_name = SimpleString(max_length=PAGE_NAME_MAX_LENGTH) 18 | markdown = Markdown(max_length=1_000_000) 19 | -------------------------------------------------------------------------------- /tildes/tildes/schemas/message.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Validation/dumping schemas for messages.""" 5 | 6 | from marshmallow import Schema 7 | from marshmallow.fields import DateTime, String 8 | 9 | from tildes.schemas.fields import ID36, Markdown, SimpleString 10 | 11 | 12 | SUBJECT_MAX_LENGTH = 200 13 | 14 | 15 | class MessageConversationSchema(Schema): 16 | """Marshmallow schema for message conversations.""" 17 | 18 | conversation_id36 = ID36() 19 | subject = SimpleString(max_length=SUBJECT_MAX_LENGTH) 20 | markdown = Markdown() 21 | rendered_html = String(dump_only=True) 22 | created_time = DateTime(dump_only=True) 23 | 24 | 25 | class MessageReplySchema(Schema): 26 | """Marshmallow schema for message replies.""" 27 | 28 | reply_id36 = ID36() 29 | markdown = Markdown() 30 | rendered_html = String(dump_only=True) 31 | created_time = DateTime(dump_only=True) 32 | -------------------------------------------------------------------------------- /tildes/tildes/scrapers/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains scrapers.""" 2 | 3 | from .embedly_scraper import EmbedlyScraper 4 | from .exceptions import ScraperError 5 | from .youtube_scraper import YoutubeScraper 6 | -------------------------------------------------------------------------------- /tildes/tildes/scrapers/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Exception classes related to scraping.""" 5 | 6 | 7 | class ScraperError(Exception): 8 | """Exception class for an error while scraping.""" 9 | 10 | pass 11 | -------------------------------------------------------------------------------- /tildes/tildes/templates/base.atom.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2021 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later -#} 3 | 4 | 5 | 6 | 7 | {% block feed_title %}Tildes Atom feed{% endblock %} 8 | {% block feed_id %}{{ request.current_route_url() }}{% endblock %} 9 | 10 | {% block feed_updated %}{{ current_time.strftime("%Y-%m-%dT%H:%M:%SZ") }}{% endblock %} 11 | 12 | {% block feed_entries %}{% endblock %} 13 | 14 | 15 | -------------------------------------------------------------------------------- /tildes/tildes/templates/base.rss.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2021 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later -#} 3 | 4 | 5 | 6 | 7 | 8 | {% block channel_title %}Tildes{% endblock %} 9 | {% block channel_link %}https://tildes.net/{% endblock %} 10 | {% block channel_description %}Tildes RSS feed{% endblock %} 11 | 12 | {% block channel_items %}{% endblock %} 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tildes/tildes/templates/base_no_sidebar.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'base.jinja2' %} 5 | 6 | {% block body_tag %} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /tildes/tildes/templates/base_settings.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2019 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'base_no_sidebar.jinja2' %} 5 | 6 | {% block main_classes %}text-formatted{% endblock %} 7 | 8 | {% block content %} 9 | {% block settings %}{% endblock %} 10 | 11 |
12 | 13 | Back to main Settings page 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /tildes/tildes/templates/base_user_menu.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'base.jinja2' %} 5 | 6 | {% from 'macros/user_menu.jinja2' import render_user_menu with context %} 7 | 8 | {% block sidebar %} 9 | {{ render_user_menu() }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /tildes/tildes/templates/donate_stripe_redirect.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'base_no_sidebar.jinja2' %} 5 | 6 | {% block title %}Stripe donation{% endblock %} 7 | 8 | {% block content %} 9 | 10 | 11 |

Redirecting to Stripe...

12 | 13 | {# This div will cause the page to redirect to the Stripe Checkout page #} 14 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /tildes/tildes/templates/donate_success.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2019 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'base_no_sidebar.jinja2' %} 5 | 6 | {% block title %}Thanks for donating!{% endblock %} 7 | 8 | {% block content %} 9 |
10 |

Thanks for donating to Tildes!

11 |

You should receive an email receipt. If you have any questions, please feel free to contact donate@tildes.net

12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /tildes/tildes/templates/error_group_not_found.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'base_no_sidebar.jinja2' %} 5 | 6 | {% from 'macros/links.jinja2' import link_to_group with context %} 7 | 8 | {% block title %} 9 | Group not found 10 | {% endblock %} 11 | 12 | {% block content %} 13 | 14 |
15 |

No group named '{{ supplied_name }}'

16 | {% if group_suggestions %} 17 |

Did you mean one of these groups instead?

18 |
    19 | {% for group in group_suggestions %} 20 |
  • {{ link_to_group(group) }}
  • 21 | {% endfor %} 22 |
23 | {% endif %} 24 | 27 |
28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /tildes/tildes/templates/error_page.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'base_no_sidebar.jinja2' %} 5 | 6 | {% block title %}{{ error }}{% endblock %} 7 | 8 | {% block content %} 9 |
10 |

{{ error }}

11 |

{{ description }}

12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /tildes/tildes/templates/home.atom.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2021 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {%- extends 'topic_listing.atom.jinja2' %} 5 | 6 | {% block feed_title %}Tildes Atom feed{% endblock %} 7 | -------------------------------------------------------------------------------- /tildes/tildes/templates/home.rss.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2021 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {%- extends 'topic_listing.rss.jinja2' %} 5 | 6 | {% block channel_title %}Tildes{% endblock %} 7 | {% block channel_link %}https://tildes.net/{% endblock %} 8 | {% block channel_description %}Topics RSS feed{% endblock %} 9 | -------------------------------------------------------------------------------- /tildes/tildes/templates/includes/password_restrictions.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |
Password restrictions
5 |
6 |
    7 |
  • At least 8 characters long.
  • 8 |
  • Does not contain the username, and is not contained in the username.
  • 9 |
  • 10 |

    Has not been previously exposed in a data breach (checked locally against a list downloaded from Troy Hunt's "Have I been pwned?").

    11 |
  • 12 |
13 |
14 | -------------------------------------------------------------------------------- /tildes/tildes/templates/includes/topic_tags.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |
{% if topic.tags %}Tags:{% endif %} 5 | {% for tag in topic.tags_ordered %} 6 | {{ tag }} 7 | {%- if not loop.last %},{% endif %} 8 | {% endfor %} 9 |
10 | -------------------------------------------------------------------------------- /tildes/tildes/templates/includes/wiki_editing_notes.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2019 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |
5 |

Important notes about editing wiki pages

6 |

By submitting content to the Tildes wiki, you are agreeing to license it under Creative Commons Attribution-ShareAlike 4.0 and understand that doing so means that the content can be copied, modified, and redistributed by others (as long as they follow the terms of that license).

7 |

The full history of Tildes wiki pages is retained and publicly available. Please be very careful not to include private information or other sensitive data, since it may be impossible to remove from the history.

8 |
9 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/comment_contents.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/comments.jinja2' import render_comment_contents with context %} 5 | 6 | {{ render_comment_contents(comment, is_individual_comment=True) }} 7 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/group_subscription_box.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/groups.jinja2' import render_group_subscription_box with context %} 5 | 6 | {{ render_group_subscription_box(group) }} 7 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/invite_code.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |
5 | {% if num_remaining > 0 %} 6 | 15 | {% else %} 16 |

You aren't able to generate more invite links right now.

17 | {% endif %} 18 |

You have the following invite links active that have not been used yet:

19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/login_two_factor.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |

Two-factor authentication is enabled on this account. Please enter the code from your authenticator app below. If you do not have access to your authenticator device, enter a backup code.

5 | 6 |
7 | 8 | 9 | {% if keep %} 10 | 11 | {% endif %} 12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/markdown_preview.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {{ rendered_html|safe }} 5 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/markdown_source.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2020 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |
5 | 12 | 18 |
19 | 24 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/post_action_toggle_button.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2019 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/buttons.jinja2' import post_action_toggle_button with context %} 5 | 6 | {{ post_action_toggle_button(name, subject, is_toggled) }} 7 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/single_comment.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/comments.jinja2' import render_single_comment with context %} 5 | 6 | {{ render_single_comment(comment) }} 7 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/single_message.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/messages.jinja2' import render_message with context %} 5 | 6 | {{ render_message(message) }} 7 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/topic_contents.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {{ topic.rendered_html|safe }} 5 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/topic_edit.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/forms.jinja2' import markdown_textarea %} 5 | 6 |
16 | {{ markdown_textarea(id="topic-markdown", text=topic.markdown, auto_focus=True) }} 17 |
18 | 19 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/topic_group_edit.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |
12 | 13 | 14 |
15 | 16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/topic_link_edit.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2019 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |
13 | 14 | 15 |
16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/topic_tags.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% include 'includes/topic_tags.jinja2' %} 5 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/topic_tags_edit.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/forms.jinja2' import topic_tagging %} 5 | 6 |
16 | 17 | {{ topic_tagging(value=topic.tags|join(', '), auto_focus=True, autocomplete_options=topic.group.autocomplete_topic_tags) }} 18 |
19 | 20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/topic_title_edit.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |
13 | 14 | 15 |
16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/topic_voting.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/topics.jinja2' import topic_voting with context %} 5 | 6 | {{ topic_voting(topic) }} 7 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/two_factor_backup_codes.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2020 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/user.jinja2' import two_factor_backup_codes with context %} 5 | 6 | {{ two_factor_backup_codes() }} 7 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/two_factor_disabled.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 |

Two-factor authentication has been disabled. You will no longer need a code when logging in.

5 | 6 |

Keep in mind: if you ever reenable two-factor authentication, your previous backup codes will not be valid.

7 | -------------------------------------------------------------------------------- /tildes/tildes/templates/intercooler/two_factor_enabled.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% from 'macros/user.jinja2' import two_factor_backup_codes with context %} 5 | 6 |

Congratulations! Two-factor authentication has been enabled.

7 | 8 | {{ two_factor_backup_codes() }} 9 | -------------------------------------------------------------------------------- /tildes/tildes/templates/macros/datetime.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% macro time_ago(datetime, abbreviate=False) -%} 5 | 6 | {%- endmacro %} 7 | 8 | {% macro adaptive_date_responsive(datetime, class_=None, precision=None) -%} 9 | 15 | {%- endmacro %} 16 | -------------------------------------------------------------------------------- /tildes/tildes/templates/macros/links.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% macro link_to_user(user) -%} 5 | {% if user.is_real_user %} 6 | {{ user.username }} 11 | {% else %} 12 | {{ user.username }} 13 | {% endif %} 14 | {%- endmacro %} 15 | 16 | {% macro link_to_group(group) -%} 17 | ~{{ group.path }} 22 | {%- endmacro %} 23 | -------------------------------------------------------------------------------- /tildes/tildes/templates/macros/utils.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2019 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% macro pluralize(counter, singular_form, plural_form=None) %} 5 | {% if not plural_form %} 6 | {% set plural_form = singular_form~"s" %} 7 | {% endif %} 8 | 9 | {%- trans counter=counter -%} 10 | {{ counter }} {{ singular_form }} 11 | {%- pluralize -%} 12 | {{ counter }} {{ plural_form }} 13 | {%- endtrans %} 14 | {% endmacro %} 15 | 16 | {% macro format_money(amount) %} 17 | {{ "${:,.2f}".format(amount) }} 18 | {% endmacro %} 19 | -------------------------------------------------------------------------------- /tildes/tildes/templates/messages_sent.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'messages.jinja2' %} 5 | 6 | {% block title %}Sent Messages{% endblock %} 7 | 8 | {% block main_heading %}Sent Messages{% endblock %} 9 | -------------------------------------------------------------------------------- /tildes/tildes/templates/messages_unread.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'messages.jinja2' %} 5 | 6 | {% block title %}Unread Messages{% endblock %} 7 | 8 | {% block main_heading %}Unread Messages{% endblock %} 9 | -------------------------------------------------------------------------------- /tildes/tildes/templates/new_topic.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'base_no_sidebar.jinja2' %} 5 | 6 | {% block title %}New topic{% endblock %} 7 | 8 | {% block header_context_link %} 9 | ~{{ group.path }} 10 | {% endblock %} 11 | 12 | {% block main_heading %}Post a new topic in ~{{ group.path }}{% endblock %} 13 | 14 | {% block content %} 15 | {% include "includes/new_topic_form.jinja2" %} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /tildes/tildes/templates/notifications.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2018 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'notifications_unread.jinja2' %} 5 | 6 | {% block title %}Previously read notifications{% endblock %} 7 | 8 | {% block main_heading %}Previously read notifications{% endblock %} 9 | -------------------------------------------------------------------------------- /tildes/tildes/templates/settings_bio.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2019 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'base_settings.jinja2' %} 5 | 6 | {% from 'macros/forms.jinja2' import markdown_textarea %} 7 | 8 | {% block title %}Edit your user bio{% endblock %} 9 | 10 | {% block main_heading %}Edit your user bio{% endblock %} 11 | 12 | {% block settings %} 13 |

Enter a bio that others will be able to view on your user page (maximum {{ bio_max_length }} characters). To remove your bio, set it to blank.

14 | 15 |
16 | 17 |
24 | {{ markdown_textarea(text=request.user.bio_markdown) }} 25 | 26 |
27 | 28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /tildes/tildes/templates/user_search.jinja2: -------------------------------------------------------------------------------- 1 | {# Copyright (c) 2019 Tildes contributors #} 2 | {# SPDX-License-Identifier: AGPL-3.0-or-later #} 3 | 4 | {% extends 'user.jinja2' %} 5 | 6 | {% block title %}Search {{ user }}'s posts: {{ search }}{% endblock %} 7 | 8 | {% block main_heading %}Search results: {{ search }}{% endblock %} 9 | 10 | {% block no_posts_message %}No results found{% endblock %} 11 | -------------------------------------------------------------------------------- /tildes/tildes/templates/votes.jinja2: -------------------------------------------------------------------------------- 1 | {% extends 'bookmarks.jinja2' %} 2 | 3 | {% block title %}Votes{% endblock %} 4 | 5 | {% block main_heading %}Votes{% endblock %} 6 | 7 | {% block content %} 8 |
Tildes only retains individual voting data until each post is 30 days old, so don't use this page to keep track of posts longer-term. For that purpose, use the "Bookmark" button available on every post.
9 | {{ super() }} 10 | {% endblock %} 11 | 12 | {% block empty_message %} 13 | {% if post_type == 'topic' %} 14 | You haven't voted on any topics 15 | {% elif post_type == 'comment' %} 16 | You haven't voted on any comments 17 | {% endif %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /tildes/tildes/typing.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Custom type aliases to use in type annotations.""" 5 | 6 | from typing import Any 7 | 8 | # types for an ACE (Access Control Entry), and the ACL (Access Control List) of them 9 | AceType = tuple[str, Any, str] 10 | AclType = list[AceType] 11 | -------------------------------------------------------------------------------- /tildes/tildes/views/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains the application's views.""" 2 | 3 | from pyramid.response import Response 4 | 5 | 6 | # Intercooler uses an empty response as a no-op and won't replace anything. 7 | # 204 would probably be more correct than 200, but Intercooler errors on it 8 | IC_NOOP = Response(status_int=200) 9 | IC_NOOP_404 = Response(status_int=404) 10 | 11 | # Because of the above, in order to deliberately cause Intercooler to replace an element 12 | # with whitespace, the response needs to contain at least two spaces 13 | IC_EMPTY = Response(" ") 14 | -------------------------------------------------------------------------------- /tildes/tildes/views/api/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains API views.""" 2 | -------------------------------------------------------------------------------- /tildes/tildes/views/api/v0/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains views for v0 of the JSON API.""" 2 | -------------------------------------------------------------------------------- /tildes/tildes/views/api/v0/group.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """API v0 endpoints related to groups.""" 5 | 6 | from pyramid.request import Request 7 | 8 | from tildes.api import APIv0 9 | from tildes.resources.group import group_by_path 10 | 11 | 12 | ONE = APIv0(name="group", path="/groups/{path}", factory=group_by_path) 13 | 14 | 15 | @ONE.get() 16 | def get_group(request: Request) -> dict: 17 | """Get a single group's data.""" 18 | return request.context 19 | -------------------------------------------------------------------------------- /tildes/tildes/views/api/v0/topic.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """API v0 endpoints related to topics.""" 5 | 6 | from pyramid.request import Request 7 | 8 | from tildes.api import APIv0 9 | from tildes.resources.topic import topic_by_id36 10 | 11 | 12 | ONE = APIv0( 13 | name="topic", path="/groups/{path}/topics/{topic_id36}", factory=topic_by_id36 14 | ) 15 | 16 | 17 | @ONE.get() 18 | def get_topic(request: Request) -> dict: 19 | """Get a single topic's data.""" 20 | return request.context 21 | -------------------------------------------------------------------------------- /tildes/tildes/views/api/v0/user.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """API v0 endpoints related to users.""" 5 | 6 | from pyramid.request import Request 7 | 8 | from tildes.api import APIv0 9 | from tildes.resources.user import user_by_username 10 | 11 | 12 | ONE = APIv0(name="user", path="/users/{username}", factory=user_by_username) 13 | 14 | 15 | @ONE.get() 16 | def get_user(request: Request) -> dict: 17 | """Get a single user's data.""" 18 | return request.context 19 | -------------------------------------------------------------------------------- /tildes/tildes/views/api/web/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains views for the web API (used by Intercooler).""" 2 | -------------------------------------------------------------------------------- /tildes/tildes/views/api/web/markdown_preview.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Web API endpoint for previewing Markdown.""" 5 | 6 | from pyramid.request import Request 7 | 8 | from tildes.lib.markdown import convert_markdown_to_safe_html 9 | from tildes.schemas.group_wiki_page import GroupWikiPageSchema 10 | from tildes.views.decorators import ic_view_config, use_kwargs 11 | 12 | 13 | @ic_view_config( 14 | route_name="markdown_preview", 15 | request_method="POST", 16 | renderer="markdown_preview.jinja2", 17 | ) 18 | # uses GroupWikiPageSchema because it should always have the highest max_length 19 | @use_kwargs(GroupWikiPageSchema(only=("markdown",)), location="form") 20 | def markdown_preview(request: Request, markdown: str) -> dict: 21 | """Render the provided text as Markdown.""" 22 | # pylint: disable=unused-argument 23 | 24 | rendered_html = convert_markdown_to_safe_html(markdown) 25 | return {"rendered_html": rendered_html} 26 | -------------------------------------------------------------------------------- /tildes/tildes/views/metrics.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """The view for exposing metrics to be picked up by Prometheus.""" 5 | 6 | from prometheus_client import CollectorRegistry, generate_latest, multiprocess 7 | from pyramid.request import Request 8 | from pyramid.security import NO_PERMISSION_REQUIRED 9 | from pyramid.view import view_config 10 | 11 | 12 | @view_config(route_name="metrics", renderer="string", permission=NO_PERMISSION_REQUIRED) 13 | def get_metrics(request: Request) -> str: 14 | """Merge together the metrics from all workers and output them.""" 15 | registry = CollectorRegistry() 16 | multiprocess.MultiProcessCollector(registry) 17 | data = generate_latest(registry) 18 | 19 | # When Prometheus accesses this page it will always create a new session. This 20 | # session is useless and will never be used again, so we can just invalidate it to 21 | # cause it to be deleted from storage. It would be even better to find a way to not 22 | # create it in the first place. 23 | request.session.invalidate() 24 | 25 | return data.decode("utf-8") 26 | -------------------------------------------------------------------------------- /tildes/tildes/views/shortener.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Tildes contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | """Views related to the link shortener.""" 5 | 6 | from typing import NoReturn 7 | 8 | from pyramid.httpexceptions import HTTPMovedPermanently 9 | from pyramid.request import Request 10 | from pyramid.security import NO_PERMISSION_REQUIRED 11 | from pyramid.view import view_config 12 | 13 | 14 | @view_config(route_name="shortener_group", permission=NO_PERMISSION_REQUIRED) 15 | def get_shortener_group(request: Request) -> NoReturn: 16 | """Redirect to the base path of a group.""" 17 | destination = f"https://tildes.net/~{request.context.path}" 18 | raise HTTPMovedPermanently(location=destination) 19 | 20 | 21 | @view_config(route_name="shortener_topic", permission=NO_PERMISSION_REQUIRED) 22 | def get_shortener_topic(request: Request) -> NoReturn: 23 | """Redirect to the full permalink for a topic.""" 24 | destination = f"https://tildes.net{request.context.permalink}" 25 | raise HTTPMovedPermanently(location=destination) 26 | -------------------------------------------------------------------------------- /tildes/webassets.yaml: -------------------------------------------------------------------------------- 1 | directory: static 2 | url: / 3 | manifest: json 4 | 5 | bundles: 6 | javascript: 7 | contents: 8 | # keep scripts.js at the top so it can define things needed in other ones 9 | - js/scripts.js 10 | - js/behaviors/*.js 11 | output: js/tildes.js 12 | javascript-third-party: 13 | contents: 14 | # jquery needs to be at the top since others depend on it 15 | - js/third_party/jquery-*.js 16 | - js/third_party/*.js 17 | output: js/third_party.js 18 | css: 19 | contents: 20 | # keep styles.css at the bottom so it can override Spectre 21 | - css/spectre-0.5.1/spectre.css 22 | - css/spectre-0.5.1/spectre-icons.css 23 | - css/styles.css 24 | output: css/tildes.css 25 | --------------------------------------------------------------------------------