├── templates ├── error.html.hbs ├── message.html.hbs ├── thread_editor.html.hbs ├── components │ ├── user_control_panel_tabs.html.hbs │ ├── post_profile.html.hbs │ ├── editor_form.html.hbs │ ├── footer.html.hbs │ ├── pagination.html.hbs │ ├── post.html.hbs │ └── comment.html.hbs ├── site │ ├── mod_log.html.hbs │ ├── setup.html.hbs │ ├── index.html.hbs │ ├── community_list.html.hbs │ └── search.html.hbs ├── private_message │ ├── editor.html.hbs │ ├── overview.html.hbs │ └── thread.html.hbs ├── comment_editor.html.hbs ├── report.html.hbs ├── view_topic.html.hbs ├── remove_item.html.hbs ├── user │ ├── login.html.hbs │ ├── view_profile.html.hbs │ └── register.html.hbs └── view_forum.html.hbs ├── .dockerignore ├── assets ├── styles │ ├── prosilver │ │ ├── images │ │ │ ├── index.htm │ │ │ ├── loading.gif │ │ │ ├── forum_link.gif │ │ │ ├── forum_read.gif │ │ │ ├── no_avatar.gif │ │ │ ├── topic_read.gif │ │ │ ├── announce_read.gif │ │ │ ├── forum_unread.gif │ │ │ ├── icons_contact.png │ │ │ ├── plupload │ │ │ │ ├── done.gif │ │ │ │ ├── error.gif │ │ │ │ └── throbber.gif │ │ │ ├── sticky_read.gif │ │ │ ├── sticky_unread.gif │ │ │ ├── topic_moved.gif │ │ │ ├── topic_unread.gif │ │ │ ├── announce_unread.gif │ │ │ ├── topic_read_hot.gif │ │ │ ├── topic_read_mine.gif │ │ │ ├── announce_read_mine.gif │ │ │ ├── forum_read_locked.gif │ │ │ ├── sticky_read_locked.gif │ │ │ ├── sticky_read_mine.gif │ │ │ ├── sticky_unread_mine.gif │ │ │ ├── topic_read_locked.gif │ │ │ ├── topic_unread_hot.gif │ │ │ ├── topic_unread_mine.gif │ │ │ ├── announce_read_locked.gif │ │ │ ├── announce_unread_mine.gif │ │ │ ├── forum_read_subforum.gif │ │ │ ├── forum_unread_locked.gif │ │ │ ├── sticky_unread_locked.gif │ │ │ ├── topic_read_hot_mine.gif │ │ │ ├── topic_unread_locked.gif │ │ │ ├── announce_unread_locked.gif │ │ │ ├── forum_unread_subforum.gif │ │ │ ├── sticky_read_locked_mine.gif │ │ │ ├── topic_read_locked_mine.gif │ │ │ ├── topic_unread_hot_mine.gif │ │ │ ├── announce_read_locked_mine.gif │ │ │ ├── sticky_unread_locked_mine.gif │ │ │ ├── topic_unread_locked_mine.gif │ │ │ └── announce_unread_locked_mine.gif │ │ ├── stylesheet.css │ │ ├── tweaks.css │ │ ├── utilities.css │ │ ├── plupload.css │ │ ├── icons.css │ │ ├── base.css │ │ ├── buttons.css │ │ ├── links.css │ │ └── print.css │ └── lemmybb.css ├── images │ ├── spacer.gif │ ├── icons │ │ ├── misc │ │ │ ├── fire.gif │ │ │ ├── star.gif │ │ │ ├── heart.gif │ │ │ ├── thinking.gif │ │ │ ├── radioactive.gif │ │ │ └── index.htm │ │ ├── smile │ │ │ ├── alert.gif │ │ │ ├── info.gif │ │ │ ├── mrgreen.gif │ │ │ ├── question.gif │ │ │ ├── redface.gif │ │ │ └── index.htm │ │ └── index.htm │ ├── smilies │ │ ├── icon_cool.gif │ │ ├── icon_cry.gif │ │ ├── icon_eek.gif │ │ ├── icon_evil.gif │ │ ├── icon_idea.gif │ │ ├── icon_lol.gif │ │ ├── icon_mad.gif │ │ ├── icon_razz.gif │ │ ├── icon_arrow.gif │ │ ├── icon_e_geek.gif │ │ ├── icon_e_sad.gif │ │ ├── icon_e_smile.gif │ │ ├── icon_e_ugeek.gif │ │ ├── icon_e_wink.gif │ │ ├── icon_exclaim.gif │ │ ├── icon_mrgreen.gif │ │ ├── icon_neutral.gif │ │ ├── icon_redface.gif │ │ ├── icon_twisted.gif │ │ ├── icon_e_biggrin.gif │ │ ├── icon_question.gif │ │ ├── icon_rolleyes.gif │ │ ├── icon_e_confused.gif │ │ └── icon_e_surprised.gif │ ├── upload_icons │ │ ├── avi.gif │ │ ├── bmp.gif │ │ ├── doc.gif │ │ ├── exe.gif │ │ ├── gif.gif │ │ ├── html.gif │ │ ├── jpg.gif │ │ ├── mid.gif │ │ ├── mov.gif │ │ ├── mp3.gif │ │ ├── mpg.gif │ │ ├── pdf.gif │ │ ├── ppt.gif │ │ ├── rar.gif │ │ ├── txt.gif │ │ ├── wav.gif │ │ ├── xls.gif │ │ ├── zip.gif │ │ └── netscape.gif │ ├── index.htm │ ├── avatars │ │ ├── index.htm │ │ ├── upload │ │ │ ├── index.htm │ │ │ └── .htaccess │ │ └── gallery │ │ │ └── index.htm │ └── ranks │ │ └── index.htm └── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── config └── config.hjson ├── screenshots ├── lemmybb_1.png ├── lemmybb_2.png └── lemmybb_3.png ├── .rustfmt.toml ├── .gitignore ├── .gitmodules ├── .github └── ISSUE_TEMPLATE │ ├── QUESTION.yml │ ├── FEATURE_REQUEST.yml │ └── BUG_REPORT.yml ├── docker ├── lemmy.hjson ├── Dockerfile ├── docker-compose.yml └── nginx.conf ├── release.sh ├── src ├── env.rs ├── routes │ ├── mod.rs │ ├── community.rs │ ├── comment.rs │ ├── post.rs │ ├── moderation.rs │ └── site.rs ├── api │ ├── moderation.rs │ ├── private_message.rs │ ├── site.rs │ ├── post.rs │ ├── categories.rs │ ├── community.rs │ ├── image.rs │ ├── comment.rs │ ├── mod.rs │ ├── user.rs │ └── extra.rs ├── pagination.rs ├── error.rs ├── utils.rs ├── main.rs ├── site_fairing.rs └── template_helpers.rs ├── Cargo.toml └── .woodpecker.yml /templates/error.html.hbs: -------------------------------------------------------------------------------- 1 | {{error}} -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | docker 3 | -------------------------------------------------------------------------------- /assets/styles/prosilver/images/index.htm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/config.hjson: -------------------------------------------------------------------------------- 1 | # necessary for tests 2 | { 3 | hostname: lemmy-alpha 4 | } 5 | -------------------------------------------------------------------------------- /assets/images/spacer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/spacer.gif -------------------------------------------------------------------------------- /screenshots/lemmybb_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/screenshots/lemmybb_1.png -------------------------------------------------------------------------------- /screenshots/lemmybb_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/screenshots/lemmybb_2.png -------------------------------------------------------------------------------- /screenshots/lemmybb_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/screenshots/lemmybb_3.png -------------------------------------------------------------------------------- /assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /assets/images/icons/misc/fire.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/misc/fire.gif -------------------------------------------------------------------------------- /assets/images/icons/misc/star.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/misc/star.gif -------------------------------------------------------------------------------- /assets/images/icons/misc/heart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/misc/heart.gif -------------------------------------------------------------------------------- /assets/images/icons/smile/alert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/smile/alert.gif -------------------------------------------------------------------------------- /assets/images/icons/smile/info.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/smile/info.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_cool.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_cool.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_cry.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_cry.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_eek.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_eek.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_evil.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_evil.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_idea.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_idea.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_lol.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_lol.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_mad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_mad.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_razz.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_razz.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/avi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/avi.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/bmp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/bmp.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/doc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/doc.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/exe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/exe.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/gif.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/html.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/html.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/jpg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/jpg.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/mid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/mid.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/mov.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/mov.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/mp3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/mp3.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/mpg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/mpg.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/pdf.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/pdf.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/ppt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/ppt.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/rar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/rar.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/txt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/txt.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/wav.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/wav.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/xls.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/xls.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/zip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/zip.gif -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /assets/images/icons/misc/thinking.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/misc/thinking.gif -------------------------------------------------------------------------------- /assets/images/icons/smile/mrgreen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/smile/mrgreen.gif -------------------------------------------------------------------------------- /assets/images/icons/smile/question.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/smile/question.gif -------------------------------------------------------------------------------- /assets/images/icons/smile/redface.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/smile/redface.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_arrow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_arrow.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_e_geek.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_e_geek.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_e_sad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_e_sad.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_e_smile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_e_smile.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_e_ugeek.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_e_ugeek.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_e_wink.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_e_wink.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_exclaim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_exclaim.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_mrgreen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_mrgreen.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_neutral.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_neutral.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_redface.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_redface.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_twisted.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_twisted.gif -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition="2021" 2 | imports_layout="HorizontalVertical" 3 | imports_granularity="Crate" 4 | reorder_imports=true 5 | -------------------------------------------------------------------------------- /assets/images/icons/misc/radioactive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/icons/misc/radioactive.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_e_biggrin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_e_biggrin.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_question.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_question.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_rolleyes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_rolleyes.gif -------------------------------------------------------------------------------- /assets/images/upload_icons/netscape.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/upload_icons/netscape.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_e_confused.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_e_confused.gif -------------------------------------------------------------------------------- /assets/images/smilies/icon_e_surprised.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/images/smilies/icon_e_surprised.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/loading.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/forum_link.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/forum_link.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/forum_read.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/forum_read.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/no_avatar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/no_avatar.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_read.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_read.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/announce_read.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/announce_read.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/forum_unread.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/forum_unread.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/icons_contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/icons_contact.png -------------------------------------------------------------------------------- /assets/styles/prosilver/images/plupload/done.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/plupload/done.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/sticky_read.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/sticky_read.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/sticky_unread.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/sticky_unread.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_moved.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_moved.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_unread.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_unread.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/announce_unread.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/announce_unread.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/plupload/error.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/plupload/error.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_read_hot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_read_hot.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_read_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_read_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/announce_read_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/announce_read_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/forum_read_locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/forum_read_locked.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/plupload/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/plupload/throbber.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/sticky_read_locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/sticky_read_locked.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/sticky_read_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/sticky_read_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/sticky_unread_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/sticky_unread_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_read_locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_read_locked.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_unread_hot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_unread_hot.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_unread_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_unread_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/announce_read_locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/announce_read_locked.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/announce_unread_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/announce_unread_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/forum_read_subforum.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/forum_read_subforum.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/forum_unread_locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/forum_unread_locked.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/sticky_unread_locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/sticky_unread_locked.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_read_hot_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_read_hot_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_unread_locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_unread_locked.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/announce_unread_locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/announce_unread_locked.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/forum_unread_subforum.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/forum_unread_subforum.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/sticky_read_locked_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/sticky_read_locked_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_read_locked_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_read_locked_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_unread_hot_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_unread_hot_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/announce_read_locked_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/announce_read_locked_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/sticky_unread_locked_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/sticky_unread_locked_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/topic_unread_locked_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/topic_unread_locked_mine.gif -------------------------------------------------------------------------------- /assets/styles/prosilver/images/announce_unread_locked_mine.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemmyNet/lemmyBB/main/assets/styles/prosilver/images/announce_unread_locked_mine.gif -------------------------------------------------------------------------------- /templates/message.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 |

{{message}}

4 | 5 | {{#if link_url }} 6 | {{link_text}} 7 | {{/if}} 8 | 9 | {{> components/footer }} -------------------------------------------------------------------------------- /assets/images/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/avatars/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/icons/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/ranks/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/avatars/upload/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/icons/misc/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/icons/smile/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/avatars/gallery/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rust 2 | /target 3 | 4 | # nodejs 5 | /node_modules/ 6 | /package-lock.json 7 | /package.json 8 | 9 | # ide 10 | /.idea 11 | 12 | # docker 13 | /docker/volumes 14 | 15 | # lemmybb 16 | /lemmybb_categories.hjson 17 | http-cacache/ 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lemmybb-translations"] 2 | url = https://github.com/LemmyNet/lemmybb_translations.git 3 | path = lemmybb-translations 4 | [submodule "lemmy-translations"] 5 | path = lemmy-translations 6 | url = https://github.com/LemmyNet/lemmy-translations.git 7 | -------------------------------------------------------------------------------- /templates/thread_editor.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 |

{{community.community_view.community.title}}

4 | 5 | {{> components/editor_form editor_title=(i18n site_data "post_topic") subject_field=true}} 6 | 7 | {{> components/footer }} 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/QUESTION.yml: -------------------------------------------------------------------------------- 1 | name: "? Question" 2 | description: "General questions about LemmyBB" 3 | title: "[Question]: " 4 | labels: ["question"] 5 | body: 6 | - type: textarea 7 | id: question 8 | attributes: 9 | label: Question description 10 | description: | 11 | What's the question you have about lemmyBB? 12 | validations: 13 | required: true 14 | -------------------------------------------------------------------------------- /templates/components/user_control_panel_tabs.html.hbs: -------------------------------------------------------------------------------- 1 |

{{{i18n site_data "user_control_panel"}}}

2 | 3 |
4 | 8 |
-------------------------------------------------------------------------------- /templates/site/mod_log.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 | 4 | {{# each entries }} 5 | 6 | {{log this}} 7 | 8 | 9 | 15 | 16 | 17 | {{/each}} 18 |
{{timestamp_human this.when}} 10 | {{{this.message}}} 11 | {{#if this.reason}} 12 |

Reason: {{this.reason}}

13 | {{/if}} 14 |
19 | 20 | {{> components/footer }} -------------------------------------------------------------------------------- /docker/lemmy.hjson: -------------------------------------------------------------------------------- 1 | # See the documentation for available config fields and descriptions: 2 | # https://join-lemmy.org/docs/en/administration/configuration.html 3 | { 4 | hostname: example.com 5 | pictrs_url: "http://pictrs:8080" 6 | database: { 7 | host: postgres 8 | } 9 | # increase read rate limit to handle extra requests necessary to render replies and frontpage 10 | rate_limit: { 11 | message: 9999 12 | message_per_second: 1 13 | search: 9999 14 | search_per_second: 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.68-slim-bullseye AS builder 2 | WORKDIR app 3 | RUN apt update && apt install git --no-install-recommends --yes 4 | COPY . . 5 | RUN LEMMYBB_VERSION=$(git describe --tag --always) cargo build --release 6 | 7 | FROM debian:bullseye-slim AS runtime 8 | WORKDIR app 9 | COPY --from=builder /app/target/release/lemmy_bb . 10 | COPY assets assets 11 | COPY templates templates 12 | COPY lemmy-translations lemmy-translations 13 | COPY lemmybb-translations lemmybb-translations 14 | ENTRYPOINT ["/app/lemmy_bb"] 15 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | new_tag="$1" 5 | 6 | old_tag=$(head -3 Cargo.toml | tail -1 | cut -d'"' -f 2) 7 | sed -i "0,/version = \"$old_tag\"/s//version = \"$new_tag\"/g" Cargo.toml 8 | 9 | cd lemmybb-translations 10 | git checkout main 11 | git pull http://weblate.join-lemmy.org/git/lemmy/lemmybb/ main --rebase 12 | cd .. 13 | git add lemmybb-translations 14 | 15 | git submodule update --recursive --remote -- lemmy-translations 16 | git add lemmy-translations 17 | 18 | git add Cargo.toml Cargo.lock 19 | git commit -m "Update to $new_tag" 20 | git tag "$new_tag" 21 | git push origin main --tags 22 | -------------------------------------------------------------------------------- /src/env.rs: -------------------------------------------------------------------------------- 1 | /// Address where lemmyBB should listen for incoming requests. 2 | pub fn listen_address() -> String { 3 | std::env::var("LEMMYBB_LISTEN_ADDRESS").unwrap_or_else(|_| "127.0.0.1:1244".to_string()) 4 | } 5 | 6 | /// Address where Lemmy API is available. 7 | pub fn lemmy_backend() -> String { 8 | std::env::var("LEMMYBB_BACKEND").unwrap_or_else(|_| "http://localhost:8536".to_string()) 9 | } 10 | 11 | /// Set true if Lemmy backend runs with increased message rate limit. This is necessary to show 12 | /// last replies for threads and forums. 13 | pub fn increased_rate_limit() -> bool { 14 | !std::env::var("LEMMYBB_INCREASED_RATE_LIMIT") 15 | .unwrap_or_default() 16 | .is_empty() 17 | } 18 | -------------------------------------------------------------------------------- /src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod backend_endpoints; 2 | pub mod comment; 3 | pub mod community; 4 | pub mod moderation; 5 | pub mod post; 6 | pub mod private_message; 7 | pub mod site; 8 | pub mod user; 9 | 10 | use crate::{api::CLIENT, error::ErrorPage}; 11 | use lemmy_api_common::sensitive::Sensitive; 12 | use rocket::http::{Cookie, CookieJar, SameSite}; 13 | 14 | pub fn auth(cookies: &CookieJar<'_>) -> Option> { 15 | cookies 16 | .get("jwt") 17 | .map(|c| Sensitive::new(c.value().to_string())) 18 | } 19 | 20 | pub fn build_jwt_cookie(jwt: Sensitive) -> Cookie<'static> { 21 | Cookie::build("jwt", jwt.into_inner()) 22 | .http_only(true) 23 | .secure(true) 24 | .same_site(SameSite::Strict) 25 | .finish() 26 | } 27 | -------------------------------------------------------------------------------- /assets/styles/prosilver/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* phpBB3 Style Sheet 2 | -------------------------------------------------------------- 3 | Style name: prosilver (the default phpBB 3.3.x style) 4 | Based on style: 5 | Original author: Tom Beddard ( http://www.subblue.com/ ) 6 | Modified by: phpBB Limited ( https://www.phpbb.com/ ) 7 | -------------------------------------------------------------- 8 | */ 9 | 10 | @import url("normalize.css?hash=48eb3f89"); 11 | @import url("base.css?hash=7c5543be"); 12 | @import url("utilities.css?hash=d8f72c42"); 13 | @import url("common.css?hash=a9741ba1"); 14 | @import url("links.css?hash=18286e16"); 15 | @import url("content.css?hash=be57a41d"); 16 | @import url("buttons.css?hash=56f0d25f"); 17 | @import url("cp.css?hash=50d868ab"); 18 | @import url("forms.css?hash=b64464fb"); 19 | @import url("icons.css?hash=64da33ce"); 20 | @import url("colours.css?hash=fcb2f289"); 21 | @import url("responsive.css?hash=87b53e08"); 22 | -------------------------------------------------------------------------------- /templates/private_message/editor.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 | {{> components/user_control_panel_tabs pm_active=1 }} 4 | 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 | {{{user_actor_id recipient}}} 14 |
15 |
16 | 17 | {{> components/editor_form editor_action=(concat "/send_private_message?u=" recipient.id) editor_title=(i18n site_data "compose_private_message")}} 18 | 19 |
20 |
21 |
22 | 23 | {{> components/footer }} 24 | -------------------------------------------------------------------------------- /assets/images/avatars/upload/.htaccess: -------------------------------------------------------------------------------- 1 | # With Apache 2.4 the "Order, Deny" syntax has been deprecated and moved from 2 | # module mod_authz_host to a new module called mod_access_compat (which may be 3 | # disabled) and a new "Require" syntax has been introduced to mod_authz_core. 4 | # We could just conditionally provide both versions, but unfortunately Apache 5 | # does not explicitly tell us its version if the module mod_version is not 6 | # available. In this case, we check for the availability of module 7 | # mod_authz_core (which should be on 2.4 or higher only) as a best guess. 8 | 9 | 10 | Order Allow,Deny 11 | Deny from All 12 | 13 | = 2.4> 14 | Require all denied 15 | 16 | 17 | 18 | 19 | Order Allow,Deny 20 | Deny from All 21 | 22 | 23 | Require all denied 24 | 25 | 26 | -------------------------------------------------------------------------------- /assets/styles/prosilver/tweaks.css: -------------------------------------------------------------------------------- 1 | /* Style Sheet Tweaks 2 | 3 | These style definitions are IE 8 & 9 only. 4 | They are required due to the poor CSS support in IE browsers. 5 | ------------------------------------------------------------------------------*/ 6 | 7 | /* IE 8 Tweaks (value)\9 equates to IE <= 8 8 | ------------------------------------------------------------------------------*/ 9 | 10 | /* Clear float fix */ 11 | .inner, 12 | ul.linklist { 13 | zoom: 1\9; 14 | } 15 | 16 | /* Align checkboxes/radio buttons nicely */ 17 | dd label input { 18 | vertical-align: text-bottom\9; 19 | } 20 | 21 | /* Fixes header-avatar aspect-ratio */ 22 | .header-avatar img { 23 | height: 20px\9; 24 | } 25 | 26 | /* IE8 often can't handle max-width in %, so we use px instead */ 27 | .postprofile .avatar img { 28 | max-width: 150px\9; 29 | } 30 | 31 | /* IE 9 Tweaks 32 | ------------------------------------------------------------------------------*/ 33 | 34 | /* Border-radius bleed fix in IE9 */ 35 | .search-header, 36 | .search-header .inputbox, 37 | .search-header a.button { 38 | border-radius: 0; 39 | } 40 | 41 | .tabs .tab > a { 42 | border-radius: 0; 43 | } 44 | -------------------------------------------------------------------------------- /templates/components/post_profile.html.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{#if user.avatar}} 5 | 6 | User avatar 7 | 8 | {{/if}} 9 |
10 | {{{user_name user}}} 11 |
12 | {{#if user.admin}} 13 |
Administrator
14 | {{/if}} 15 | {{#if (is_mod user moderators)}} 16 |
Moderator
17 | {{/if}} 18 | {{#if user.banned}} 19 |
Banned
20 | {{/if}} 21 | {{#if banned_from_community}} 22 |
Banned from forum
23 | {{/if}} 24 |
25 |
{{{user_actor_id user}}}
26 |
{{{i18n site_data "joined"}}}{{timestamp_human user.published}}
27 |
28 | -------------------------------------------------------------------------------- /templates/comment_editor.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 |

{{post.post_view.post.name}}

4 | 5 | {{> components/editor_form editor_title=(i18n site_data "post_reply")}} 6 | 7 |

8 | {{{i18n site_data "expand_view"}}} 9 | {{{i18n site_data "topic_review_title" post.post_view.post.name }}} 10 |

11 | 12 |
13 | {{#each page_comments}} 14 | {{> components/comment site_data=../site_data hide_author=true all_comments=../all_comments post=../post }} 15 | {{/each}} 16 | {{log (len page_comments)}} 17 | {{#unless eq (len page_comments) 20}} 18 | {{> components/post hide_author=true }} 19 | {{/unless}} 20 |
21 | 22 | {{> components/footer }} 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature request" 2 | description: Suggest an idea for improving LemmyBB 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: textarea 7 | id: problem 8 | attributes: 9 | label: Is your proposal related to a problem? 10 | description: | 11 | Provide a clear and concise description of what the problem is. 12 | For example, "I'm always frustrated when..." 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: solution 17 | attributes: 18 | label: Describe the solution you'd like 19 | description: | 20 | Provide a clear and concise description of what you want to happen. 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: solution-alt 25 | attributes: 26 | label: Describe alternatives you've considered 27 | description: | 28 | Please let us know about other solutions you've tried or researched. 29 | - type: textarea 30 | id: additional 31 | attributes: 32 | label: Additional context 33 | description: | 34 | Is there anything else you can add about the proposal? 35 | You might want to link to related issues here, if you haven't already. 36 | -------------------------------------------------------------------------------- /src/api/moderation.rs: -------------------------------------------------------------------------------- 1 | use crate::api::{get, post}; 2 | use anyhow::Error; 3 | use lemmy_api_common::{ 4 | comment::{CommentResponse, RemoveComment}, 5 | lemmy_db_schema::newtypes::{CommentId, PostId}, 6 | post::{PostResponse, RemovePost}, 7 | sensitive::Sensitive, 8 | site::{GetModlog, GetModlogResponse}, 9 | }; 10 | 11 | pub async fn remove_post( 12 | post_id: i32, 13 | reason: String, 14 | auth: Sensitive, 15 | ) -> Result { 16 | let params = RemovePost { 17 | post_id: PostId(post_id), 18 | removed: true, 19 | reason: Some(reason), 20 | auth, 21 | }; 22 | post("/post/remove", ¶ms).await 23 | } 24 | 25 | pub async fn remove_comment( 26 | comment_id: i32, 27 | reason: String, 28 | auth: Sensitive, 29 | ) -> Result { 30 | let params = RemoveComment { 31 | comment_id: CommentId(comment_id), 32 | removed: true, 33 | reason: Some(reason), 34 | auth, 35 | }; 36 | post("/comment/remove", ¶ms).await 37 | } 38 | 39 | pub async fn get_mod_log(auth: Option>) -> Result { 40 | let params = GetModlog { 41 | auth, 42 | ..Default::default() 43 | }; 44 | get("/modlog", ¶ms).await 45 | } 46 | -------------------------------------------------------------------------------- /assets/styles/lemmybb.css: -------------------------------------------------------------------------------- 1 | /* truncated latest reply */ 2 | a.lastsubject { 3 | white-space: nowrap; 4 | overflow: hidden; 5 | text-overflow: ellipsis; 6 | width: 235px; 7 | } 8 | 9 | /* sidebar info */ 10 | .navbar .inner h1 { 11 | color: #28313f; 12 | font-size: 2.5em; 13 | } 14 | 15 | /* topics */ 16 | .content h1, 17 | .panel h1 { 18 | color: #115098; 19 | border-bottom: 1px solid #cccccc; 20 | margin: 0; 21 | margin-top: 1em; 22 | margin-bottom: 0.5em; 23 | padding-bottom: 0.5em; 24 | } 25 | 26 | .postprofile dt a { 27 | width: 100%; 28 | width: -moz-available; 29 | width: -webkit-fill-available; 30 | width: fill-available; 31 | } 32 | 33 | /* truncated signature */ 34 | .signature { 35 | display: -webkit-box; 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | word-wrap: break-word; 39 | -webkit-line-clamp: 5; 40 | -webkit-box-orient: vertical; 41 | } 42 | 43 | .administrator-badge { 44 | color: #ef5350; 45 | font-weight: bold; 46 | } 47 | 48 | .moderator-badge { 49 | color: #00a600; 50 | font-weight: bold; 51 | } 52 | 53 | .banned-badge { 54 | color: #ff0000; 55 | font-weight: bold; 56 | } 57 | 58 | #mod_log { 59 | width: 95%; 60 | margin: 0 auto; 61 | } 62 | 63 | #mod_log th, 64 | td { 65 | border-bottom: 1px solid; 66 | } 67 | 68 | #mod_log p { 69 | margin-bottom: 0.5em; 70 | } 71 | -------------------------------------------------------------------------------- /templates/report.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 |

{{{i18n site_data "report_post_title"}}}

4 | 5 |
6 |
7 |
8 | 9 |
10 |

{{{i18n site_data "report_post_info"}}}

11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 |   29 | 30 | 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | {{> components/footer }} 39 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | lemmybb: 5 | image: lemmynet/lemmybb:0.1 6 | restart: always 7 | ports: 8 | - "127.0.0.1:8701:8701" 9 | environment: 10 | - LEMMYBB_BACKEND=http://lemmy:8536 11 | - LEMMYBB_LISTEN_ADDRESS=0.0.0.0:8701 12 | - LEMMYBB_INCREASED_RATE_LIMIT=1 13 | volumes: 14 | - ./lemmybb_categories.hjson:/app/lemmybb_categories.hjson:ro 15 | depends_on: 16 | - lemmy 17 | 18 | lemmy-ui: 19 | image: dessalines/lemmy-ui:0.16.7 20 | restart: always 21 | ports: 22 | - "127.0.0.1:8702:1234" 23 | environment: 24 | - LEMMY_INTERNAL_HOST=lemmy:8536 25 | - LEMMY_EXTERNAL_HOST=${LEMMY_UI_HOST} 26 | - LEMMY_HTTPS=true 27 | depends_on: 28 | - lemmy 29 | 30 | lemmy: 31 | image: dessalines/lemmy:0.16.7 32 | restart: always 33 | ports: 34 | - "127.0.0.1:8703:8536" 35 | environment: 36 | - RUST_LOG=warn 37 | volumes: 38 | - ./lemmy.hjson:/config/config.hjson:ro 39 | depends_on: 40 | - pictrs 41 | - postgres 42 | 43 | postgres: 44 | image: postgres:15-alpine 45 | restart: always 46 | environment: 47 | - POSTGRES_USER=lemmy 48 | - POSTGRES_PASSWORD=password 49 | - POSTGRES_DB=lemmy 50 | volumes: 51 | - ./volumes/postgres:/var/lib/postgresql/data 52 | 53 | pictrs: 54 | image: asonix/pictrs:0.3.1 55 | restart: always 56 | user: 991:991 57 | volumes: 58 | - ./volumes/pictrs:/mnt 59 | -------------------------------------------------------------------------------- /src/api/private_message.rs: -------------------------------------------------------------------------------- 1 | use crate::api::{get, post}; 2 | use anyhow::Error; 3 | use lemmy_api_common::{ 4 | lemmy_db_schema::newtypes::{PersonId, PrivateMessageId}, 5 | private_message::{ 6 | CreatePrivateMessage, 7 | GetPrivateMessages, 8 | MarkPrivateMessageAsRead, 9 | PrivateMessageResponse, 10 | PrivateMessagesResponse, 11 | }, 12 | sensitive::Sensitive, 13 | }; 14 | 15 | pub(crate) async fn list_private_messages( 16 | unread_only: bool, 17 | auth: Sensitive, 18 | ) -> Result { 19 | let params = GetPrivateMessages { 20 | auth, 21 | unread_only: Some(unread_only), 22 | ..Default::default() 23 | }; 24 | get("/private_message/list", ¶ms).await 25 | } 26 | 27 | pub(crate) async fn mark_private_message_read( 28 | private_message_id: PrivateMessageId, 29 | auth: Sensitive, 30 | ) -> Result { 31 | let params = MarkPrivateMessageAsRead { 32 | private_message_id, 33 | read: true, 34 | auth, 35 | }; 36 | post("/private_message/mark_as_read", ¶ms).await 37 | } 38 | 39 | pub(crate) async fn create_private_message( 40 | content: String, 41 | recipient_id: PersonId, 42 | auth: Sensitive, 43 | ) -> Result { 44 | let params = CreatePrivateMessage { 45 | content, 46 | recipient_id, 47 | auth, 48 | }; 49 | post("/private_message", ¶ms).await 50 | } 51 | -------------------------------------------------------------------------------- /assets/styles/prosilver/utilities.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | $Utilities 3 | -------------------------------------------------------------- */ 4 | 5 | .sr-only { 6 | position: absolute; 7 | width: 1px; 8 | height: 1px; 9 | margin: -1px; 10 | padding: 0; 11 | overflow: hidden; 12 | clip: rect(0, 0, 0, 0); 13 | border: 0; 14 | } 15 | 16 | .sr-only-focusable:active, 17 | .sr-only-focusable:focus { 18 | position: static; 19 | width: auto; 20 | height: auto; 21 | margin: 0; 22 | overflow: visible; 23 | clip: auto; 24 | } 25 | 26 | .clearfix:before, 27 | .clearfix:after, 28 | .container:before, 29 | .container:after, 30 | .container-fluid:before, 31 | .container-fluid:after, 32 | .row:before, 33 | .row:after { 34 | content: " "; 35 | display: table; 36 | } 37 | .clearfix:after, 38 | .container:after, 39 | .container-fluid:after, 40 | .row:after { 41 | clear: both; 42 | } 43 | 44 | .center-block { 45 | display: block; 46 | margin-left: auto; 47 | margin-right: auto; 48 | } 49 | 50 | .pull-right { 51 | float: right !important; 52 | } 53 | .pull-left { 54 | float: left !important; 55 | } 56 | .hide { 57 | display: none !important; 58 | } 59 | .show { 60 | display: block !important; 61 | } 62 | .invisible { 63 | visibility: hidden; 64 | } 65 | 66 | .text-hide { 67 | font: 0/0 a; 68 | color: transparent; 69 | text-shadow: none; 70 | background-color: transparent; 71 | border: 0; 72 | } 73 | 74 | .hidden { 75 | display: none; 76 | } 77 | 78 | .affix { 79 | position: fixed; 80 | } 81 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41E Bug Report" 2 | description: Create a report to help us improve LemmyBB 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Found a bug? Please fill out the sections below. 👍 10 | Thanks for taking the time to fill out this bug report! 11 | - type: textarea 12 | id: reproduce 13 | attributes: 14 | label: Steps to reproduce 15 | description: | 16 | Describe the steps to reproduce the bug. 17 | The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ answer. 18 | value: | 19 | 1. 20 | 2. 21 | 3. 22 | validations: 23 | required: true 24 | - type: input 25 | id: lemmybb-version 26 | attributes: 27 | label: Version 28 | description: Which LemmyBB version do you use? Displayed in the footer. 29 | placeholder: ex. 0.1.0 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: lemmybb-log 34 | attributes: 35 | label: LemmyBB Logs 36 | description: | 37 | Provide LemmyBB logs lines. 38 | To get this information, execute one of the following commands on the CLI: 39 | ```shell 40 | docker-compose log -f 41 | ``` 42 | render: shell 43 | - type: textarea 44 | id: additional 45 | attributes: 46 | label: Additional context 47 | description: | 48 | Is there anything else you can add about the proposal? 49 | You might want to link to related issues here, if you haven't already. 50 | -------------------------------------------------------------------------------- /assets/styles/prosilver/plupload.css: -------------------------------------------------------------------------------- 1 | .attach-panel-multi { 2 | display: none; 3 | margin-bottom: 1em; 4 | } 5 | 6 | .attach-row-tpl { 7 | display: none; 8 | } 9 | 10 | .file-list td { 11 | vertical-align: middle; 12 | } 13 | 14 | .attach-name { 15 | width: 50%; 16 | } 17 | 18 | .attach-comment { 19 | width: 30%; 20 | } 21 | 22 | .attach-comment .inputbox { 23 | resize: vertical; 24 | width: 100%; 25 | } 26 | 27 | .attach-filesize { 28 | width: 15%; 29 | } 30 | 31 | .attach-status { 32 | width: 5%; 33 | } 34 | 35 | .attach-filesize, 36 | .attach-status { 37 | text-align: center; 38 | } 39 | 40 | .attach-controls { 41 | display: inline-block; 42 | } 43 | 44 | .nojs .file-inline-bbcode { 45 | display: none; 46 | } 47 | 48 | .file-total-progress { 49 | height: 2px; 50 | display: block; 51 | position: relative; 52 | margin: 4px -10px -6px -10px; 53 | } 54 | 55 | .file-progress { 56 | background-color: #cccccc; 57 | display: inline-block; 58 | height: 8px; 59 | width: 50px; 60 | } 61 | 62 | .file-progress-bar, 63 | .file-total-progress-bar { 64 | background-color: green; 65 | display: block; 66 | height: 100%; 67 | width: 0; 68 | } 69 | 70 | .file-status.file-working { 71 | background: url("./images/plupload/throbber.gif"); 72 | } 73 | 74 | .file-status.file-uploaded { 75 | background: url("./images/plupload/done.gif"); 76 | } 77 | 78 | .file-status.file-error { 79 | background: url("./images/plupload/error.gif"); 80 | } 81 | 82 | .file-status { 83 | display: inline-block; 84 | height: 16px; 85 | width: 16px; 86 | } 87 | 88 | .file-name { 89 | max-width: 65%; 90 | vertical-align: bottom; 91 | } 92 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lemmy_bb" 3 | version = "0.2.2" 4 | edition = "2021" 5 | 6 | [profile.release] 7 | strip = "symbols" 8 | debug = 0 9 | lto = "thin" 10 | 11 | [profile.dev] 12 | strip = "symbols" 13 | debug = 0 14 | 15 | [features] 16 | default = [] 17 | embed-lemmy = ["lemmy_server", "send_wrapper"] 18 | embed-pictrs = ["embed-lemmy", "lemmy_server/embed-pictrs"] 19 | 20 | [dependencies] 21 | log = "0.4.17" 22 | env_logger = "0.10.0" 23 | lemmy_api_common = { git = "https://github.com/LemmyNet/lemmy.git", tag = "0.17.2" } 24 | lemmy_server = { git = "https://github.com/LemmyNet/lemmy.git", tag = "0.17.2", optional = true } 25 | once_cell = "1.17.1" 26 | anyhow = "1.0.70" 27 | rocket = { version = "0.5.0-rc.3", default-features = false } 28 | rocket_dyn_templates = { version = "0.1.0-rc.3", features = ["handlebars"] } 29 | serde = "1.0.160" 30 | reqwest = { version = "0.11.16", features = ["rustls-tls", "json", "cookies", "multipart"], default-features = false } 31 | serde_json = "1.0.96" 32 | comrak = { version = "0.18.0", default-features = false } 33 | chrono = "0.4.24" 34 | url = "2.3.1" 35 | futures = { version = "0.3.28", default-features = false } 36 | itertools = "0.10.5" 37 | rand = "0.8.5" 38 | deser-hjson = "1.1.0" 39 | json-gettext = { version = "4.0.5", default-features = false } 40 | tokio = "1.27.0" 41 | send_wrapper = { version = "0.6.0", features = ["futures"], optional = true } 42 | typed-builder = "0.14.0" 43 | http-cache-reqwest = "0.9.0" 44 | reqwest-middleware = "0.2.1" 45 | 46 | [dev-dependencies] 47 | serial_test = "2.0.0" 48 | ctor = "0.2.0" 49 | lemmy_server = { git = "https://github.com/LemmyNet/lemmy.git", tag = "0.17.2" } 50 | actix-rt = "2.8.0" 51 | -------------------------------------------------------------------------------- /templates/view_topic.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 |

{{ post.post_view.post.name }}

4 | 5 | 18 | 19 | {{#if (eq pagination.current_page 1)}} 20 | {{> components/post }} 21 | {{/if}} 22 | 23 | {{#each page_comments}} 24 | {{> components/comment post=../post site_data=../site_data all_comments=../all_comments }} 25 | {{/each}} 26 | 27 | 40 | 41 | {{> components/footer }} 42 | -------------------------------------------------------------------------------- /templates/remove_item.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 |
4 |
5 |
6 | 7 |

Delete post

8 | 9 |

Are you sure you want to delete this post?

10 | 11 |
12 | {{!-- 13 |
14 |
15 |
16 | 19 |
20 |
21 | --}} 22 | 23 | 24 |
25 |

The specified reason for deletion will be publicly visible.
26 |
27 |
28 |
29 | 30 |
31 |   32 | 33 | {{#if t}} 34 | 35 | {{/if}} 36 | {{#if r}} 37 | 38 | {{/if}} 39 |
40 | 41 |
42 |
43 |
44 | 45 | {{> components/footer }} 46 | -------------------------------------------------------------------------------- /templates/components/editor_form.html.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{#if message}} 4 |
5 |
6 |
7 |

Preview:

8 |
9 | {{{markdown message}}} 10 |
11 |
12 | 13 |
14 |
15 | {{/if}} 16 | 17 |
18 |
19 | 20 |

{{editor_title}}

21 | 22 |
23 | 24 | {{#if subject_field}} 25 |
26 |
27 |
28 | 29 |
30 |
31 | {{/if}} 32 | 33 | {{> components/editor }} 34 | 35 |
36 | 37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 |   46 |   47 | 48 |
49 | 50 |
51 |
52 | 53 |
54 | 55 |

56 | 57 | Top 58 | 59 |

60 | 61 |
62 | -------------------------------------------------------------------------------- /templates/user/login.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 |
4 |
5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 |
 
22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 |
35 |

{{{i18n site_data "register"}}}

36 |

{{{i18n site_data "register_info"}}}

37 |
38 |

{{{i18n site_data "register"}}}

39 |
40 | 41 |
42 |
43 | 44 |
45 | 46 | {{> components/footer }} -------------------------------------------------------------------------------- /templates/components/footer.html.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 89 | 90 | {{> components/footer }} -------------------------------------------------------------------------------- /src/routes/post.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{ 3 | comment::list_comments, 4 | community::get_community, 5 | post::{create_post, edit_post, get_post}, 6 | NameOrId, 7 | }, 8 | error::ErrorPage, 9 | pagination::{PageLimit, Pagination, PAGE_ITEMS}, 10 | rocket_uri_macro_login, 11 | routes::CLIENT, 12 | site_fairing::SiteData, 13 | utils::{replace_smilies, Context}, 14 | }; 15 | use lemmy_api_common::lemmy_db_views::structs::CommentView; 16 | use reqwest::header::HeaderName; 17 | use rocket::{form::Form, response::Redirect, Either}; 18 | use rocket_dyn_templates::{context, Template}; 19 | use url::Url; 20 | 21 | #[get("/view_topic?&")] 22 | pub async fn view_topic( 23 | t: i32, 24 | page: Option, 25 | site_data: SiteData, 26 | ) -> Result { 27 | let post = get_post(t, site_data.auth.clone()).await?; 28 | 29 | let all_comments = list_comments(post.post_view.post.id, site_data.auth.clone()).await?; 30 | let page_comments: Vec = all_comments 31 | .iter() 32 | // select items for current page 33 | .skip(((page.unwrap_or(1) - 1) * PAGE_ITEMS) as usize) 34 | .take(PAGE_ITEMS as usize) 35 | .cloned() 36 | .collect(); 37 | 38 | // determine if post.url should be rendered as or 39 | let mut is_image_url = false; 40 | if let Some(ref url) = post.post_view.post.url { 41 | // TODO: use HEAD request once that is supported by pictrs/lemmy 42 | let image = CLIENT.get::(url.clone().into()).send().await?; 43 | let content_type = &image.headers()[HeaderName::from_static("content-type")]; 44 | is_image_url = content_type.to_str()?.starts_with("image/"); 45 | } 46 | let limit = PageLimit::Known((all_comments.len() as f32 / PAGE_ITEMS as f32).ceil() as i32); 47 | let pagination = Pagination::new(page.unwrap_or(1), limit, format!("/viewtopic?t={t}&")); 48 | 49 | let ctx = Context::builder() 50 | .title(post.post_view.post.name.clone()) 51 | .site_data(site_data) 52 | .other(context! { post, is_image_url, page_comments, all_comments, pagination }) 53 | .build(); 54 | Ok(Template::render("view_topic", ctx)) 55 | } 56 | 57 | #[get("/post_editor?&")] 58 | pub async fn post_editor( 59 | f: i32, 60 | edit: Option, 61 | site_data: SiteData, 62 | ) -> Result, ErrorPage> { 63 | if site_data.auth.is_none() { 64 | return Ok(Either::Right(Redirect::to(uri!(login)))); 65 | } 66 | match edit { 67 | Some(e) => { 68 | let p = get_post(e, site_data.auth.clone()).await?.post_view.post; 69 | Ok(Either::Left( 70 | render_editor( 71 | f, 72 | Some((p.name, p.body.unwrap_or_default())), 73 | edit, 74 | site_data, 75 | ) 76 | .await?, 77 | )) 78 | } 79 | None => Ok(Either::Left(render_editor(f, None, None, site_data).await?)), 80 | } 81 | } 82 | 83 | async fn render_editor( 84 | community_id: i32, 85 | subject_and_message: Option<(String, String)>, 86 | edit_post_id: Option, 87 | site_data: SiteData, 88 | ) -> Result { 89 | let community = get_community(NameOrId::Id(community_id), site_data.auth.clone()).await?; 90 | let mut editor_action = format!("/post?f={}", community.community_view.community.id.0); 91 | if let Some(edit_post_id) = edit_post_id { 92 | editor_action = format!("{editor_action}&edit={edit_post_id}"); 93 | } 94 | let subject = subject_and_message 95 | .as_ref() 96 | .map(|s| s.0.clone()) 97 | .unwrap_or_default(); 98 | let message = subject_and_message 99 | .as_ref() 100 | .map(|s| s.1.clone()) 101 | .unwrap_or_default(); 102 | let ctx = Context::builder() 103 | .title("Post a new topic") 104 | .site_data(site_data) 105 | .other(context! { community, editor_action, subject, message }) 106 | .build(); 107 | Ok(Template::render("thread_editor", ctx)) 108 | } 109 | 110 | #[derive(FromForm)] 111 | pub struct PostForm { 112 | pub(crate) subject: String, 113 | pub(crate) message: String, 114 | pub(crate) preview: Option, 115 | } 116 | 117 | #[post("/post?&", data = "
")] 118 | pub async fn do_post( 119 | f: i32, 120 | edit: Option, 121 | form: Form, 122 | site_data: SiteData, 123 | ) -> Result, ErrorPage> { 124 | let subject = form.subject.clone(); 125 | let message = replace_smilies(&form.message, &site_data); 126 | 127 | if form.preview.is_some() { 128 | return Ok(Either::Left( 129 | render_editor(f, Some((subject, message)), edit, site_data).await?, 130 | )); 131 | } 132 | 133 | let auth = site_data.auth.expect("user not logged in"); 134 | let post = match edit { 135 | None => create_post(subject, message, f, auth).await?, 136 | Some(e) => edit_post(subject, message, e, auth).await?, 137 | }; 138 | Ok(Either::Right(Redirect::to(uri!(view_topic( 139 | post.post_view.post.id.0, 140 | Some(1) 141 | ))))) 142 | } 143 | -------------------------------------------------------------------------------- /templates/site/search.html.hbs: -------------------------------------------------------------------------------- 1 | {{> components/header }} 2 | 3 |

{{{i18n site_data "search_match_count" search_results_count}}} {{keywords}}

4 |

{{{i18n site_data "searched_query"}}} {{keywords}}

5 | 6 | 7 |
8 | 11 |
12 | 13 | {{#if search_results.communities search_results.users}} 14 |
15 | 27 |
28 | {{/if}} 29 | 30 | {{#each search_results.posts}} 31 |
32 |
33 | 34 |
35 |
by {{{user_actor_id this.creator}}}
36 |
{{{timestamp_human this.creator.published}}}
37 |
{{{i18n ../site_data "forum_title"}}} {{{community_actor_id this.community}}}
38 |
{{{i18n ../site_data "topic_title"}}} {{this.post.name}}
39 |
{{{i18n ../site_data "replies_title"}}} {{this.counts.comments}}
40 |
41 | 42 |
43 |

{{this.post.name}}

44 |
45 | {{#if this.post.url}} 46 | {{#if is_image_url}} 47 | 48 |
49 |
50 | {{else}} 51 | {{this.post.url}} 52 |
53 |
54 | {{/if}} 55 | {{/if}} 56 | {{{markdown this.post.body}}} 57 |
58 |
59 | 60 | 67 | 68 |
69 |
70 | {{/each}} 71 | 72 | {{#each search_results.comments}} 73 |
74 |
75 | 76 |
77 |
by {{{user_actor_id this.creator}}}
78 |
{{{timestamp_human this.creator.published}}}
79 |
{{{i18n ../site_data "forum_title"}}} {{{community_actor_id this.community}}}
80 |
{{{i18n ../site_data "topic_title"}}} {{this.post.name}}
81 |
{{{i18n ../site_data "replies_title"}}} ?
82 |
83 | 84 |
85 |

test post

86 |
{{{markdown this.comment.content}}}
87 |
88 | 89 | 96 | 97 |
98 |
99 | {{/each}} 100 | 101 | {{#unless search_results_count}} 102 |
103 |
104 | {{{i18n site_data "search_no_matches"}}} 105 |
106 |
107 | {{/unless}} 108 | 109 |
110 | 113 |
114 | 115 | {{> components/footer }} -------------------------------------------------------------------------------- /src/site_fairing.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{ 3 | extra::{get_notifications, Notification}, 4 | gen_request_url, 5 | handle_response, 6 | private_message::list_private_messages, 7 | CLIENT, 8 | }, 9 | routes::auth, 10 | }; 11 | use anyhow::Error; 12 | use chrono::Local; 13 | use futures::future::join; 14 | use itertools::Itertools; 15 | use lemmy_api_common::{ 16 | sensitive::Sensitive, 17 | site::{GetSite, GetSiteResponse}, 18 | }; 19 | use rocket::{ 20 | fairing::{Fairing, Info, Kind}, 21 | http::Cookie, 22 | request, 23 | request::FromRequest, 24 | Data, 25 | Request, 26 | }; 27 | use serde::{Deserialize, Serialize}; 28 | 29 | pub struct SiteFairing {} 30 | 31 | #[rocket::async_trait] 32 | impl Fairing for SiteFairing { 33 | fn info(&self) -> Info { 34 | Info { 35 | name: "Site data fetcher", 36 | kind: Kind::Request, 37 | } 38 | } 39 | 40 | /// Load site data for everything except /assets paths 41 | async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) { 42 | // TODO: might not need this anymore as we have caching 43 | if !req.uri().path().starts_with("/assets") { 44 | let _: &Option = req 45 | .local_cache_async(async { 46 | let site_data = get_site_data(req).await; 47 | if let Err(e) = &site_data { 48 | warn!("{}", e); 49 | } 50 | site_data.ok() 51 | }) 52 | .await; 53 | } 54 | } 55 | } 56 | 57 | #[rocket::async_trait] 58 | impl<'r> FromRequest<'r> for SiteData { 59 | type Error = (); 60 | 61 | async fn from_request(request: &'r Request<'_>) -> request::Outcome { 62 | let site_data: &Option = request.local_cache(|| None::); 63 | request::Outcome::Success(site_data.clone().unwrap()) 64 | } 65 | } 66 | 67 | #[derive(Serialize, Deserialize, Debug, Clone)] 68 | pub struct SiteData { 69 | pub site: GetSiteResponse, 70 | pub notifications: Vec, 71 | pub unread_pm_count: usize, 72 | pub current_date_time: String, 73 | pub auth: Option>, 74 | pub lang: String, 75 | pub lemmybb_version: String, 76 | } 77 | 78 | async fn get_site_data(request: &Request<'_>) -> Result { 79 | let mut auth = auth(request.cookies()); 80 | let params = GetSite { auth: auth.clone() }; 81 | let res = CLIENT 82 | .get(&gen_request_url("/site")) 83 | .query(¶ms) 84 | .send() 85 | .await?; 86 | let site: GetSiteResponse = match handle_response(res, "/site").await { 87 | Ok(o) => o, 88 | Err(e) => { 89 | if e.to_string() == "not_logged_in" { 90 | // if auth cookie is invalid, remove it and retry 91 | request.cookies().remove(Cookie::named("jwt")); 92 | auth = None; 93 | let res = CLIENT 94 | .get(&gen_request_url("/site")) 95 | .query(&GetSite { auth: None }) 96 | .send() 97 | .await?; 98 | handle_response(res, "/site").await? 99 | } else { 100 | Err(e)? 101 | } 102 | } 103 | }; 104 | let browser_lang = request 105 | .headers() 106 | .get("accept-language") 107 | .collect_vec() 108 | .first() 109 | .unwrap_or(&"") 110 | .to_string(); 111 | let lang = match &site.my_user { 112 | Some(u) => { 113 | let user_lang = u.local_user_view.local_user.interface_language.clone(); 114 | match user_lang == "browser" { 115 | true => browser_lang, 116 | false => user_lang, 117 | } 118 | } 119 | None => browser_lang, 120 | }; 121 | 122 | let mut site_data = SiteData { 123 | site, 124 | notifications: vec![], 125 | // TODO: why is this? 126 | unread_pm_count: 0, 127 | current_date_time: Local::now().naive_local().format("%a %v %R").to_string(), 128 | auth: auth.clone(), 129 | lang, 130 | lemmybb_version: option_env!("LEMMYBB_VERSION") 131 | .unwrap_or("unknown version") 132 | .to_string(), 133 | }; 134 | if let Some(auth) = auth { 135 | let (notifications, private_messages) = join( 136 | get_notifications(auth.clone()), 137 | list_private_messages(true, auth.clone()), 138 | ) 139 | .await; 140 | site_data.notifications = notifications?; 141 | site_data.unread_pm_count = private_messages?.private_messages.len(); 142 | } 143 | Ok(site_data) 144 | } 145 | 146 | #[cfg(test)] 147 | pub async fn test_site_data(auth: Option>) -> SiteData { 148 | let params = GetSite { auth: auth.clone() }; 149 | let res = CLIENT 150 | .get(&gen_request_url("/site")) 151 | .query(¶ms) 152 | .send() 153 | .await 154 | .unwrap(); 155 | let site: GetSiteResponse = handle_response(res, "/site").await.unwrap(); 156 | SiteData { 157 | site, 158 | notifications: vec![], 159 | unread_pm_count: 0, 160 | current_date_time: "".to_string(), 161 | auth, 162 | lang: "".to_string(), 163 | lemmybb_version: "".to_string(), 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/routes/moderation.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{ 3 | comment::get_comment, 4 | moderation::{get_mod_log, remove_comment, remove_post}, 5 | post::get_post, 6 | }, 7 | error::ErrorPage, 8 | site_fairing::SiteData, 9 | template_helpers::i18n_, 10 | utils::Context, 11 | }; 12 | use anyhow::anyhow; 13 | use chrono::NaiveDateTime; 14 | use comrak::{markdown_to_html, ComrakOptions}; 15 | use itertools::Itertools; 16 | use lemmy_api_common::lemmy_db_schema::source::community::CommunitySafe; 17 | use rocket::{form::Form, response::Redirect, Either}; 18 | use rocket_dyn_templates::{context, Template}; 19 | use serde::Serialize; 20 | 21 | #[get("/remove_item?&")] 22 | pub async fn remove_item( 23 | t: Option, 24 | r: Option, 25 | site_data: SiteData, 26 | ) -> Result { 27 | if t.is_some() == r.is_some() { 28 | return Err(anyhow!("One of params t and r needs to be set").into()); 29 | } 30 | let ctx = Context::builder() 31 | .title("Delete item") 32 | .site_data(site_data) 33 | .other(context! { t, r }) 34 | .build(); 35 | Ok(Template::render("remove_item", ctx)) 36 | } 37 | 38 | #[derive(FromForm)] 39 | pub struct RemoveItemForm { 40 | t: Option, 41 | r: Option, 42 | delete_reason: String, 43 | cancel: Option, 44 | } 45 | 46 | #[post("/do_remove_item", data = "")] 47 | pub async fn do_remove_item<'r>( 48 | form: Form, 49 | site_data: SiteData, 50 | ) -> Result, ErrorPage> { 51 | let auth = site_data.auth.clone().unwrap(); 52 | let link_url = match (form.t, form.r) { 53 | (Some(t), None) => { 54 | get_post(t, site_data.auth.clone()) 55 | .await? 56 | .post_view 57 | .post 58 | .ap_id 59 | } 60 | (None, Some(r)) => { 61 | get_comment(r, site_data.auth.clone()) 62 | .await? 63 | .comment_view 64 | .comment 65 | .ap_id 66 | } 67 | _ => return Err(anyhow!("One of params t and r needs to be set").into()), 68 | }; 69 | if form.cancel.is_some() { 70 | // cancelled, redirect back 71 | return Ok(Either::Right(Redirect::to(link_url.to_string()))); 72 | } 73 | match (form.t, form.r) { 74 | (Some(t), None) => { 75 | remove_post(t, form.delete_reason.clone(), auth).await?; 76 | } 77 | (None, Some(r)) => { 78 | remove_comment(r, form.delete_reason.clone(), auth).await?; 79 | } 80 | _ => return Err(anyhow!("Invalid parameters").into()), 81 | }; 82 | let message = "Item deleted successfully"; 83 | let link_text = "Click here to return"; 84 | let ctx = Context::builder() 85 | .title(message) 86 | .site_data(site_data) 87 | .other(context! { message, link_text, link_url }) 88 | .build(); 89 | Ok(Either::Left(Template::render("message", ctx))) 90 | } 91 | 92 | #[get("/mod_log")] 93 | pub async fn mod_log(site_data: SiteData) -> Result { 94 | let mod_log = get_mod_log(site_data.auth.clone()).await?; 95 | // TODO: consider moving this upstream 96 | let entries: Vec> = vec![ 97 | mod_log 98 | .removed_posts 99 | .into_iter() 100 | .map(|m| { 101 | // TODO: why is removed an option?? 102 | let action = if m.mod_remove_post.removed.unwrap() { 103 | "Removed" 104 | } else { 105 | "Restored" 106 | }; 107 | let message = format!("{action} post [{}]({})", m.post.name, m.post.ap_id); 108 | ModLogEntry { 109 | community: Some(m.community), 110 | reason: m.mod_remove_post.reason, 111 | when: m.mod_remove_post.when_, 112 | message, 113 | } 114 | }) 115 | .collect(), 116 | mod_log 117 | .removed_comments 118 | .into_iter() 119 | .map(|m| { 120 | let action = if m.mod_remove_comment.removed.unwrap() { 121 | "Removed" 122 | } else { 123 | "Restored" 124 | }; 125 | let mut content = m.comment.content.replace('\n', " "); 126 | if content.chars().count() > 100 { 127 | content = format!("{}...", content.chars().take(100).collect::()); 128 | } 129 | let message = format!("{action} comment [{}]({})", content, m.comment.ap_id); 130 | ModLogEntry { 131 | community: Some(m.community), 132 | reason: m.mod_remove_comment.reason, 133 | when: m.mod_remove_comment.when_, 134 | message, 135 | } 136 | }) 137 | .collect(), 138 | ]; 139 | let entries: Vec<_> = entries 140 | .into_iter() 141 | .flatten() 142 | .map(|mut e| { 143 | e.message = markdown_to_html(&e.message, &ComrakOptions::default()); 144 | e 145 | }) 146 | .sorted_by_key(|e| e.when) 147 | .rev() 148 | .collect(); 149 | 150 | let ctx = Context::builder() 151 | .title(i18n_(&site_data, "mod_log_title")) 152 | .site_data(site_data) 153 | .other(context! { entries }) 154 | .build(); 155 | Ok(Template::render("site/mod_log", ctx)) 156 | } 157 | 158 | #[derive(Debug, Serialize)] 159 | pub struct ModLogEntry { 160 | community: Option, 161 | reason: Option, 162 | when: NaiveDateTime, 163 | message: String, 164 | } 165 | -------------------------------------------------------------------------------- /src/api/extra.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{ 3 | comment::{list_comments, list_community_comments}, 4 | post::{get_post, list_posts}, 5 | user::{get_person, list_mentions, list_replies}, 6 | NameOrId, 7 | }, 8 | env::increased_rate_limit, 9 | }; 10 | use anyhow::Error; 11 | use chrono::NaiveDateTime; 12 | use futures::future::{join, join_all}; 13 | use lemmy_api_common::{ 14 | comment::GetCommentsResponse, 15 | lemmy_db_schema::{ 16 | newtypes::{CommunityId, PostId}, 17 | source::person::PersonSafe, 18 | }, 19 | lemmy_db_views::structs::{CommentView, PostView}, 20 | post::GetPostsResponse, 21 | sensitive::Sensitive, 22 | }; 23 | use serde::{Deserialize, Serialize}; 24 | 25 | #[derive(Serialize, Debug)] 26 | pub struct PostOrComment { 27 | title: String, 28 | creator: PersonSafe, 29 | post_id: PostId, 30 | reply_id: i32, 31 | time: NaiveDateTime, 32 | } 33 | 34 | fn generate_comment_title(post_title: &str) -> String { 35 | format!("Re: {post_title}") 36 | } 37 | 38 | pub async fn get_last_reply_in_thread( 39 | post: &PostView, 40 | auth: Option>, 41 | ) -> Result { 42 | if post.counts.comments == 0 { 43 | Ok(PostOrComment { 44 | title: post.post.name.clone(), 45 | creator: post.creator.clone(), 46 | post_id: post.post.id, 47 | reply_id: post.post.id.0, 48 | time: post.post.published, 49 | }) 50 | } else { 51 | let post = get_post(post.post.id.0, auth.clone()).await?; 52 | let comments: Vec = list_comments(post.post_view.post.id, auth.clone()) 53 | .await? 54 | .into_iter() 55 | .filter(|c| !c.comment.deleted && !c.comment.removed) 56 | .collect(); 57 | let creator_id = comments.last().unwrap().comment.creator_id; 58 | let creator = get_person(NameOrId::Id(creator_id.0), auth).await?; 59 | let last_comment = &comments.last().unwrap().comment; 60 | Ok(PostOrComment { 61 | title: generate_comment_title(&post.post_view.post.name), 62 | creator: creator.person_view.person, 63 | post_id: post.post_view.post.id, 64 | reply_id: last_comment.id.0, 65 | time: last_comment.published, 66 | }) 67 | } 68 | } 69 | 70 | pub async fn get_last_reply_in_community( 71 | community_id: CommunityId, 72 | auth: Option>, 73 | ) -> Result, Error> { 74 | if !increased_rate_limit() { 75 | return Ok(None); 76 | } 77 | let (comment, post) = join( 78 | list_community_comments(community_id, auth.clone()), 79 | list_posts(community_id.0, 1, 1, auth.clone()), 80 | ) 81 | .await; 82 | let (comment, post): (GetCommentsResponse, GetPostsResponse) = (comment?, post?); 83 | let comment = join_all( 84 | comment 85 | .comments 86 | .iter() 87 | .filter(|c| !c.comment.deleted && !c.comment.removed) 88 | .last() 89 | .map(|c| async { 90 | PostOrComment { 91 | title: generate_comment_title(&c.post.name), 92 | creator: c.creator.clone(), 93 | post_id: c.post.id, 94 | reply_id: c.comment.id.0, 95 | time: c.comment.published, 96 | } 97 | }), 98 | ) 99 | .await 100 | .pop(); 101 | let post = post 102 | .posts 103 | .iter() 104 | .filter(|p| !p.post.deleted && !p.post.removed) 105 | .last() 106 | .map(|p| PostOrComment { 107 | title: p.post.name.clone(), 108 | creator: p.creator.clone(), 109 | post_id: p.post.id, 110 | reply_id: p.post.id.0, 111 | time: p.post.published, 112 | }); 113 | // return data for post or comment, depending which is newer 114 | Ok(if let Some(comment) = comment { 115 | if let Some(post) = post { 116 | if post.time > comment.time { 117 | Some(post) 118 | } else { 119 | Some(comment) 120 | } 121 | } else { 122 | Some(comment) 123 | } 124 | } else { 125 | post 126 | }) 127 | } 128 | 129 | #[derive(Serialize, Debug, Deserialize, Clone)] 130 | pub struct Notification { 131 | pub title: String, 132 | pub from_user: PersonSafe, 133 | pub reference: String, 134 | pub time: NaiveDateTime, 135 | pub link: String, 136 | } 137 | 138 | /// combine all types of notifications in a single "api call" 139 | pub async fn get_notifications(auth: Sensitive) -> Result, Error> { 140 | let (m, r) = join(list_mentions(auth.clone()), list_replies(auth.clone())).await; 141 | // TODO: would be good if we can find out the comment's position in the topic, and link like 142 | // viewtopic?t=1#p2 143 | let mentions: Vec = m? 144 | .mentions 145 | .into_iter() 146 | .map(|m| Notification { 147 | title: "Mention".to_string(), 148 | from_user: m.creator, 149 | reference: m.comment.content, 150 | time: m.comment.published, 151 | link: format!("/view_topic?t={}", m.post.id), 152 | }) 153 | .collect(); 154 | let mut replies = r? 155 | .replies 156 | .into_iter() 157 | .map(|r| Notification { 158 | title: "Reply".to_string(), 159 | from_user: r.creator, 160 | reference: r.comment.content, 161 | time: r.comment.published, 162 | link: format!("/view_topic?t={}", r.post.id), 163 | }) 164 | .collect(); 165 | let mut notifications = mentions; 166 | notifications.append(&mut replies); 167 | notifications.sort_by_key(|n| n.time); 168 | Ok(notifications) 169 | } 170 | -------------------------------------------------------------------------------- /src/template_helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::{pagination::PAGE_ITEMS, site_fairing::SiteData}; 2 | use chrono::NaiveDateTime; 3 | use comrak::ComrakOptions; 4 | use json_gettext::{JSONGetText, JSONGetTextBuilder}; 5 | use lemmy_api_common::{ 6 | lemmy_db_schema::{ 7 | newtypes::CommentId, 8 | source::{community::CommunitySafe, person::PersonSafe}, 9 | }, 10 | lemmy_db_views::structs::CommentView, 11 | lemmy_db_views_actor::structs::CommunityModeratorView, 12 | }; 13 | use once_cell::sync::{Lazy, OnceCell}; 14 | use rocket_dyn_templates::handlebars::{ 15 | handlebars_helper, 16 | Context, 17 | Handlebars, 18 | Helper, 19 | Output, 20 | RenderContext, 21 | RenderError, 22 | }; 23 | use serde_json::Value; 24 | 25 | static COMRAK: Lazy = Lazy::new(|| { 26 | let mut comrak = ComrakOptions::default(); 27 | comrak.extension.autolink = true; 28 | comrak 29 | }); 30 | 31 | handlebars_helper!(timestamp_machine: |ts: NaiveDateTime| { 32 | ts.format("%Y-%m-%dT%H:%M:%S%.f+00:00").to_string() 33 | }); 34 | 35 | handlebars_helper!(timestamp_human: |ts: NaiveDateTime| { 36 | // Wed Oct 05, 2022 9:17 pm 37 | ts.format("%a %v %R").to_string() 38 | }); 39 | 40 | handlebars_helper!(add: |a: i32, b: i32| { 41 | a + b 42 | }); 43 | 44 | handlebars_helper!(sub: |a: i32, b: i32| { 45 | a - b 46 | }); 47 | 48 | handlebars_helper!(modulo: |a: i32, b: i32| { 49 | a % b 50 | }); 51 | 52 | // Returns position of comment in thread. vec is assumed to be sorted 53 | handlebars_helper!(comment_page: |comment_id: CommentId, comments: Vec| { 54 | let index = comments.iter().position(|c| c.comment.id == comment_id); 55 | if let Some(i) = index { 56 | (i as f32 / PAGE_ITEMS as f32).ceil() as i32 57 | } else { 58 | // TODO: properly handle case of deleted parent 59 | -1 60 | } 61 | }); 62 | 63 | // Converts markdown to html. Replace generated

with

for newlines, because 64 | // otherwise fonts are rendered too big. 65 | handlebars_helper!(markdown: |md: Option| { 66 | match md { 67 | Some(m) => { 68 | comrak::markdown_to_html(&m, &COMRAK) 69 | .replace("

\n

", "

") 70 | .replace(r"

", "") 71 | .replace(r"

", "") 72 | } 73 | None => "".to_string() 74 | } 75 | }); 76 | 77 | handlebars_helper!(community_actor_id: |c: CommunitySafe| { 78 | if c.local { 79 | format!("!{}", c.name) 80 | } else { 81 | format!("!{}@{}", c.name, c.actor_id.domain().unwrap()) 82 | } 83 | }); 84 | 85 | handlebars_helper!(user_name: |p: PersonSafe| { 86 | p.display_name.unwrap_or(p.name) 87 | }); 88 | 89 | handlebars_helper!(user_actor_id: |p: PersonSafe| { 90 | if p.local { 91 | format!("@{}", p.name) 92 | } else { 93 | format!("@{}@{}", p.name, p.actor_id.domain().unwrap()) 94 | } 95 | }); 96 | 97 | // Handlebars is automatically inserting spaces at line beginning in editor text area, this 98 | // workaround avoids that. 99 | handlebars_helper!(raw: |s: String| { 100 | s 101 | }); 102 | 103 | handlebars_helper!(is_mod: |user: PersonSafe, moderators: Vec| { 104 | moderators.iter().any(|m| m.moderator.id == user.id) 105 | }); 106 | 107 | handlebars_helper!(is_mod_or_admin: |user: PersonSafe, moderators: Vec| { 108 | user.admin || moderators.iter().any(|m| m.moderator.id == user.id) 109 | }); 110 | 111 | pub fn concat( 112 | h: &Helper, 113 | _: &Handlebars, 114 | _: &Context, 115 | _: &mut RenderContext, 116 | out: &mut dyn Output, 117 | ) -> Result<(), RenderError> { 118 | let a = h.param(0).map(|v| v.render()).unwrap(); 119 | let b = h.param(1).map(|v| v.value().to_string()).unwrap(); 120 | 121 | out.write(&format!("{a}{b}"))?; 122 | 123 | Ok(()) 124 | } 125 | 126 | // https://github.com/sunng87/handlebars-rust/issues/43?ysclid=l5jxaw92um440916198#issuecomment-427482841 127 | pub fn length( 128 | h: &Helper, 129 | _: &Handlebars, 130 | _: &Context, 131 | _: &mut RenderContext, 132 | out: &mut dyn Output, 133 | ) -> Result<(), RenderError> { 134 | let length = h 135 | .param(0) 136 | .as_ref() 137 | .and_then(|v| v.value().as_array()) 138 | .map(|arr| arr.len()) 139 | .ok_or_else(|| { 140 | RenderError::new("Param 0 with 'array' type is required for array_length helper") 141 | })?; 142 | 143 | out.write(length.to_string().as_ref())?; 144 | 145 | Ok(()) 146 | } 147 | 148 | pub const ALL_LANGUAGES: [(&str, &str); 5] = [ 149 | ("en", "English"), 150 | ("de", "Deutsch"), 151 | ("fi", "Suomi"), 152 | ("id", "Bahasa Indonesia"), 153 | ("pt", "Português"), 154 | ]; 155 | 156 | handlebars_helper!(i18n: |site_data: SiteData, key: String, *args| { 157 | i18n_private(site_data.lang, key, args) 158 | }); 159 | 160 | fn i18n_private(lang: String, key: String, args: Vec<&Value>) -> String { 161 | static LANG_CELL: OnceCell = OnceCell::new(); 162 | let langs = LANG_CELL.get_or_init(|| { 163 | let mut builder = JSONGetTextBuilder::new("en"); 164 | for l in ALL_LANGUAGES { 165 | builder 166 | .add_json_file( 167 | l.0, 168 | format!("lemmybb-translations/translations/{}.json", l.0), 169 | ) 170 | .unwrap(); 171 | } 172 | builder.build().unwrap() 173 | }); 174 | let mut text = get_text!(langs, lang, key.clone()) 175 | .as_ref() 176 | .map(ToString::to_string) 177 | .unwrap_or_else(|| { 178 | warn!("Failed to retrieve translation for key {key}"); 179 | key 180 | }); 181 | if text.contains("{}") { 182 | let str = &args[2].to_string(); 183 | text = text.replacen("{}", args[2].as_str().unwrap_or(str), 1); 184 | } 185 | text 186 | } 187 | 188 | pub fn i18n_(site_data: &SiteData, key: &'static str) -> String { 189 | i18n_private(site_data.lang.clone(), key.to_string(), vec![]) 190 | } 191 | -------------------------------------------------------------------------------- /src/routes/site.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{ 3 | categories::get_categories, 4 | community::list_communities, 5 | extra::{get_last_reply_in_community, PostOrComment}, 6 | site::create_site, 7 | user::register, 8 | }, 9 | env::lemmy_backend, 10 | forward_get_request, 11 | pagination::{PageLimit, Pagination}, 12 | routes::{backend_endpoints::AcceptHeader, build_jwt_cookie, user::RegisterForm, ErrorPage}, 13 | site_fairing::SiteData, 14 | utils::{main_site_title, Context}, 15 | BackendResponse, 16 | }; 17 | use anyhow::Error; 18 | use futures::future::join_all; 19 | use lemmy_api_common::{ 20 | lemmy_db_schema::{source::local_site::RegistrationMode, ListingType}, 21 | lemmy_db_views_actor::structs::CommunityView, 22 | }; 23 | use rocket::{form::Form, http::CookieJar, response::Redirect, Either}; 24 | use rocket_dyn_templates::{context, Template}; 25 | use std::{collections::HashMap, str::FromStr}; 26 | 27 | #[get("/")] 28 | pub async fn index( 29 | site_data: SiteData, 30 | accept: AcceptHeader, 31 | ) -> Result, BackendResponse>, ErrorPage> { 32 | use Either::*; 33 | if !site_data.site.site_view.local_site.site_setup { 34 | // need to setup site 35 | return Ok(Left(Left(Redirect::to(uri!(setup))))); 36 | } 37 | // fetch apub site actor 38 | if accept.0.starts_with("application/") { 39 | return Ok(Right( 40 | forward_get_request(lemmy_backend(), accept, HashMap::new()).await?, 41 | )); 42 | } 43 | 44 | match get_categories(site_data.auth.clone()).await { 45 | Ok(categories) => { 46 | let ctx = Context::builder() 47 | .title(main_site_title(&site_data.site)) 48 | .site_data(site_data) 49 | .other(context! { categories }) 50 | .build(); 51 | Ok(Left(Right(Template::render("site/index", ctx)))) 52 | } 53 | Err(e) => { 54 | warn!("{}", e); 55 | Ok(Left(Left(Redirect::to(uri!("/community_list"))))) 56 | } 57 | } 58 | } 59 | 60 | #[get("/community_list?&")] 61 | pub async fn community_list( 62 | page: Option, 63 | mode: Option<&str>, 64 | site_data: SiteData, 65 | ) -> Result, ErrorPage> { 66 | let auth = site_data.auth.clone(); 67 | let listing_type: ListingType = mode 68 | .map(ListingType::from_str) 69 | .unwrap_or(Ok(ListingType::All))?; 70 | let mut communities: Vec = list_communities(listing_type, page, auth.clone()) 71 | .await? 72 | .communities; 73 | communities.sort_unstable_by_key(|c| c.community.id.0); 74 | let last_replies = join_all( 75 | communities 76 | .iter() 77 | .map(|c| get_last_reply_in_community(c.community.id, auth.clone())), 78 | ) 79 | .await 80 | .into_iter() 81 | .collect::>, Error>>()?; 82 | 83 | let limit = PageLimit::Unknown(communities.len()); 84 | let pagination = Pagination::new(page.unwrap_or(1), limit, "/community_list?"); 85 | let ctx = Context::builder() 86 | .title(main_site_title(&site_data.site)) 87 | .site_data(site_data) 88 | .other(context! { communities, last_replies, pagination }) 89 | .build(); 90 | Ok(Either::Right(Template::render("site/community_list", ctx))) 91 | } 92 | 93 | #[get("/setup")] 94 | pub async fn setup(site_data: SiteData) -> Result { 95 | let ctx = Context::builder() 96 | .title("Setup") 97 | .site_data(site_data) 98 | .other(()) 99 | .build(); 100 | Ok(Template::render("site/setup", ctx)) 101 | } 102 | 103 | #[derive(FromForm)] 104 | pub struct SetupForm { 105 | pub username: String, 106 | pub password: String, 107 | pub password_verify: String, 108 | pub show_nsfw: bool, 109 | pub email: Option, 110 | pub site_name: String, 111 | pub site_description: Option, 112 | } 113 | 114 | #[post("/setup", data = "")] 115 | pub async fn do_setup( 116 | form: Form, 117 | cookies: &CookieJar<'_>, 118 | ) -> Result { 119 | let register_form = RegisterForm { 120 | username: form.username.clone(), 121 | password: form.password.clone(), 122 | password_verify: form.password_verify.clone(), 123 | show_nsfw: form.show_nsfw, 124 | ..Default::default() 125 | }; 126 | let jwt = register(register_form).await?.jwt.unwrap(); 127 | cookies.add(build_jwt_cookie(jwt.clone())); 128 | 129 | create_site( 130 | form.site_name.clone(), 131 | form.site_description.clone(), 132 | RegistrationMode::RequireApplication, 133 | jwt, 134 | ) 135 | .await?; 136 | 137 | Ok(Redirect::to(uri!("/"))) 138 | } 139 | 140 | #[get("/legal")] 141 | pub async fn legal(site_data: SiteData) -> Result { 142 | let message = site_data 143 | .site 144 | .site_view 145 | .local_site 146 | .legal_information 147 | .clone(); 148 | let ctx = Context::builder() 149 | .title(format!("Legal - {}", site_data.site.site_view.site.name)) 150 | .site_data(site_data) 151 | .other(context! { message }) 152 | .build(); 153 | Ok(Template::render("message", ctx)) 154 | } 155 | 156 | #[get("/search?")] 157 | pub async fn search(keywords: String, site_data: SiteData) -> Result { 158 | let search_results = crate::api::site::search(keywords.clone(), site_data.auth.clone()).await?; 159 | let search_results_count = search_results.users.len() 160 | + search_results.communities.len() 161 | + search_results.posts.len() 162 | + search_results.comments.len(); 163 | let ctx = Context::builder() 164 | .title(format!( 165 | "Search {} - {}", 166 | keywords, site_data.site.site_view.site.name 167 | )) 168 | .site_data(site_data) 169 | .other(context! { keywords, search_results, search_results_count }) 170 | .build(); 171 | Ok(Template::render("site/search", ctx)) 172 | } 173 | --------------------------------------------------------------------------------