├── migrations ├── .gitkeep ├── 2018-04-09-135738_create_groups │ ├── down.sql │ └── up.sql ├── 2018-04-09-135835_create_users │ ├── down.sql │ └── up.sql ├── 2018-04-09-140606_create_peers │ ├── down.sql │ └── up.sql ├── 2018-04-09-140244_create_torrents │ ├── down.sql │ └── up.sql ├── 2018-04-23-164613_create_transfers │ ├── down.sql │ └── up.sql ├── 2018-05-03-120715_create_messages │ ├── down.sql │ └── up.sql ├── 2018-04-09-135642_create_categories │ ├── down.sql │ └── up.sql ├── 2018-04-17-121101_create_torrent_nfos │ ├── down.sql │ └── up.sql ├── 2018-04-18-191602_create_torrent_list │ ├── down.sql │ └── up.sql ├── 2018-04-09-230549_create_acl_user_rules │ ├── down.sql │ └── up.sql ├── 2018-04-16-202936_create_torrent_files │ ├── down.sql │ └── up.sql ├── 2018-04-23-164650_create_user_transfer │ ├── down.sql │ └── up.sql ├── 2018-04-26-182812_create_chat_messages │ ├── down.sql │ └── up.sql ├── 2018-04-30-090640_create_torrent_images │ ├── down.sql │ └── up.sql ├── 2018-05-03-232652_create_static_content │ ├── down.sql │ └── up.sql ├── 2018-05-08-150335_create_user_profiles │ ├── down.sql │ └── up.sql ├── 2018-04-09-140018_create_user_properties │ ├── down.sql │ └── up.sql ├── 2018-05-03-114210_create_message_folders │ ├── down.sql │ └── up.sql ├── 2018-05-13-092226_create_torrent_comments │ ├── down.sql │ └── up.sql ├── 2018-04-16-202932_create_torrent_meta_files │ ├── down.sql │ └── up.sql ├── 2018-04-23-211719_create_completed_torrents │ ├── down.sql │ └── up.sql ├── 2018-04-30-095301_update_torrent_defaults │ ├── down.sql │ └── up.sql ├── 2018-04-09-202959_create_acl_group_rules │ ├── down.sql │ └── up.sql ├── 2018-04-29-150223_add_last_active_to_users │ ├── down.sql │ └── up.sql ├── 00000000000000_diesel_initial_setup │ ├── down.sql │ └── up.sql └── 2018-05-14-132001_add_comments_to_torrent_list │ ├── down.sql │ └── up.sql ├── webroot ├── aimg │ └── .gitkeep ├── timg │ └── .gitkeep └── static │ ├── js │ └── bootstrap.min.js │ ├── img │ └── 6NfmQ.jpg │ ├── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-regular-400.eot │ ├── fa-regular-400.ttf │ ├── fa-solid-900.eot │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ ├── fa-solid-900.woff2 │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.woff │ └── fa-regular-400.woff2 │ └── css │ ├── fa-brands.min.css │ ├── fa-solid.min.css │ ├── fa-regular.min.css │ ├── fa-brands.css │ └── fa-regular.css ├── rustfmt.toml ├── .gitmodules ├── .gitignore ├── templates ├── index │ ├── public.html │ ├── authenticated.html │ └── shoutbox.html ├── signup │ ├── confirm_fail.html │ ├── confirm_complete.html │ ├── signup_complete.html │ └── signup.html ├── error │ ├── 5xx.html │ └── 4xx.html ├── torrent │ ├── failed.html │ ├── denied.html │ ├── success.html │ ├── delete.html │ ├── edit.html │ ├── new.html │ └── list.html ├── static_content │ ├── edit_failed.html │ ├── view.html │ └── edit.html ├── user │ └── settings_failed.html ├── layouts │ ├── base_public.html │ └── layout.html ├── login │ └── login.html └── message │ ├── show.html │ └── new.html ├── src ├── util │ ├── user.rs │ ├── rand.rs │ ├── password.rs │ └── mod.rs ├── models │ ├── mod.rs │ ├── group.rs │ ├── category.rs │ ├── static_content.rs │ ├── peer.rs │ └── chat.rs ├── handlers │ ├── mod.rs │ ├── static_content.rs │ └── chat.rs ├── api │ ├── user.rs │ ├── mod.rs │ └── chat.rs ├── cleanup.rs ├── db.rs ├── error.rs ├── app │ ├── index.rs │ ├── login.rs │ ├── message.rs │ ├── signup.rs │ └── static_content.rs ├── settings.rs ├── state.rs ├── tracker │ └── scrape.rs └── template │ └── mod.rs ├── doc └── sql │ ├── groups.sql │ └── categories.sql ├── Cargo.toml ├── config └── ripalt.toml.example ├── README.md ├── CHANGELOG.md └── assets └── scss └── main.scss /migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webroot/aimg/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webroot/timg/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 -------------------------------------------------------------------------------- /migrations/2018-04-09-135738_create_groups/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.groups; -------------------------------------------------------------------------------- /migrations/2018-04-09-135835_create_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.users; -------------------------------------------------------------------------------- /migrations/2018-04-09-140606_create_peers/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.peers; -------------------------------------------------------------------------------- /webroot/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | ../../../bootstrap/dist/js/bootstrap.min.js -------------------------------------------------------------------------------- /migrations/2018-04-09-140244_create_torrents/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.torrents; -------------------------------------------------------------------------------- /migrations/2018-04-23-164613_create_transfers/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.transfers; -------------------------------------------------------------------------------- /migrations/2018-05-03-120715_create_messages/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.messages; -------------------------------------------------------------------------------- /migrations/2018-04-09-135642_create_categories/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.categories; -------------------------------------------------------------------------------- /migrations/2018-04-17-121101_create_torrent_nfos/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.torrent_nfos; -------------------------------------------------------------------------------- /migrations/2018-04-18-191602_create_torrent_list/down.sql: -------------------------------------------------------------------------------- 1 | DROP VIEW public.torrent_list; -------------------------------------------------------------------------------- /migrations/2018-04-09-230549_create_acl_user_rules/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.acl_user_rules; -------------------------------------------------------------------------------- /migrations/2018-04-16-202936_create_torrent_files/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.torrent_files; -------------------------------------------------------------------------------- /migrations/2018-04-23-164650_create_user_transfer/down.sql: -------------------------------------------------------------------------------- 1 | DROP VIEW public.user_transfer; -------------------------------------------------------------------------------- /migrations/2018-04-26-182812_create_chat_messages/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.chat_messages; -------------------------------------------------------------------------------- /migrations/2018-04-30-090640_create_torrent_images/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.torrent_images; -------------------------------------------------------------------------------- /migrations/2018-05-03-232652_create_static_content/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.static_content; -------------------------------------------------------------------------------- /migrations/2018-05-08-150335_create_user_profiles/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.user_profiles; -------------------------------------------------------------------------------- /migrations/2018-04-09-140018_create_user_properties/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.user_properties; -------------------------------------------------------------------------------- /migrations/2018-05-03-114210_create_message_folders/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.message_folders; -------------------------------------------------------------------------------- /migrations/2018-05-13-092226_create_torrent_comments/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.torrent_comments; -------------------------------------------------------------------------------- /migrations/2018-04-16-202932_create_torrent_meta_files/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.torrent_meta_files; -------------------------------------------------------------------------------- /migrations/2018-04-23-211719_create_completed_torrents/down.sql: -------------------------------------------------------------------------------- 1 | DROP VIEW public.completed_torrents; -------------------------------------------------------------------------------- /migrations/2018-04-30-095301_update_torrent_defaults/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` -------------------------------------------------------------------------------- /webroot/static/img/6NfmQ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/img/6NfmQ.jpg -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "bootstrap"] 2 | path = bootstrap 3 | url = https://github.com/twbs/bootstrap.git 4 | branch = v4-dev 5 | -------------------------------------------------------------------------------- /migrations/2018-04-09-202959_create_acl_group_rules/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE public.acl_group_rules; 2 | DROP TYPE public.acl_permission; -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /webroot/static/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuchsi/ripalt/HEAD/webroot/static/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /migrations/2018-04-29-150223_add_last_active_to_users/down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX public.users_last_active_key; 2 | ALTER TABLE public.users 3 | DROP COLUMN last_active; -------------------------------------------------------------------------------- /migrations/2018-04-16-202932_create_torrent_meta_files/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.torrent_meta_files 2 | ( 3 | id uuid NOT NULL, 4 | data bytea NOT NULL, 5 | CONSTRAINT torrent_meta_files_pkey PRIMARY KEY (id) 6 | ) 7 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /migrations/2018-04-30-095301_update_torrent_defaults/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.torrents 2 | ALTER COLUMN visible SET DEFAULT false, 3 | ALTER COLUMN completed SET DEFAULT 0, 4 | ALTER COLUMN last_action SET DEFAULT NULL, 5 | ALTER COLUMN last_seeder SET DEFAULT NULL; -------------------------------------------------------------------------------- /migrations/2018-04-29-150223_add_last_active_to_users/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE public.users 2 | ADD COLUMN last_active timestamp with time zone; 3 | 4 | CREATE INDEX users_last_active_key 5 | ON public.users USING btree 6 | (last_active ASC NULLS LAST) 7 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Generated by Cargo 3 | # will have compiled files and executables 4 | /target/ 5 | **/*.rs.bk 6 | 7 | # Rust Plugin for IntelliJ 8 | *.iml 9 | 10 | # Dotenv files 11 | .env 12 | 13 | config/ripalt.toml 14 | webroot/static/css/main.css 15 | webroot/static/css/main.css.map 16 | webroot/timg/* 17 | webroot/aimg/* -------------------------------------------------------------------------------- /templates/index/public.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_public.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |

Welcome to ripalt!

7 |
8 |
9 |
10 | {% endblock content %} -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /migrations/2018-04-09-135642_create_categories/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.categories 2 | ( 3 | id uuid NOT NULL, 4 | name character varying(255) COLLATE pg_catalog."default" NOT NULL, 5 | created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | CONSTRAINT categories_pkey PRIMARY KEY (id), 8 | CONSTRAINT categories_name_key UNIQUE (name) 9 | ) 10 | 11 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /migrations/2018-04-17-121101_create_torrent_nfos/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.torrent_nfos 2 | ( 3 | id uuid NOT NULL, 4 | torrent_id uuid NOT NULL, 5 | data bytea NOT NULL, 6 | CONSTRAINT torrent_nfos_pkey PRIMARY KEY (id), 7 | CONSTRAINT torrent_id_key FOREIGN KEY (torrent_id) 8 | REFERENCES public.torrents (id) MATCH SIMPLE 9 | ON UPDATE CASCADE 10 | ON DELETE CASCADE 11 | ) 12 | TABLESPACE pg_default; 13 | 14 | CREATE INDEX torrent_nfos_torrent_id_index 15 | ON public.torrent_nfos USING btree 16 | (torrent_id) 17 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /templates/signup/confirm_fail.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_public.html" %} 2 | {% block title %}Confirmation{% endblock title %} 3 | {% block content %} 4 |
5 |
6 |
7 | Confirmation 8 |
9 |
10 |
Confirmation failed
11 |

{{error}}

12 |
13 |
14 |
15 | {% endblock content %} -------------------------------------------------------------------------------- /migrations/2018-05-03-232652_create_static_content/up.sql: -------------------------------------------------------------------------------- 1 | -- DROP TABLE public.static_content; 2 | 3 | CREATE TABLE public.static_content 4 | ( 5 | id character varying(100) COLLATE pg_catalog."default" NOT NULL, 6 | title character varying(100) COLLATE pg_catalog."default" NOT NULL, 7 | content text COLLATE pg_catalog."default" NOT NULL, 8 | content_type character varying(32) COLLATE pg_catalog."default" NOT NULL, 9 | created_at timestamp with time zone NOT NULL, 10 | updated_at timestamp with time zone NOT NULL, 11 | CONSTRAINT static_content_pkey PRIMARY KEY (id) 12 | ) 13 | WITH ( 14 | OIDS = FALSE 15 | ) 16 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /webroot/static/css/fa-brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Brands;font-style:normal;font-weight:400;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:Font Awesome\ 5 Brands} -------------------------------------------------------------------------------- /webroot/static/css/fa-solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:900;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:Font Awesome\ 5 Free;font-weight:900} -------------------------------------------------------------------------------- /migrations/2018-04-09-135738_create_groups/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.groups 2 | ( 3 | id uuid NOT NULL, 4 | name character varying COLLATE pg_catalog."default" NOT NULL, 5 | parent_id uuid, 6 | created_at timestamp(6) with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | updated_at timestamp(6) with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | CONSTRAINT groups_pkey PRIMARY KEY (id), 9 | CONSTRAINT groups_name_key UNIQUE (name), 10 | CONSTRAINT groups_parent_key FOREIGN KEY (parent_id) 11 | REFERENCES public.groups (id) MATCH SIMPLE 12 | ON UPDATE NO ACTION 13 | ON DELETE RESTRICT 14 | ) 15 | TABLESPACE pg_default; 16 | -------------------------------------------------------------------------------- /webroot/static/css/fa-regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face{font-family:Font Awesome\ 5 Free;font-style:normal;font-weight:400;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:Font Awesome\ 5 Free;font-weight:400} -------------------------------------------------------------------------------- /templates/signup/confirm_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_public.html" %} 2 | {% block title %}Confirmation{% endblock title %} 3 | {% block content %} 4 |
5 |
6 |
7 | Confirmation 8 |
9 |
10 |
Confirmation successful
11 |

You have successfully confirmed your account

12 | Goto the main page 13 |
14 |
15 |
16 | {% endblock content %} -------------------------------------------------------------------------------- /migrations/2018-05-08-150335_create_user_profiles/up.sql: -------------------------------------------------------------------------------- 1 | -- DROP TABLE public.user_profiles; 2 | 3 | CREATE TABLE public.user_profiles 4 | ( 5 | id uuid NOT NULL, 6 | avatar character varying(100) COLLATE pg_catalog."default" DEFAULT NULL::character varying, 7 | flair character varying(100) COLLATE pg_catalog."default" DEFAULT NULL::character varying, 8 | about text COLLATE pg_catalog."default", 9 | CONSTRAINT user_profiles_pkey PRIMARY KEY (id), 10 | CONSTRAINT user_profiles_id_fkey FOREIGN KEY (id) 11 | REFERENCES public.users (id) MATCH SIMPLE 12 | ON UPDATE CASCADE 13 | ON DELETE CASCADE 14 | ) 15 | WITH ( 16 | OIDS = FALSE 17 | ) 18 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /webroot/static/css/fa-brands.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Brands'; 7 | font-style: normal; 8 | font-weight: normal; 9 | src: url("../webfonts/fa-brands-400.eot"); 10 | src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); } 11 | 12 | .fab { 13 | font-family: 'Font Awesome 5 Brands'; } 14 | -------------------------------------------------------------------------------- /migrations/2018-04-23-211719_create_completed_torrents/up.sql: -------------------------------------------------------------------------------- 1 | -- View: public.completed_torrents 2 | 3 | -- DROP VIEW public.completed_torrents; 4 | 5 | CREATE OR REPLACE VIEW public.completed_torrents AS 6 | SELECT tr.id, 7 | tr.user_id, 8 | tr.torrent_id, 9 | tr.bytes_downloaded, 10 | tr.bytes_uploaded, 11 | tr.time_seeded, 12 | tr.completed_at, 13 | t.name, 14 | t.size, 15 | COALESCE(ut.is_seeder, false) AS is_seeder, 16 | tl.seeder, 17 | tl.leecher 18 | FROM transfers tr 19 | JOIN torrents t ON t.id = tr.torrent_id 20 | JOIN torrent_list tl ON tl.id = tr.torrent_id 21 | LEFT JOIN user_transfer ut ON ut.torrent_id = tr.torrent_id AND ut.user_id = tr.user_id AND ut.is_seeder = true 22 | WHERE tr.completed_at IS NOT NULL; -------------------------------------------------------------------------------- /templates/error/5xx.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_public.html" %} 2 | {% block content %} 3 |
4 |

{{status}}

5 |

Something went wrong

6 |
{{error}}
7 |
8 |
9 |

URI

10 |
{{uri}}
11 | 12 |

Headers

13 |
{{headers}}
14 |
15 | {% endblock content %} 16 | {% block title %}{{status}}{% endblock title %} 17 | {% block header %} 18 | 21 | {% endblock %} -------------------------------------------------------------------------------- /webroot/static/css/fa-regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.0.10 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 400; 9 | src: url("../webfonts/fa-regular-400.eot"); 10 | src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); } 11 | 12 | .far { 13 | font-family: 'Font Awesome 5 Free'; 14 | font-weight: 400; } 15 | -------------------------------------------------------------------------------- /templates/torrent/failed.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
Failure
8 |
9 |
{{sub_title}}
10 |
11 |

{{error}}

12 | Go back 13 |
14 |
15 |
16 |
17 |
18 |
19 | {% endblock content %} 20 | {% block title %}{{title}}{% endblock title %} -------------------------------------------------------------------------------- /migrations/2018-04-16-202936_create_torrent_files/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.torrent_files 2 | ( 3 | id uuid NOT NULL, 4 | torrent_id uuid NOT NULL, 5 | file_name character varying (255) NOT NULL, 6 | size bigint NOT NULL, 7 | CONSTRAINT torrent_files_pkey PRIMARY KEY (id), 8 | CONSTRAINT torrent_id_key FOREIGN KEY (torrent_id) 9 | REFERENCES public.torrents (id) MATCH SIMPLE 10 | ON UPDATE CASCADE 11 | ON DELETE CASCADE 12 | ) 13 | TABLESPACE pg_default; 14 | 15 | CREATE INDEX torrent_files_torrent_id_index 16 | ON public.torrent_files USING btree 17 | (torrent_id) 18 | TABLESPACE pg_default; 19 | 20 | CREATE INDEX torrent_files_torrent_file_name_index 21 | ON public.torrent_files USING btree 22 | (file_name) 23 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /templates/static_content/edit_failed.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
Failure
8 |
9 |
{{title}}
10 |
11 |

{{error}}

12 | Go back 13 |
14 |
15 |
16 |
17 |
18 |
19 | {% endblock content %} 20 | {% block title %}{{title}}{% endblock title %} -------------------------------------------------------------------------------- /templates/torrent/denied.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
Error
8 |
9 |
Insufficient privileges
10 |
11 |

{{message}}

12 | Go back 13 |
14 |
15 |
16 |
17 |
18 |
19 | {% endblock content %} 20 | {% block title %}{{title}}{% endblock title %} -------------------------------------------------------------------------------- /templates/torrent/success.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
Success
8 |
9 |
{{sub_title}}
10 |
11 |

{{message}}

12 | Continue 13 |
14 |
15 |
16 |
17 |
18 |
19 | {% endblock content %} 20 | {% block title %}{{title}}{% endblock title %} -------------------------------------------------------------------------------- /templates/error/4xx.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_public.html" %} 2 | {% block content %} 3 |
4 |
5 |

{{status}}

6 |
7 |
8 |
9 |
10 |

URI

11 |
{{uri}}
12 | 13 |

Headers

14 |
{{headers}}
15 |
16 |
17 | {% endblock content %} 18 | {% block title %}{{status}}{% endblock title %} 19 | {% block header %} 20 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/user/settings_failed.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
Error
8 |
9 |
Update User settings failed
10 |
11 |

{{error}}

12 | Go back 13 |
14 |
15 |
16 |
17 |
18 |
19 | {% endblock content %} 20 | {% block title %}Error{% endblock title %} -------------------------------------------------------------------------------- /migrations/2018-04-26-182812_create_chat_messages/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | CREATE TABLE "public".chat_messages ( 3 | id uuid NOT NULL, 4 | user_id uuid NOT NULL, 5 | chat smallint NOT NULL DEFAULT 0, 6 | message text NOT NULL, 7 | created_at timestamp with time zone NOT NULL DEFAULT now(), 8 | CONSTRAINT chat_messages_pkey PRIMARY KEY (id), 9 | CONSTRAINT chat_messages_user_id_fkey FOREIGN KEY (user_id) 10 | REFERENCES "public".users (id) MATCH SIMPLE 11 | ON UPDATE CASCADE 12 | ON DELETE CASCADE 13 | ) 14 | WITH ( 15 | OIDS = FALSE 16 | ) 17 | TABLESPACE pg_default; 18 | 19 | CREATE INDEX chat_messages_chat_key 20 | ON "public".chat_messages USING btree 21 | (chat) 22 | TABLESPACE pg_default; 23 | 24 | CREATE INDEX chat_messages_created_at_key 25 | ON "public".chat_messages USING btree 26 | (created_at) 27 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /templates/static_content/view.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
{{title}}
8 |
9 | {{content | safe }} 10 | 11 | {% if may_edit %} 12 | Edit 13 | {% endif %} 14 |
15 | 18 |
19 |
20 |
21 |
22 | 23 | {% endblock content %} 24 | {% block title %}{{title}}{% endblock title %} -------------------------------------------------------------------------------- /migrations/2018-04-30-090640_create_torrent_images/up.sql: -------------------------------------------------------------------------------- 1 | -- DROP TABLE public.torrent_images; 2 | 3 | CREATE TABLE public.torrent_images 4 | ( 5 | id uuid NOT NULL, 6 | torrent_id uuid NOT NULL, 7 | file_name character varying(255) COLLATE pg_catalog."default" NOT NULL, 8 | index smallint NOT NULL, 9 | created_at timestamp with time zone NOT NULL DEFAULT now(), 10 | CONSTRAINT torrent_images_pkey PRIMARY KEY (id), 11 | CONSTRAINT torrent_images_torrent_id_file_name_key UNIQUE (torrent_id, file_name), 12 | CONSTRAINT torrent_images_torrent_id_index_key UNIQUE (torrent_id, index), 13 | CONSTRAINT torrent_images_torrent_id_fkey FOREIGN KEY (torrent_id) 14 | REFERENCES public.torrents (id) MATCH SIMPLE 15 | ON UPDATE NO CASCADE 16 | ON DELETE NO CASCADE, 17 | CONSTRAINT torrent_images_index_check CHECK (index >= 0) 18 | ) 19 | WITH ( 20 | OIDS = FALSE 21 | ) 22 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /migrations/2018-05-13-092226_create_torrent_comments/up.sql: -------------------------------------------------------------------------------- 1 | -- DROP TABLE public.torrent_comments; 2 | 3 | CREATE TABLE public.torrent_comments 4 | ( 5 | id uuid NOT NULL, 6 | user_id uuid NOT NULL, 7 | torrent_id uuid NOT NULL, 8 | content text COLLATE pg_catalog."default" NOT NULL, 9 | created_at timestamp with time zone NOT NULL DEFAULT now(), 10 | updated_at timestamp with time zone NOT NULL DEFAULT now(), 11 | CONSTRAINT torrent_comments_pkey PRIMARY KEY (id), 12 | CONSTRAINT torrent_comments_torrent_id_fkey FOREIGN KEY (torrent_id) 13 | REFERENCES public.torrents (id) MATCH SIMPLE 14 | ON UPDATE CASCADE 15 | ON DELETE CASCADE, 16 | CONSTRAINT torrent_comments_user_id_fkey FOREIGN KEY (user_id) 17 | REFERENCES public.users (id) MATCH SIMPLE 18 | ON UPDATE CASCADE 19 | ON DELETE CASCADE 20 | ) 21 | WITH ( 22 | OIDS = FALSE 23 | ) 24 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /templates/layouts/base_public.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/layout.html" %} 2 | {% block header %} 3 | 17 | {% endblock header %} 18 | {% block footer %} 19 | 26 | {% endblock footer %} -------------------------------------------------------------------------------- /templates/signup/signup_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_public.html" %} 2 | {% block title %}Sign Up{% endblock title %} 3 | {% block content %} 4 |
5 |
6 |
7 | Sign Up 8 |
9 |
10 |
Sign Up successful
11 |

You have successfully signed up for ripalt

12 | {% if confirm_id %} 13 |

14 | Please click here to confirm your 15 | account. 16 |

17 | {% else %} 18 |

You should soon receive an email with the confirmation link.

19 | {% endif %} 20 |
21 |
22 |
23 | {% endblock content %} -------------------------------------------------------------------------------- /migrations/2018-04-23-164650_create_user_transfer/up.sql: -------------------------------------------------------------------------------- 1 | -- View: public.user_transfer 2 | 3 | -- DROP VIEW public.user_transfer; 4 | 5 | CREATE OR REPLACE VIEW public.user_transfer AS 6 | SELECT p.id, 7 | t.id AS torrent_id, 8 | u.id AS user_id, 9 | t.name, 10 | p.seeder AS is_seeder, 11 | t.size, 12 | COALESCE(p2.seeder, 0::bigint) AS seeder, 13 | COALESCE(p2.leecher, 0::bigint) AS leecher, 14 | p.bytes_uploaded, 15 | p.bytes_downloaded, 16 | tr.bytes_uploaded AS total_uploaded, 17 | tr.bytes_downloaded AS total_downloaded 18 | FROM peers p 19 | JOIN torrents t ON t.id = p.torrent_id 20 | JOIN users u ON u.id = p.user_id 21 | JOIN transfers tr ON tr.torrent_id = t.id AND tr.user_id = u.id 22 | LEFT JOIN ( SELECT peers.torrent_id, 23 | count(peers.id) FILTER (WHERE peers.seeder = true) AS seeder, 24 | count(peers.id) FILTER (WHERE peers.seeder = false) AS leecher 25 | FROM peers 26 | GROUP BY peers.torrent_id) p2 ON p2.torrent_id = t.id 27 | GROUP BY p.id, t.id, u.id, p2.seeder, p2.leecher, tr.id; 28 | -------------------------------------------------------------------------------- /migrations/2018-05-14-132001_add_comments_to_torrent_list/down.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW public.torrent_list 2 | WITH (security_barrier=false) 3 | AS 4 | SELECT t.id, 5 | t.info_hash, 6 | t.name, 7 | t.category_id, 8 | c.name AS category_name, 9 | t.user_id, 10 | u.name AS user_name, 11 | t.size, 12 | count(f.id) AS files, 13 | t.visible, 14 | t.completed, 15 | COALESCE(p.seeder, 0::bigint) AS seeder, 16 | COALESCE(p.leecher, 0::bigint) AS leecher, 17 | t.last_action, 18 | t.last_seeder, 19 | t.created_at 20 | FROM torrents t 21 | LEFT JOIN users u ON u.id = t.user_id 22 | JOIN categories c ON c.id = t.category_id 23 | JOIN torrent_files f ON f.torrent_id = t.id 24 | LEFT JOIN ( SELECT peers.torrent_id, 25 | count(peers.id) FILTER (WHERE peers.seeder = true) AS seeder, 26 | count(peers.id) FILTER (WHERE peers.seeder = false) AS leecher 27 | FROM peers 28 | GROUP BY peers.torrent_id) p ON p.torrent_id = t.id 29 | GROUP BY t.id, c.id, u.id, p.seeder, p.leecher 30 | ORDER BY t.created_at DESC; -------------------------------------------------------------------------------- /migrations/2018-04-18-191602_create_torrent_list/up.sql: -------------------------------------------------------------------------------- 1 | -- View: public.torrent_list 2 | 3 | -- DROP VIEW public.torrent_list; 4 | 5 | CREATE OR REPLACE VIEW public.torrent_list AS 6 | SELECT 7 | t.id, 8 | t.info_hash, 9 | t.name, 10 | t.category_id, 11 | c.name AS "category_name", 12 | t.user_id, 13 | u.name AS "user_name", 14 | t.size, 15 | COUNT(f.id) AS "files", 16 | t.visible, 17 | t.completed, 18 | COALESCE(p.seeder, 0) AS "seeder", 19 | COALESCE(p.leecher, 0) AS "leecher", 20 | t.last_action, 21 | t.last_seeder, 22 | t.created_at 23 | FROM public.torrents AS t 24 | LEFT JOIN public.users AS u ON u.id = t.user_id 25 | INNER JOIN public.categories AS c ON c.id = t.category_id 26 | INNER JOIN public.torrent_files AS f ON f.torrent_id = t.id 27 | LEFT JOIN ( 28 | SELECT torrent_id, COUNT(id) FILTER (WHERE seeder = E'true') AS "seeder", COUNT(id) FILTER (WHERE seeder = E'false') AS "leecher" 29 | FROM public.peers 30 | GROUP BY torrent_id 31 | ) AS p ON p.torrent_id = t.id 32 | GROUP BY t.id, c.id, u.id, p.seeder, p.leecher 33 | ORDER BY t.created_at DESC 34 | 35 | -------------------------------------------------------------------------------- /src/util/user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use diesel::PgConnection; 20 | use models::user::Property; 21 | use uuid::Uuid; 22 | 23 | use SETTINGS; 24 | 25 | pub fn user_timezone(user_id: &Uuid, db: &PgConnection) -> i32 { 26 | if let Some(prop) = Property::find(user_id, "timezone", db) { 27 | if let Some(number) = prop.value().as_i64() { 28 | return number as i32; 29 | } 30 | } 31 | 32 | SETTINGS.read().unwrap().user.default_timezone 33 | } 34 | -------------------------------------------------------------------------------- /migrations/2018-04-09-230549_create_acl_user_rules/up.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.acl_user_rules 2 | 3 | -- DROP TABLE public.acl_user_rules; 4 | 5 | CREATE TABLE public.acl_user_rules 6 | ( 7 | id uuid NOT NULL, 8 | namespace character varying(100) COLLATE pg_catalog."default" NOT NULL, 9 | user_id uuid NOT NULL, 10 | permission acl_permission NOT NULL, 11 | CONSTRAINT acl_user_rules_pkey PRIMARY KEY (id), 12 | CONSTRAINT acl_user_rules_namespace_user_id_key UNIQUE (namespace, user_id), 13 | CONSTRAINT acl_user_rules_user_id_fkey FOREIGN KEY (user_id) 14 | REFERENCES public.users (id) MATCH SIMPLE 15 | ON UPDATE NO ACTION 16 | ON DELETE NO ACTION 17 | ) 18 | TABLESPACE pg_default; 19 | 20 | -- Index: acl_user_rules_namespae_index 21 | 22 | -- DROP INDEX public.acl_user_rules_namespae_index; 23 | 24 | CREATE INDEX acl_user_rules_namespae_index 25 | ON public.acl_user_rules USING btree 26 | (namespace COLLATE pg_catalog."default") 27 | TABLESPACE pg_default; 28 | 29 | -- Index: acl_user_rules_user_id_index 30 | 31 | -- DROP INDEX public.acl_user_rules_user_id_index; 32 | 33 | CREATE INDEX acl_user_rules_user_id_index 34 | ON public.acl_user_rules USING btree 35 | (user_id) 36 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /src/util/rand.rs: -------------------------------------------------------------------------------- 1 | //! RNG related functions 2 | 3 | use rand::{OsRng, Rng}; 4 | use std::cell::RefCell; 5 | 6 | /// Generate `len` random bytes 7 | /// 8 | /// # Panics 9 | /// 10 | /// The function panics if `len` is 0 11 | /// 12 | /// # Example 13 | /// 14 | /// ``` 15 | /// use util::rand; 16 | /// 17 | /// fn main() { 18 | /// let bytes = rand::gen_random_bytes(32); 19 | /// 20 | /// assert!(bytes.len() == 32); 21 | /// } 22 | /// ``` 23 | pub fn gen_random_bytes(len: usize) -> Vec { 24 | assert!(len > 0); 25 | thread_local!(static RNG: RefCell = RefCell::new(OsRng::new().unwrap())); 26 | 27 | RNG.with(|rng| { 28 | let mut brng = rng.borrow_mut(); 29 | 30 | let mut salt: Vec = Vec::with_capacity(len); 31 | unsafe { 32 | salt.set_len(len); 33 | } 34 | 35 | brng.fill_bytes(&mut salt); 36 | 37 | salt 38 | }) 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_random_bytes() { 47 | let bytes = gen_random_bytes(16); 48 | assert_eq!(bytes.len(), 16); 49 | assert_ne!(bytes, gen_random_bytes(16)); 50 | } 51 | 52 | #[test] 53 | #[should_panic] 54 | fn test_random_bytes_panics() { 55 | gen_random_bytes(0); 56 | } 57 | } -------------------------------------------------------------------------------- /migrations/2018-05-14-132001_add_comments_to_torrent_list/up.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW public.torrent_list 2 | WITH (security_barrier=false) 3 | AS 4 | SELECT t.id, 5 | t.info_hash, 6 | t.name, 7 | t.category_id, 8 | c.name AS category_name, 9 | t.user_id, 10 | u.name AS user_name, 11 | t.size, 12 | count(f.id) AS files, 13 | t.visible, 14 | t.completed, 15 | COALESCE(p.seeder, 0::bigint) AS seeder, 16 | COALESCE(p.leecher, 0::bigint) AS leecher, 17 | t.last_action, 18 | t.last_seeder, 19 | t.created_at, 20 | COALESCE(com.comments, 0::bigint) AS comments 21 | FROM torrents t 22 | LEFT JOIN users u ON u.id = t.user_id 23 | JOIN categories c ON c.id = t.category_id 24 | JOIN torrent_files f ON f.torrent_id = t.id 25 | LEFT JOIN ( SELECT com.torrent_id, count(com.id) as comments FROM torrent_comments com GROUP BY com.torrent_id) com ON com.torrent_id = t.id 26 | LEFT JOIN ( SELECT peers.torrent_id, 27 | count(peers.id) FILTER (WHERE peers.seeder = true) AS seeder, 28 | count(peers.id) FILTER (WHERE peers.seeder = false) AS leecher 29 | FROM peers 30 | GROUP BY peers.torrent_id) p ON p.torrent_id = t.id 31 | GROUP BY t.id, c.id, u.id, p.seeder, p.leecher, com.comments 32 | ORDER BY t.created_at DESC; -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /migrations/2018-05-03-114210_create_message_folders/up.sql: -------------------------------------------------------------------------------- 1 | -- DROP TABLE public.message_folders; 2 | 3 | CREATE TABLE public.message_folders 4 | ( 5 | id uuid NOT NULL, 6 | user_id uuid NOT NULL, 7 | name character varying(100) COLLATE pg_catalog."default" NOT NULL, 8 | purge smallint NOT NULL, 9 | CONSTRAINT message_folders_pkey PRIMARY KEY (id), 10 | CONSTRAINT message_folders_user_id_name_key UNIQUE (user_id, name), 11 | CONSTRAINT message_folders_user_id_fkey FOREIGN KEY (user_id) 12 | REFERENCES public.users (id) MATCH SIMPLE 13 | ON UPDATE CASCADE 14 | ON DELETE CASCADE, 15 | CONSTRAINT message_folders_purge_check CHECK (purge >= 0) 16 | ) 17 | WITH ( 18 | OIDS = FALSE 19 | ) 20 | TABLESPACE pg_default; 21 | 22 | WITH folder AS ( 23 | SELECT gen_random_uuid() as id, u.id AS user_id, 'inbox' AS name, 0 as purge FROM public.users u 24 | ) 25 | INSERT INTO public.message_folders SELECT * FROM folder; 26 | WITH folder AS ( 27 | SELECT gen_random_uuid() as id, u.id AS user_id, 'sent' AS name, 0 as purge FROM public.users u 28 | ) 29 | INSERT INTO public.message_folders SELECT * FROM folder; 30 | WITH folder AS ( 31 | SELECT gen_random_uuid() as id, u.id AS user_id, 'system' AS name, 0 as purge FROM public.users u 32 | ) 33 | INSERT INTO public.message_folders SELECT * FROM folder; 34 | -------------------------------------------------------------------------------- /doc/sql/groups.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 10.3 6 | -- Dumped by pg_dump version 10.3 7 | 8 | -- Started on 2018-04-24 03:24:44 CEST 9 | 10 | SET statement_timeout = 0; 11 | SET lock_timeout = 0; 12 | SET idle_in_transaction_session_timeout = 0; 13 | SET client_encoding = 'UTF8'; 14 | SET standard_conforming_strings = on; 15 | SELECT pg_catalog.set_config('search_path', '', false); 16 | SET check_function_bodies = false; 17 | SET client_min_messages = warning; 18 | SET row_security = off; 19 | 20 | -- 21 | -- TOC entry 2293 (class 0 OID 19566) 22 | -- Dependencies: 200 23 | -- Data for Name: groups; Type: TABLE DATA; Schema: public; Owner: ripalt 24 | -- 25 | 26 | INSERT INTO public.groups VALUES ('0eb8ac8f-01f4-4bf9-bb0d-e3ac0ecb15f9', 'User', NULL, NOW(), NOW()); 27 | INSERT INTO public.groups VALUES ('91c1ba93-6153-4913-9993-18ba638452d2', 'Moderator', '0eb8ac8f-01f4-4bf9-bb0d-e3ac0ecb15f9', NOW(), NOW()); 28 | INSERT INTO public.groups VALUES ('5a4517e3-f615-43f3-8852-9bb310ae688e', 'Administrator', '91c1ba93-6153-4913-9993-18ba638452d2', NOW(), NOW()); 29 | INSERT INTO public.groups VALUES ('7ad31559-5be8-40e0-9656-8b50ad1cdb39', 'Sysop', '5a4517e3-f615-43f3-8852-9bb310ae688e', NOW(), NOW()); 30 | 31 | 32 | -- Completed on 2018-04-24 03:24:44 CEST 33 | 34 | -- 35 | -- PostgreSQL database dump complete 36 | -- 37 | 38 | -------------------------------------------------------------------------------- /migrations/2018-04-09-202959_create_acl_group_rules/up.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.acl_group_rules 2 | 3 | -- DROP TABLE public.acl_group_rules; 4 | 5 | CREATE TYPE public.acl_permission AS ENUM 6 | ('none', 'read', 'write', 'create', 'delete'); 7 | 8 | CREATE TABLE public.acl_group_rules 9 | ( 10 | id uuid NOT NULL, 11 | namespace character varying(100) COLLATE pg_catalog."default" NOT NULL, 12 | group_id uuid NOT NULL, 13 | permission acl_permission NOT NULL, 14 | CONSTRAINT acl_group_rules_pkey PRIMARY KEY (id), 15 | CONSTRAINT acl_group_rules_namespace_group_id_key UNIQUE (namespace, group_id), 16 | CONSTRAINT acl_group_rules_group_id_fkey FOREIGN KEY (group_id) 17 | REFERENCES public.groups (id) MATCH SIMPLE 18 | ON UPDATE CASCADE 19 | ON DELETE CASCADE 20 | ) 21 | TABLESPACE pg_default; 22 | 23 | 24 | -- Index: acl_group_rules_group_id_key 25 | 26 | -- DROP INDEX public.acl_group_rules_group_id_key; 27 | 28 | CREATE INDEX acl_group_rules_group_id_key 29 | ON public.acl_group_rules USING btree 30 | (group_id) 31 | TABLESPACE pg_default; 32 | 33 | -- Index: acl_group_rules_namespace_index 34 | 35 | -- DROP INDEX public.acl_group_rules_namespace_index; 36 | 37 | CREATE INDEX acl_group_rules_namespace_index 38 | ON public.acl_group_rules USING btree 39 | (namespace COLLATE pg_catalog."default") 40 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /migrations/2018-04-09-140018_create_user_properties/up.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.user_properties 2 | 3 | -- DROP TABLE public.user_properties; 4 | 5 | CREATE TABLE public.user_properties 6 | ( 7 | id uuid NOT NULL, 8 | user_id uuid NOT NULL, 9 | name character varying COLLATE pg_catalog."default" NOT NULL, 10 | value jsonb NOT NULL, 11 | created_at timestamp(6) with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | updated_at timestamp(6) with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | CONSTRAINT user_properties_pkey PRIMARY KEY (id), 14 | CONSTRAINT user_properties_user_id_name_key UNIQUE (user_id, name), 15 | CONSTRAINT user_properties_user_id_key FOREIGN KEY (user_id) 16 | REFERENCES public.users (id) MATCH SIMPLE 17 | ON UPDATE CASCADE 18 | ON DELETE CASCADE 19 | ) 20 | TABLESPACE pg_default; 21 | 22 | -- Index: user_properties_user_id_index 23 | 24 | -- DROP INDEX public.user_properties_user_id_index; 25 | 26 | CREATE INDEX user_properties_user_id_index 27 | ON public.user_properties USING btree 28 | (user_id) 29 | TABLESPACE pg_default; 30 | 31 | -- Index: user_properties_user_id_name_index 32 | 33 | -- DROP INDEX public.user_properties_user_id_name_index; 34 | 35 | CREATE UNIQUE INDEX user_properties_user_id_name_index 36 | ON public.user_properties USING btree 37 | (user_id, name COLLATE pg_catalog."default") 38 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /templates/layouts/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | {% block title %}index{% endblock title %} - ripalt 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% endblock head %} 14 | 15 | 16 | {% block header %}{% endblock header %} 17 |
18 |
19 | {% block sidebar %}
{% endblock sidebar %} 20 | {% block content %}{% endblock content %} 21 |
22 |
23 |
24 | {% block footer %}{% endblock footer %} 25 | 26 | 27 | 28 | 29 | {% block script %}{% endblock script %} 30 | 31 | 32 | -------------------------------------------------------------------------------- /migrations/2018-05-03-120715_create_messages/up.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.messages 2 | 3 | -- DROP TABLE public.messages; 4 | 5 | CREATE TABLE public.messages 6 | ( 7 | id uuid NOT NULL, 8 | folder_id uuid NOT NULL, 9 | sender_id uuid, 10 | receiver_id uuid NOT NULL, 11 | subject character varying(255) COLLATE pg_catalog."default" NOT NULL, 12 | body text COLLATE pg_catalog."default" NOT NULL, 13 | is_read boolean NOT NULL DEFAULT false, 14 | created_at timestamp with time zone NOT NULL DEFAULT now(), 15 | CONSTRAINT messages_pkey PRIMARY KEY (id), 16 | CONSTRAINT messages_folder_id_fkey FOREIGN KEY (folder_id) 17 | REFERENCES public.message_folders (id) MATCH SIMPLE 18 | ON UPDATE CASCADE 19 | ON DELETE CASCADE, 20 | CONSTRAINT messages_receiver_id_fkey FOREIGN KEY (receiver_id) 21 | REFERENCES public.users (id) MATCH SIMPLE 22 | ON UPDATE CASCADE 23 | ON DELETE CASCADE, 24 | CONSTRAINT messages_sender_id_fkey FOREIGN KEY (sender_id) 25 | REFERENCES public.users (id) MATCH SIMPLE 26 | ON UPDATE CASCADE 27 | ON DELETE SET NULL 28 | ) 29 | WITH ( 30 | OIDS = FALSE 31 | ) 32 | TABLESPACE pg_default; 33 | 34 | -- Index: messages_folder_id_read_key 35 | 36 | -- DROP INDEX public.messages_folder_id_read_key; 37 | 38 | CREATE INDEX messages_folder_id_read_key 39 | ON public.messages USING btree 40 | (folder_id, is_read) 41 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /src/util/password.rs: -------------------------------------------------------------------------------- 1 | //! Password handling functions 2 | //! 3 | //! # Example 4 | //! 5 | //! ``` 6 | //! use util::{password, rand}; 7 | //! 8 | //! fn main() { 9 | //! let password = "correct horse battery staple"; 10 | //! let salt = rand::gen_random_bytes(32); 11 | //! let hash = password::generate_passhash(password.as_bytes(), &salt); 12 | //! 13 | //! // verify password 14 | //! assert!(password::verify(password.as_bytes(), &hash, &salt)); 15 | //! } 16 | //! ``` 17 | 18 | use argon2rs::{defaults, verifier, Argon2, Variant}; 19 | 20 | /// Generate the argon2i hash for the given password and salt 21 | pub fn generate_passhash(password: &[u8], salt: &[u8]) -> Vec { 22 | let mut passhash = [0u8; defaults::LENGTH]; 23 | let a2 = Argon2::default(Variant::Argon2i); 24 | a2.hash(&mut passhash, password, &salt, &[], &[]); 25 | 26 | passhash.to_vec() 27 | } 28 | 29 | /// Verify that he given password is identical to the stored one 30 | pub fn verify(stored: &[u8], supplied: &[u8], salt: &[u8]) -> bool { 31 | let hash = generate_passhash(supplied, salt); 32 | 33 | verifier::constant_eq(&stored, &hash) 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | use util::rand; 40 | 41 | #[test] 42 | fn test_passhash() { 43 | let password = "correct horse battery staple"; 44 | let salt = rand::gen_random_bytes(32); 45 | let hash = generate_passhash(password.as_bytes(), &salt); 46 | 47 | assert_eq!(hash.len(), defaults::LENGTH); 48 | assert!(verify(&hash, password.as_bytes(), &salt)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ripalt" 3 | version = "0.3.0" 4 | authors = ["Daniel Müller "] 5 | description = "An Anti-Leech Torrent Tracker" 6 | license = "GPL-3.0-or-later" 7 | readme = "README.md" 8 | build = "build.rs" 9 | 10 | [profile.release] 11 | lto = true 12 | 13 | [dependencies] 14 | actix = "0.5.6" 15 | actix-web = "0.6.2" 16 | actix-redis = "0.4.0" 17 | diesel = { version = "1.2.2", features = ["postgres", "extras", "32-column-tables"] } 18 | diesel-derive-enum = { version = "0.4.3", features = ["postgres"] } 19 | r2d2-diesel = "1.0.0" 20 | r2d2 = "0.8.2" 21 | uuid = { version = "0.6.3", features = ["v4", "serde"] } 22 | dotenv = "0.11.0" 23 | serde = "1.0.52" 24 | serde_derive = "1.0.52" 25 | serde_json = "1.0.17" 26 | chrono = { version = "0.4.2", features = ["serde"] } 27 | ipnetwork = "0.12.8" 28 | log = "0.4.1" 29 | env_logger = "0.5.10" 30 | error-chain = "0.11.0" 31 | argon2rs = "0.2.5" 32 | rand = "0.4.2" 33 | config = "0.8.0" 34 | lazy_static = "1.0.0" 35 | ring = "0.12.1" 36 | tera = "0.11.7" 37 | num_cpus = "1.8.0" 38 | walkdir = "2.1.4" 39 | notify = "4.0.3" 40 | futures = "~0.1.21" 41 | multipart = { version = "0.14.2", default-features = false, features = [ "server", "mock" ] } 42 | bytes = "0.4.7" 43 | number_prefix = "0.2.8" 44 | codepage-437 = "0.1.0" 45 | serde_bencode = "0.2.0" 46 | data-encoding = "2.1.1" 47 | url = "1.7.0" 48 | jsonwebtoken = "4.0.1" 49 | regex = "1.0.0" 50 | fast_chemail = "0.9.5" 51 | markdown = { git = "https://github.com/fuchsi/markdown.rs.git", branch = "bootstrap" } 52 | image = "0.19.0" 53 | tempfile = "3.0.1" 54 | 55 | [dev-dependencies] 56 | pretty_assertions = "0.5.1" 57 | 58 | [build-dependencies] 59 | sass-rs = "0.2.1" 60 | walkdir = "2.1.4" 61 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! Data models 20 | 21 | use super::*; 22 | 23 | use diesel; 24 | use diesel::pg::upsert::on_constraint; 25 | 26 | use SETTINGS; 27 | 28 | use error::*; 29 | use schema; 30 | 31 | /// Convenient wrapper around [DateTime](/chrono/struct.DateTime.html)<[Utc](/chrono/struct.Utc.html)> 32 | pub type Timestamp = DateTime; 33 | /// Convenient wrapper around `Vec` 34 | pub type Bytes = Vec; 35 | 36 | pub use self::category::Category; 37 | pub use self::group::Group; 38 | pub use self::message::{Message, MessageFolder}; 39 | pub use self::peer::Peer; 40 | pub use self::torrent::{Torrent, TorrentFile, TorrentImage, TorrentList, TorrentMetaFile, 41 | TorrentMsg, TorrentNFO}; 42 | pub use self::user::{HasUser, MaybeHasUser, Property, User, username}; 43 | 44 | pub mod acl; 45 | pub mod category; 46 | pub mod chat; 47 | pub mod group; 48 | pub mod message; 49 | pub mod peer; 50 | pub mod static_content; 51 | pub mod torrent; 52 | pub mod user; 53 | -------------------------------------------------------------------------------- /src/models/group.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! Use group model 20 | 21 | use super::*; 22 | use super::schema::groups; 23 | 24 | #[derive(Queryable, Debug, Identifiable, Associations, PartialEq, Insertable, AsChangeset, Serialize)] 25 | #[table_name = "groups"] 26 | #[belongs_to(Group, foreign_key = "parent_id")] 27 | pub struct Group { 28 | pub id: Uuid, 29 | pub name: String, 30 | pub parent_id: Option, 31 | pub created_at: Timestamp, 32 | pub updated_at: Timestamp, 33 | } 34 | 35 | impl Default for Group { 36 | fn default() -> Self { 37 | Group { 38 | id: Default::default(), 39 | name: Default::default(), 40 | parent_id: None, 41 | created_at: Utc::now(), 42 | updated_at: Utc::now(), 43 | } 44 | } 45 | } 46 | 47 | impl Group { 48 | /// Find n Group by its id 49 | pub fn find(id: &Uuid, db: &PgConnection) -> Option { 50 | groups::dsl::groups.find(id).first::(db).ok() 51 | } 52 | } -------------------------------------------------------------------------------- /migrations/2018-04-23-164613_create_transfers/up.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.transfers 2 | 3 | -- DROP TABLE public.transfers; 4 | 5 | CREATE TABLE public.transfers 6 | ( 7 | id uuid NOT NULL, 8 | user_id uuid NOT NULL, 9 | torrent_id uuid NOT NULL, 10 | bytes_uploaded bigint NOT NULL, 11 | bytes_downloaded bigint NOT NULL, 12 | time_seeded integer NOT NULL, 13 | created_at timestamp with time zone NOT NULL, 14 | updated_at timestamp with time zone NOT NULL, 15 | completed_at timestamp with time zone, 16 | CONSTRAINT transfers_pkey PRIMARY KEY (id), 17 | CONSTRAINT transfers_user_id_torrent_id_key UNIQUE (user_id, torrent_id), 18 | CONSTRAINT transfers_torrent_id_fkey FOREIGN KEY (torrent_id) 19 | REFERENCES public.torrents (id) MATCH SIMPLE 20 | ON UPDATE CASCADE 21 | ON DELETE CASCADE, 22 | CONSTRAINT transfers_user_id_fkey FOREIGN KEY (user_id) 23 | REFERENCES public.users (id) MATCH SIMPLE 24 | ON UPDATE CASCADE 25 | ON DELETE CASCADE 26 | ) 27 | WITH ( 28 | OIDS = FALSE 29 | ) 30 | TABLESPACE pg_default; 31 | 32 | 33 | -- Index: transfers_completed_key 34 | 35 | -- DROP INDEX public.transfers_completed_key; 36 | 37 | CREATE INDEX transfers_completed_key 38 | ON public.transfers USING btree 39 | (user_id, completed_at) 40 | TABLESPACE pg_default; 41 | 42 | -- Index: transfers_torrent_id_key 43 | 44 | -- DROP INDEX public.transfers_torrent_id_key; 45 | 46 | CREATE INDEX transfers_torrent_id_key 47 | ON public.transfers USING btree 48 | (torrent_id) 49 | TABLESPACE pg_default; 50 | 51 | -- Index: transfers_user_id_key 52 | 53 | -- DROP INDEX public.transfers_user_id_key; 54 | 55 | CREATE INDEX transfers_user_id_key 56 | ON public.transfers USING btree 57 | (user_id) 58 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | use std::convert::TryFrom; 21 | 22 | pub mod chat; 23 | pub mod message; 24 | pub mod static_content; 25 | pub mod torrent; 26 | pub mod user; 27 | 28 | #[derive(Clone)] 29 | pub struct UserSubjectMsg { 30 | uid: Uuid, 31 | gid: Uuid, 32 | acl: AclContainer, 33 | } 34 | 35 | impl UserSubjectMsg { 36 | pub fn new(uid: Uuid, gid: Uuid, acl: AclContainer) -> Self { 37 | Self { uid, gid, acl } 38 | } 39 | } 40 | 41 | impl<'req> TryFrom<&'req HttpRequest> for UserSubjectMsg { 42 | type Error = Error; 43 | 44 | fn try_from(req: &HttpRequest) -> Result { 45 | let (uid, gid) = match req.credentials() { 46 | Some((u, g)) => (*u, *g), 47 | None => bail!("credentials not available"), 48 | }; 49 | Ok(Self::new(uid, gid, req.state().acl().clone())) 50 | } 51 | } 52 | 53 | impl<'a> From<&'a UserSubjectMsg> for UserSubject<'a> { 54 | fn from(msg: &UserSubjectMsg) -> UserSubject { 55 | UserSubject::new(&msg.uid, &msg.gid, &msg.acl) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /templates/torrent/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
4 | {% if error %} 5 | 8 | {% endif %} 9 |
10 |
11 |
12 |
{{torrent.name}}
13 |
14 |
15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 | Back 25 | 26 | Edit Torrent 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 | 36 | {% endblock content %} 37 | {% block title %}Delete Torrent: {{torrent.name}}{% endblock title %} -------------------------------------------------------------------------------- /migrations/2018-04-09-135835_create_users/up.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.users 2 | 3 | -- DROP TABLE public.users; 4 | 5 | CREATE TABLE public.users 6 | ( 7 | id uuid NOT NULL, 8 | name character varying COLLATE pg_catalog."default" NOT NULL, 9 | email character varying COLLATE pg_catalog."default" NOT NULL, 10 | password bytea NOT NULL, 11 | salt bytea NOT NULL, 12 | status smallint NOT NULL DEFAULT 0, 13 | created_at timestamp(6) with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | updated_at timestamp(6) with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | passcode bytea NOT NULL, 16 | uploaded bigint NOT NULL DEFAULT 0, 17 | downloaded bigint NOT NULL DEFAULT 0, 18 | group_id uuid NOT NULL, 19 | ip_address inet NULL DEFAULT NULL, 20 | CONSTRAINT users_pkey PRIMARY KEY (id), 21 | CONSTRAINT users_email_key UNIQUE (email), 22 | CONSTRAINT users_name_key UNIQUE (name), 23 | CONSTRAINT users_passcode_key UNIQUE (passcode), 24 | CONSTRAINT users_group_id_key FOREIGN KEY (group_id) 25 | REFERENCES public.groups (id) MATCH SIMPLE 26 | ON UPDATE NO ACTION 27 | ON DELETE RESTRICT 28 | ) 29 | TABLESPACE pg_default; 30 | 31 | -- Index: users_email_index 32 | 33 | -- DROP INDEX public.users_email_index; 34 | 35 | CREATE UNIQUE INDEX users_email_index 36 | ON public.users USING btree 37 | (email COLLATE pg_catalog."default") 38 | TABLESPACE pg_default; 39 | 40 | -- Index: users_name_index 41 | 42 | -- DROP INDEX public.users_name_index; 43 | 44 | CREATE UNIQUE INDEX users_name_index 45 | ON public.users USING btree 46 | (name COLLATE pg_catalog."default") 47 | TABLESPACE pg_default; 48 | 49 | -- Index: users_passcode 50 | 51 | -- DROP INDEX public.users_passcode; 52 | 53 | CREATE UNIQUE INDEX users_passcode 54 | ON public.users USING btree 55 | (passcode) 56 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /src/models/category.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! Torrent category model 20 | 21 | use super::*; 22 | use super::schema::categories; 23 | 24 | #[derive(Debug, Queryable, Insertable, AsChangeset, Identifiable, PartialEq, Serialize)] 25 | #[table_name = "categories"] 26 | pub struct Category { 27 | pub id: Uuid, 28 | pub name: String, 29 | pub created_at: Timestamp, 30 | pub updated_at: Timestamp, 31 | } 32 | 33 | impl Category { 34 | pub fn find(id: &Uuid, db: &PgConnection) -> Option { 35 | use self::categories::dsl; 36 | 37 | dsl::categories.find(id).first::(db).ok() 38 | } 39 | 40 | pub fn all(db: &PgConnection) -> Vec { 41 | use self::categories::dsl; 42 | 43 | dsl::categories 44 | .order(dsl::name.asc()) 45 | .load::(db) 46 | .unwrap_or_default() 47 | } 48 | } 49 | 50 | impl Default for Category { 51 | fn default() -> Self { 52 | Category{ 53 | id: Default::default(), 54 | name: Default::default(), 55 | created_at: Utc::now(), 56 | updated_at: Utc::now(), 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! Utility functions for ripalt 20 | 21 | pub mod rand; 22 | pub mod password; 23 | pub mod torrent; 24 | pub mod user; 25 | 26 | use data_encoding::HEXLOWER; 27 | use number_prefix::{binary_prefix, Prefixed, Standalone, PrefixNames}; 28 | use error::*; 29 | 30 | const CHARS: &[u8] = b"0123456789abcdef"; 31 | 32 | pub fn to_hex(bytes: &[u8]) -> String { 33 | let mut v = Vec::with_capacity(bytes.len() * 2); 34 | for &byte in bytes { 35 | v.push(CHARS[(byte >> 4) as usize]); 36 | v.push(CHARS[(byte & 0xf) as usize]); 37 | } 38 | 39 | unsafe { String::from_utf8_unchecked(v) } 40 | } 41 | 42 | pub fn from_hex(str: &str) -> Result> { 43 | HEXLOWER.decode(str.as_bytes()) 44 | .map_err(|e| format!("decode from hex failed: {}", e).into()) 45 | } 46 | 47 | pub fn data_size>(bytes: T) -> String { 48 | let bytes: f64 = bytes.into(); 49 | match binary_prefix(bytes) { 50 | Standalone(bytes) => format!("{} B", bytes), 51 | Prefixed(prefix, n) => format!("{:.2} {}B", n, prefix.symbol()), 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use super::*; 58 | 59 | #[test] 60 | pub fn test_to_hex() { 61 | assert_eq!(to_hex("foobar".as_bytes()), "666f6f626172"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/api/user.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! User API 20 | 21 | use super::*; 22 | use handlers::user::LoadUserStatsMsg; 23 | use models::user::UserStatsMsg; 24 | use identity::RequestIdentity; 25 | use actix_web::AsyncResponder; 26 | 27 | /// Fetch the user stats 28 | /// 29 | /// `GET /api/v1/user/stats/` 30 | /// 31 | /// # Returns 32 | /// 33 | /// If successful, `stats` returns the [**User Stats**](../../models/user/struct.UserStatsMsg.html) 34 | /// 35 | /// # Errors 36 | /// 37 | /// - `ErrorUnauthorized` if the client is not authorized. 38 | /// - `ErrorInternalServerError` 39 | /// - if the user does not exist. 40 | /// - if any error occurs when fetching the stats. 41 | pub fn stats(req: HttpRequest) -> FutureResponse { 42 | if let Some(user_id) = req.user_id() { 43 | req.state().db().send(LoadUserStatsMsg(user_id.to_owned())) 44 | .from_err() 45 | .and_then(|result: Result| { 46 | match result { 47 | Ok(stats) => { 48 | Ok(HttpResponse::Ok().json(stats)) 49 | }, 50 | Err(_) => Ok(HttpResponse::InternalServerError().into()), 51 | } 52 | }) 53 | .responder() 54 | } else { 55 | Box::new(FutErr(ErrorUnauthorized("unauthorized"))) 56 | } 57 | } -------------------------------------------------------------------------------- /src/cleanup.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | 21 | use std::sync::mpsc; 22 | use std::thread; 23 | 24 | use chrono::Duration; 25 | 26 | use db::DbExecutor; 27 | use schema::peers; 28 | 29 | const CLEANUP_INTERVAL: u64 = 60; 30 | const SLEEP_PER_LOOP: u64 = 2; 31 | 32 | pub fn cleanup(dbe: DbExecutor, rx: &mpsc::Receiver) { 33 | info!("started cleanup thread"); 34 | 35 | loop { 36 | // delete stale peers older than 60 minutes 37 | let date = Utc::now() 38 | .checked_sub_signed(Duration::minutes(60)) 39 | .unwrap(); 40 | let db: &PgConnection = &dbe.conn(); 41 | let res = diesel::delete(peers::table) 42 | .filter(peers::dsl::updated_at.lt(date)) 43 | .execute(db); 44 | 45 | match res { 46 | Ok(num) => debug!("deleted {} orphaned peers", num), 47 | Err(e) => warn!("error while cleaning orphaned peers: {}", e), 48 | } 49 | 50 | let mut count: u64 = CLEANUP_INTERVAL; 51 | while count > 0 { 52 | // try to receive from the main_rx in order to terminate 53 | if rx.try_recv().is_ok() { 54 | info!("shutting down cleanup thread"); 55 | return; 56 | } 57 | 58 | thread::sleep(std::time::Duration::from_secs(SLEEP_PER_LOOP)); 59 | count -= SLEEP_PER_LOOP; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /templates/index/authenticated.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
News
8 |
News News News
9 |
10 |
11 |
12 | 13 |
14 |
15 | {% include "index/shoutbox.html" %} 16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 |
Active users
24 |
25 |

26 | {% for gid in active_users.group_order | reverse %} 27 | {% set group = active_users.groups | get(key=gid) %} 28 | {{ group.name }}{% if not loop.last %},{% endif %} 29 | {% endfor %} 30 |

31 |

32 | {% for gid in active_users.group_order | reverse %} 33 | {% set group = active_users.groups | get(key=gid) %} 34 | {% if active_users.user_list is containing(gid) %} 35 | {% for user in active_users.user_list | get(key=gid) | sort(key="user.1") %} 36 | {{user.1}} 37 | {% endfor %} 38 | {% endif %} 39 | {% endfor %} 40 |

41 |
42 |
43 |
44 |
45 |
46 | {% endblock content %} -------------------------------------------------------------------------------- /config/ripalt.toml.example: -------------------------------------------------------------------------------- 1 | # enable debug mode 2 | # affects reloading templates at runtime 3 | debug = true 4 | # the session name / name of the session cookie 5 | session_name = "ripalt" 6 | # has no effect yet 7 | session_strict = true 8 | # secret used to sign / encrypt the session cookie 9 | session_secret = "d8664b949068b642d5e157a18a4db20925ede9a87595eebc8488830884a745e4" 10 | # secret used to sign the JWTs 11 | jwt_secret = "926e683448191865ab1e1462dca75081698ce9d3098f37bf5bdecf8da44544ec" 12 | # hostname of the tracker 13 | domain = "localhost" 14 | # use https 15 | https = false 16 | # bind the http server to this address 17 | bind = "localhost:8081" 18 | 19 | [database] 20 | # database URL: postgres://user:password@host/database 21 | url = "postgres://ripalt:ripalt@localhost/ripalt" 22 | 23 | [user] 24 | # default group for new users 25 | default_group = "0eb8ac8f-01f4-4bf9-bb0d-e3ac0ecb15f9" 26 | # number of bytes in newly generated passcodes 27 | passcode_length = 16 28 | # default timezone, offset in seconds from UTC 29 | default_timezone = 7200 30 | # default torrents per page 31 | default_torrents_per_page = 100 32 | # delete original message when replying 33 | default_delete_message_on_reply = false 34 | # store a copy of the sent message in the 'sent' folder 35 | default_save_message_in_sent = true 36 | # accept messages from: 37 | # all: everyone, except blocked users 38 | # friends: only users in the friends list 39 | # team: only the team and system messages 40 | default_accept_messages = "all" 41 | # width for user avatar images thumbnails in pixels 42 | avatar_thumbnail_width = 200 43 | 44 | [email] 45 | # enable the email system 46 | enabled = false 47 | 48 | [tracker] 49 | # accounce url, which is set in the downloaded torrents 50 | announce_url = "http://localhost:8081/tracker/announce" 51 | # comment, set in the torrents 52 | comment = "Fe₂O₃ powered tracking" 53 | # default number of peers per announce 54 | default_numwant = 50 55 | # announce interval in seconds 56 | interval = 900 57 | 58 | [torrent] 59 | # width for torrent images thumbnails in pixels 60 | image_thumbnail_width = 200 61 | # remove dead torrents after X days 62 | remove_dead_torrents_after = 30 63 | # remove dead peers after X minutes 64 | remove_dead_peers_after = 60 -------------------------------------------------------------------------------- /src/models/static_content.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | use markdown; 21 | use schema::static_content; 22 | 23 | #[derive(Queryable, Identifiable, Insertable, AsChangeset, Serialize)] 24 | #[table_name = "static_content"] 25 | pub struct Content { 26 | pub id: String, 27 | pub title: String, 28 | pub content: String, 29 | pub content_type: String, 30 | pub created_at: Timestamp, 31 | pub updated_at: Timestamp, 32 | } 33 | 34 | impl Content { 35 | pub fn new( 36 | id: String, 37 | title: String, 38 | content: String, 39 | content_type: String, 40 | created_at: Timestamp, 41 | updated_at: Timestamp, 42 | ) -> Self { 43 | Content { 44 | id, 45 | title, 46 | content, 47 | content_type, 48 | created_at, 49 | updated_at, 50 | } 51 | } 52 | 53 | pub fn find(id: &str, db: &PgConnection) -> Option { 54 | static_content::table.find(id).first::(db).ok() 55 | } 56 | 57 | pub fn render(&self) -> String { 58 | if self.content_type == "text/markdown" { 59 | return markdown::to_html(&self.content); 60 | } 61 | 62 | self.content.clone() 63 | } 64 | 65 | pub fn save(&self, db: &PgConnection) -> Result { 66 | diesel::update(self) 67 | .set(self) 68 | .execute(db) 69 | .map_err(|e| format!("failed content: {}", e).into()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /doc/sql/categories.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 10.3 6 | -- Dumped by pg_dump version 10.3 7 | 8 | -- Started on 2018-04-24 03:33:25 CEST 9 | 10 | SET statement_timeout = 0; 11 | SET lock_timeout = 0; 12 | SET idle_in_transaction_session_timeout = 0; 13 | SET client_encoding = 'UTF8'; 14 | SET standard_conforming_strings = on; 15 | SELECT pg_catalog.set_config('search_path', '', false); 16 | SET check_function_bodies = false; 17 | SET client_min_messages = warning; 18 | SET row_security = off; 19 | 20 | -- 21 | -- TOC entry 2292 (class 0 OID 19557) 22 | -- Dependencies: 199 23 | -- Data for Name: categories; Type: TABLE DATA; Schema: public; Owner: ripalt 24 | -- 25 | 26 | INSERT INTO public.categories VALUES ('05bdca1c-79b4-4f55-8cc2-6e3e4c53b4cf', 'Movies SD', NOW(), NOW()); 27 | INSERT INTO public.categories VALUES ('21c5b22e-d380-4492-b940-a9832fd9935e', 'Games PC', NOW(), NOW()); 28 | INSERT INTO public.categories VALUES ('8fc2acaa-a59c-4d1b-8955-b8b6c1c063b4', 'Applications', NOW(), NOW()); 29 | INSERT INTO public.categories VALUES ('42bb12b1-8785-4fae-88e1-a9764eefa8a4', 'Music Album', NOW(), NOW()); 30 | INSERT INTO public.categories VALUES ('5ecadd72-068c-40b9-be2a-e9134ea7b1aa', 'Music Packs', NOW(), NOW()); 31 | INSERT INTO public.categories VALUES ('aec94fa4-d01f-4162-b040-d8179b42441a', 'Movies HD', NOW(), NOW()); 32 | INSERT INTO public.categories VALUES ('80afa25f-5b5f-4c99-8d7a-976528a06b45', 'Games Console', NOW(), NOW()); 33 | INSERT INTO public.categories VALUES ('46793805-3e0e-46c3-987b-915bf0c359ab', 'Ebooks', NOW(), NOW()); 34 | INSERT INTO public.categories VALUES ('9babd4cd-3c62-465c-bb6e-3ddf577fc650', 'TV-Series SD', NOW(), NOW()); 35 | INSERT INTO public.categories VALUES ('2ea25fda-8887-4b05-a060-c72a1bb41c85', 'TV-Series HD', NOW(), NOW()); 36 | INSERT INTO public.categories VALUES ('25b42387-a008-430d-8f2b-b079704cc373', 'TV-Series Packs', NOW(), NOW()); 37 | INSERT INTO public.categories VALUES ('75583e9f-58df-4078-a979-c02edfb9482b', 'Documentation', NOW(), NOW()); 38 | INSERT INTO public.categories VALUES ('62e0fe93-309b-4d36-b14e-7ca42d87255a', 'XXX', NOW(), NOW()); 39 | 40 | 41 | -- Completed on 2018-04-24 03:33:25 CEST 42 | 43 | -- 44 | -- PostgreSQL database dump complete 45 | -- 46 | 47 | -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | 21 | use std::ops::Deref; 22 | use r2d2; 23 | use r2d2_diesel::ConnectionManager; 24 | use SETTINGS; 25 | 26 | /// An alias to the type for a pool of Diesel Postgres connections. 27 | pub type Pool = r2d2::Pool>; 28 | 29 | /// Connection request guard type: a wrapper around an r2d2 pooled connection. 30 | pub struct DbConn(pub r2d2::PooledConnection>); 31 | 32 | // For the convenience of using an &DbConn as an &PgConnection. 33 | impl Deref for DbConn { 34 | type Target = PgConnection; 35 | 36 | fn deref(&self) -> &Self::Target { 37 | &self.0 38 | } 39 | } 40 | 41 | /// Database Executor 42 | pub struct DbExecutor(pub Pool); 43 | 44 | impl DbExecutor { 45 | /// Create a new Database Executor 46 | /// 47 | /// # Panics 48 | /// 49 | /// The method panics if the connection to the database could not be established 50 | pub fn new(pool: Pool) -> Self { 51 | DbExecutor(pool) 52 | } 53 | 54 | pub fn conn(&self) -> DbConn { 55 | DbConn(self.0.get().unwrap()) 56 | } 57 | } 58 | 59 | impl Actor for DbExecutor { 60 | type Context = SyncContext; 61 | } 62 | 63 | /// Initializes a database pool. 64 | pub fn init_pool() -> Pool { 65 | let database_url = SETTINGS.read().unwrap().database.url.to_owned(); 66 | info!("starting new database pool for {}", database_url); 67 | 68 | let manager = ConnectionManager::::new(database_url); 69 | r2d2::Pool::new(manager).expect("db pool") 70 | } -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use tera; 20 | 21 | // Create the Error, ErrorKind, ResultExt, and Result types 22 | error_chain! { 23 | links { 24 | Tera(tera::Error, tera::ErrorKind); 25 | } 26 | 27 | foreign_links { 28 | Io(::std::io::Error); 29 | Fmt(::std::fmt::Error); 30 | Utf8(::std::string::FromUtf8Error); 31 | SerdeBencode(::serde_bencode::Error); 32 | ParseInt(::std::num::ParseIntError); 33 | ParseBool(::std::str::ParseBoolError); 34 | ActixWebToStr(::actix_web::http::header::ToStrError); 35 | DataEncodingDecode(::data_encoding::DecodeError); 36 | ParseUuid(::uuid::ParseError); 37 | Image(::image::ImageError); 38 | Diesel(::diesel::result::Error); 39 | } 40 | 41 | errors { 42 | SettingsPoison(t: String) { 43 | description("settings are poisoned") 44 | display("settings are poisoned: {}", t) 45 | } 46 | } 47 | } 48 | 49 | impl Into<::actix_web::Error> for Error { 50 | fn into(self) -> ::actix_web::Error { 51 | use actix_web::error; 52 | 53 | error::ErrorInternalServerError(format!("{}", self)) 54 | } 55 | } 56 | 57 | impl Into>> for Error { 58 | fn into(self) -> Box<::futures::Future> { 59 | use futures::future::err; 60 | use actix_web::error; 61 | 62 | Box::new(err(error::ErrorInternalServerError(format!("{}", self)))) 63 | } 64 | } 65 | 66 | impl From<::actix_web::error::PayloadError> for Error { 67 | fn from(e: ::actix_web::error::PayloadError) -> Error { 68 | format!("Payload error: {}", e).into() 69 | } 70 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ripalt: An Anti-Leech Torrent Tracker 2 | ===================================== 3 | 4 | ## Overview 5 | 6 | ripalt is a private **Bittorrent Tracker CMS** based on [actix-web](https://github.com/actix/actix-web) 7 | 8 | ### Features 9 | Nothing special yet. Plain old Torrent tracking. 10 | 11 | ## Requirements 12 | 13 | - Rust nightly (2018-05-10) 14 | - PostgreSQL 15 | 16 | **Additional Build Requirements** 17 | 18 | - [libsass](https://github.com/sass/libsass) 19 | 20 | ### Browser Requirements 21 | 22 | - Any Browser which supports ES6. 23 | - That means no Internet Explorer. 24 | 25 | ## Installation 26 | 27 | ### Get the source and compile it 28 | 29 | Clone the repositry 30 | ```bash 31 | git clone https://github.com/fuchsi/ripalt.git 32 | cd ripalt 33 | git submodule update --init 34 | ``` 35 | Build ripalt 36 | ```bash 37 | cargo build 38 | # or for release builds (might take a while) 39 | cargo build --release 40 | ``` 41 | 42 | ### Setup 43 | 44 | Create a `.env` file in the ripalt directory. 45 | ``` 46 | RUST_LOG="actix=warn,actix_web=info,ripalt=info" 47 | DATABASE_URL="postgres://user:password@localhost/database" 48 | ``` 49 | The file should at least contain the `DATABASE_URL`. 50 | 51 | Copy the `config/ripalt.toml.example` to `config/ripalt.toml` and change it to your needs and settings. 52 | 53 | Install [diesel_cli](https://github.com/diesel-rs/diesel/tree/master/diesel_cli) and apply the migrations 54 | ```bash 55 | cargo install diesel_cli --no-default-features --features "postgres" 56 | diesel migration run 57 | ``` 58 | 59 | Initialize the categories and groups: 60 | ```bash 61 | psql user database -f doc/sql/categories.sql 62 | psql user database -f doc/sql/groups.sql 63 | ``` 64 | Substitute `user` and `database` for your chosen settings. 65 | 66 | **Note:** At the current state there is no initialization for additional tracker data, such as 67 | - ACL 68 | - Default User(s) 69 | 70 | You'll have to create them on your own **and** update the `ripalt.toml` file for the created data. 71 | 72 | ## Usage 73 | 74 | Run ripalt 75 | ```bash 76 | cargo run ripalt 77 | # or 78 | target/debug/ripalt 79 | # or for release builds 80 | target/release/ripalt 81 | ``` 82 | and navigate your Browser to http://localhost:8081, or whatever you set in the config. 83 | 84 | ## Documentation 85 | 86 | [API Documentation](https://fuchsi.github.io/ripalt/docs/ripalt/) 87 | 88 | ## SemVer 89 | This project follows SemVer only for the public API, public API here meaning the API endpoints appearing the the docs. 90 | 91 | -------------------------------------------------------------------------------- /migrations/2018-04-09-140606_create_peers/up.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.peers 2 | 3 | -- DROP TABLE public.peers; 4 | 5 | CREATE TABLE public.peers 6 | ( 7 | id uuid NOT NULL, 8 | torrent_id uuid NOT NULL, 9 | user_id uuid NOT NULL, 10 | ip_address inet NOT NULL, 11 | port integer NOT NULL, 12 | bytes_uploaded bigint NOT NULL DEFAULT 0, 13 | bytes_downloaded bigint NOT NULL DEFAULT 0, 14 | bytes_left bigint NOT NULL DEFAULT 0, 15 | seeder boolean NOT NULL DEFAULT false, 16 | peer_id bytea NOT NULL, 17 | user_agent character varying(255) COLLATE pg_catalog."default" NOT NULL, 18 | crypto_enabled boolean NOT NULL DEFAULT false, 19 | crypto_port integer, 20 | offset_uploaded bigint NOT NULL DEFAULT 0, 21 | offset_downloaded bigint NOT NULL DEFAULT 0, 22 | created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | finished_at timestamp with time zone, 24 | updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 25 | CONSTRAINT peers_pkey PRIMARY KEY (id), 26 | CONSTRAINT peers_tup_key UNIQUE (torrent_id, user_id, peer_id), 27 | CONSTRAINT peers_torrent_id_fkey FOREIGN KEY (torrent_id) 28 | REFERENCES public.torrents (id) MATCH SIMPLE 29 | ON UPDATE CASCADE 30 | ON DELETE CASCADE, 31 | CONSTRAINT peers_user_id_fkey FOREIGN KEY (user_id) 32 | REFERENCES public.users (id) MATCH SIMPLE 33 | ON UPDATE CASCADE 34 | ON DELETE CASCADE, 35 | CONSTRAINT peers_port_check CHECK (port > 0 AND port < 65536) 36 | ) 37 | TABLESPACE pg_default; 38 | 39 | -- Index: peers_ip_address_index 40 | 41 | -- DROP INDEX public.peers_ip_address_index; 42 | 43 | CREATE INDEX peers_ip_address_index 44 | ON public.peers USING btree 45 | (ip_address) 46 | TABLESPACE pg_default; 47 | 48 | -- Index: peers_peer_id_index 49 | 50 | -- DROP INDEX public.peers_peer_id_index; 51 | 52 | CREATE INDEX peers_peer_id_index 53 | ON public.peers USING btree 54 | (peer_id) 55 | TABLESPACE pg_default; 56 | 57 | -- Index: peers_seeder_index 58 | 59 | -- DROP INDEX public.peers_seeder_index; 60 | 61 | CREATE INDEX peers_seeder_index 62 | ON public.peers USING btree 63 | (seeder) 64 | TABLESPACE pg_default; 65 | 66 | -- Index: peers_torrent_id_index 67 | 68 | -- DROP INDEX public.peers_torrent_id_index; 69 | 70 | CREATE INDEX peers_torrent_id_index 71 | ON public.peers USING btree 72 | (torrent_id) 73 | TABLESPACE pg_default; 74 | 75 | -- Index: peers_user_id_index 76 | 77 | -- DROP INDEX public.peers_user_id_index; 78 | 79 | CREATE INDEX peers_user_id_index 80 | ON public.peers USING btree 81 | (user_id) 82 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /src/app/index.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | 21 | use models::chat::ChatRoom; 22 | use models::acl::Subject; 23 | use handlers::user::ActiveUsersMsg; 24 | use chrono::Duration; 25 | use actix_web::AsyncResponder; 26 | 27 | #[derive(Serialize)] 28 | struct Chat { 29 | id: String, 30 | nid: i16, 31 | name: String, 32 | active: bool, 33 | } 34 | 35 | pub fn authenticated(mut req: HttpRequest) -> FutureResponse { 36 | let (user_id, group_id) = match session_creds(&mut req) { 37 | Some((u, g)) => (u, g), 38 | None => return async_redirect("/login"), 39 | }; 40 | let mut ctx = Context::new(); 41 | 42 | // Chat 43 | let subj = UserSubject::new(&user_id, &group_id, req.state().acl()); 44 | let mut chatrooms = vec![Chat{id: ChatRoom::Public.to_string(), nid: ChatRoom::Public.into(), name: "Shoutbox".to_string(), active: true}]; 45 | if subj.may_read(&ChatRoom::Team) { 46 | chatrooms.push(Chat{id: ChatRoom::Team.to_string(), nid: ChatRoom::Team.into(), name: "Teambox".to_string(), active: false}); 47 | } 48 | ctx.insert("chatrooms", &chatrooms); 49 | 50 | // Active Users 51 | let active_users = ActiveUsersMsg(Duration::minutes(30)); 52 | let cloned = req.clone(); 53 | req.state().db().send(active_users) 54 | .from_err() 55 | .and_then(move |result| { 56 | match result { 57 | Ok(active_users) => { 58 | ctx.insert("active_users", &active_users); 59 | }, 60 | Err(e) => { 61 | warn!("could not fetch active users: {}", e); 62 | }, 63 | } 64 | Template::render_with_user(&cloned, "index/authenticated.html", &mut ctx) 65 | }) 66 | .responder() 67 | } 68 | 69 | 70 | pub fn index(req: HttpRequest) -> SyncResponse { 71 | let ctx = Context::new(); 72 | Template::render(&req.state().template(), "index/public.html", &ctx) 73 | } 74 | -------------------------------------------------------------------------------- /migrations/2018-04-09-140244_create_torrents/up.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.torrents 2 | 3 | -- DROP TABLE public.torrents; 4 | 5 | CREATE TABLE public.torrents 6 | ( 7 | id uuid NOT NULL, 8 | name character varying(255) COLLATE pg_catalog."default" NOT NULL, 9 | info_hash bytea NOT NULL, 10 | category_id uuid NOT NULL, 11 | user_id uuid, 12 | description text COLLATE pg_catalog."default" NOT NULL, 13 | size bigint NOT NULL, 14 | visible boolean NOT NULL, 15 | completed integer NOT NULL, 16 | last_action timestamp with time zone, 17 | last_seeder timestamp with time zone, 18 | created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, 20 | CONSTRAINT torrents_pkey PRIMARY KEY (id), 21 | CONSTRAINT name_key UNIQUE (name), 22 | CONSTRAINT info_key UNIQUE (info_hash), 23 | CONSTRAINT torrents_category_id_fkey FOREIGN KEY (category_id) 24 | REFERENCES public.categories (id) MATCH SIMPLE 25 | ON UPDATE CASCADE 26 | ON DELETE RESTRICT, 27 | CONSTRAINT user_id_key FOREIGN KEY (user_id) 28 | REFERENCES public.users (id) MATCH SIMPLE 29 | ON UPDATE CASCADE 30 | ON DELETE SET NULL 31 | ) 32 | TABLESPACE pg_default; 33 | 34 | -- Index: torrents_category_index 35 | 36 | -- DROP INDEX public.torrents_category_index; 37 | 38 | CREATE INDEX torrents_category_index 39 | ON public.torrents USING btree 40 | (category_id) 41 | TABLESPACE pg_default; 42 | 43 | -- Index: torrents_created_at_index 44 | 45 | -- DROP INDEX public.torrents_created_at_index; 46 | 47 | CREATE INDEX torrents_created_at_index 48 | ON public.torrents USING btree 49 | (created_at) 50 | TABLESPACE pg_default; 51 | 52 | -- Index: torrents_info_hash_index 53 | 54 | -- DROP INDEX public.torrents_info_hash_index; 55 | 56 | CREATE UNIQUE INDEX torrents_info_hash_index 57 | ON public.torrents USING btree 58 | (info_hash) 59 | TABLESPACE pg_default; 60 | 61 | -- Index: torrents_name_index 62 | 63 | -- DROP INDEX public.torrents_name_index; 64 | 65 | CREATE UNIQUE INDEX torrents_name_index 66 | ON public.torrents USING btree 67 | (name COLLATE pg_catalog."default") 68 | TABLESPACE pg_default; 69 | 70 | -- Index: torrents_size_index 71 | 72 | -- DROP INDEX public.torrents_size_index; 73 | 74 | CREATE INDEX torrents_size_index 75 | ON public.torrents USING btree 76 | (size) 77 | TABLESPACE pg_default; 78 | 79 | -- Index: torrents_user_id_index 80 | 81 | -- DROP INDEX public.torrents_user_id_index; 82 | 83 | CREATE INDEX torrents_user_id_index 84 | ON public.torrents USING btree 85 | (user_id) 86 | TABLESPACE pg_default; 87 | 88 | -- Index: torrents_visible_index 89 | 90 | -- DROP INDEX public.torrents_visible_index; 91 | 92 | CREATE INDEX torrents_visible_index 93 | ON public.torrents USING btree 94 | (visible) 95 | TABLESPACE pg_default; -------------------------------------------------------------------------------- /src/app/login.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | use handlers::user::LoginForm; 21 | use actix_web::AsyncResponder; 22 | use actix_web::HttpMessage; 23 | 24 | pub fn login(req: HttpRequest) -> SyncResponse { 25 | let mut ctx = Context::new(); 26 | ctx.insert("username", ""); 27 | ctx.insert("error", ""); 28 | Template::render(&req.state().template(), "login/login.html", &ctx) 29 | } 30 | 31 | pub fn take_login(req: HttpRequest) -> FutureResponse { 32 | let cloned = req.clone(); 33 | let form = match cloned.urlencoded::().wait() { 34 | Ok(form) => form, 35 | Err(e) => return Box::new(FutErr(ErrorInternalServerError(format!("{}", e)))) 36 | }; 37 | 38 | let cloned = req.clone(); 39 | cloned.state() 40 | .db() 41 | .send(form.clone()) 42 | .from_err() 43 | .and_then(move |r: Result| { 44 | let mut ctx = Context::new(); 45 | let mut fail = true; 46 | 47 | match r { 48 | Ok(user) => { 49 | match req.session().set("user_id", user.id) { 50 | Ok(_) => {}, 51 | Err(e) => return Err(ErrorInternalServerError(format!("{}", e))), 52 | }; 53 | match req.session().set("group_id", user.group_id) { 54 | Ok(_) => {}, 55 | Err(e) => return Err(ErrorInternalServerError(format!("{}", e))), 56 | }; 57 | fail = false; 58 | }, 59 | Err(e) => { 60 | ctx.insert("error", &format!("{}", e)); 61 | ctx.insert("username", &form.username); 62 | } 63 | } 64 | 65 | if fail { 66 | let tpl = req.state().template(); 67 | Template::render(&tpl, "login/login.html", &ctx) 68 | } else { 69 | Ok(redirect("/")) 70 | } 71 | }) 72 | .responder() 73 | } 74 | 75 | pub fn logout(req: HttpRequest) -> SyncResponse { 76 | req.session().clear(); 77 | let t: Vec<&str> = vec![]; 78 | let url = req.url_for("index", &t).unwrap(); 79 | sync_redirect(&url.to_string()) 80 | } 81 | -------------------------------------------------------------------------------- /templates/static_content/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
Edit {{content.title}}
8 |
9 |
10 | 11 |
12 |
13 | 14 | 15 |
16 |
17 |
18 |
19 | 20 | 23 |
24 |
25 |
26 |
27 | 28 | 29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 59 | {% endblock content %} 60 | {% block title %}Edit {{content.title}}{% endblock title %} -------------------------------------------------------------------------------- /templates/login/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_public.html" %} 2 | {% block title %}Sign In{% endblock title %} 3 | {% block content %} 4 |
5 |
6 |
Sign in
7 |
8 | {% if error %} 9 |

{{error}}

10 | {% endif %} 11 |
12 |
13 | 14 |
15 | 16 |
17 | Please choose a Username. 18 |
19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 | Please choose a password. 27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 | 39 |
40 | 41 | 61 | {% endblock content %} -------------------------------------------------------------------------------- /src/handlers/static_content.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | use models::static_content::Content; 21 | use models::acl::Subject; 22 | 23 | pub struct LoadStaticContentMsg { 24 | id: String, 25 | } 26 | 27 | impl LoadStaticContentMsg { 28 | pub fn new(id: String) -> Self { 29 | Self { id } 30 | } 31 | } 32 | 33 | impl Message for LoadStaticContentMsg { 34 | type Result = Result; 35 | } 36 | 37 | impl Handler for DbExecutor { 38 | type Result = Result; 39 | 40 | fn handle( 41 | &mut self, 42 | msg: LoadStaticContentMsg, 43 | _ctx: &mut Self::Context, 44 | ) -> >::Result { 45 | let conn = self.conn(); 46 | 47 | Content::find(&msg.id, &conn).ok_or_else(|| "failed to load content".into()) 48 | } 49 | } 50 | 51 | #[derive(Default)] 52 | pub struct UpdateStaticContentMsg { 53 | id: String, 54 | title: String, 55 | content: String, 56 | content_type: String, 57 | subj: Option, 58 | } 59 | 60 | impl UpdateStaticContentMsg { 61 | pub fn new(id: String, title: String, content: String, content_type: String) -> Self { 62 | Self { 63 | id, 64 | title, 65 | content, 66 | content_type, 67 | .. Default::default() 68 | } 69 | } 70 | 71 | pub fn set_acl(&mut self, subj: UserSubjectMsg) { 72 | self.subj = Some(subj); 73 | } 74 | } 75 | 76 | impl Message for UpdateStaticContentMsg { 77 | type Result = Result; 78 | } 79 | 80 | impl Handler for DbExecutor { 81 | type Result = Result; 82 | 83 | fn handle( 84 | &mut self, 85 | mut msg: UpdateStaticContentMsg, 86 | _ctx: &mut Self::Context, 87 | ) -> >::Result { 88 | let conn = self.conn(); 89 | 90 | let mut content = Content::find(&msg.id, &conn).ok_or_else(|| -> Error { "failed to load content".into() })?; 91 | let subj_msg = msg.subj.take().ok_or_else(|| -> Error { "no acl message".into() } )?; 92 | let subj = UserSubject::from(&subj_msg); 93 | if !subj.may_write(&content) { 94 | bail!("not allowed"); 95 | } 96 | content.title = msg.title; 97 | content.content = msg.content; 98 | content.content_type = msg.content_type; 99 | content.updated_at = Utc::now(); 100 | content.save(&conn)?; 101 | 102 | Ok(content) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | 21 | use std::env; 22 | use config::{ConfigError, Config, File, Environment}; 23 | 24 | #[derive(Debug, Deserialize)] 25 | pub struct Database { 26 | pub url: String, 27 | } 28 | 29 | #[derive(Debug, Deserialize)] 30 | pub struct User { 31 | pub default_group: Uuid, 32 | pub passcode_length: usize, 33 | pub default_timezone: i32, 34 | pub default_torrents_per_page: i64, 35 | pub default_delete_message_on_reply: bool, 36 | pub default_save_message_in_sent: bool, 37 | pub default_accept_messages: String, 38 | pub avatar_thumbnail_width: u32, 39 | } 40 | 41 | #[derive(Debug, Deserialize)] 42 | pub struct Email { 43 | pub enabled: bool, 44 | } 45 | 46 | #[derive(Debug, Deserialize)] 47 | pub struct Tracker { 48 | pub announce_url: String, 49 | pub comment: String, 50 | pub default_numwant: u16, 51 | pub interval: u16, 52 | } 53 | 54 | #[derive(Debug, Deserialize)] 55 | pub struct Torrent { 56 | pub image_thumbnail_width: u32, 57 | } 58 | 59 | #[derive(Debug, Deserialize)] 60 | pub struct Settings { 61 | pub debug: bool, 62 | pub jwt_secret: String, 63 | pub session_name: String, 64 | pub session_secret: String, 65 | pub session_strict: bool, 66 | pub domain: String, 67 | pub https: bool, 68 | pub bind: String, 69 | pub database: Database, 70 | pub user: User, 71 | pub email: Email, 72 | pub tracker: Tracker, 73 | pub torrent: Torrent, 74 | } 75 | 76 | impl Settings { 77 | pub fn new() -> std::result::Result { 78 | let mut s = Config::new(); 79 | 80 | // Start off by merging in the "default" configuration file 81 | s.merge(File::with_name("config/ripalt"))?; 82 | 83 | // Add in the current environment file 84 | // Default to 'development' env 85 | // Note that this file is _optional_ 86 | let env = env::var("RUN_MODE").unwrap_or_else(|_|"development".into()); 87 | s.merge(File::with_name(&format!("config/{}", env)).required(false))?; 88 | 89 | // Add in a local configuration file 90 | // This file shouldn't be checked in to git 91 | s.merge(File::with_name("config/local").required(false))?; 92 | 93 | // Add in settings from the environment (with a prefix of APP) 94 | // Eg.. `APP_DEBUG=1 ./target/app` would set the `debug` key 95 | s.merge(Environment::with_prefix("app"))?; 96 | 97 | // You can deserialize (and thus freeze) the entire configuration as 98 | s.try_into() 99 | } 100 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 4 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | 8 | ### Added 9 | - Use the NFO as description if the description is empty 10 | - Message API 11 | - `GET /api/v1/message/messages` get all messages in a folder. 12 | - `GET /api/v1/message/unread` get all unread messages in a folder. 13 | - `GET /api/v1/message/read` get / read a message. 14 | - `POST /api/v1/message/mark_read` mark messages as read. 15 | - `POST /api/v1/message/delete` delete messages. 16 | - `POST /api/v1/message/send` send a new message. 17 | - Message Frontend 18 | - FAQ / Rules and other _static_ content. 19 | - User Settings and Profile 20 | - Torrent Comment API 21 | - `GET /api/v1/comment/torrent` get all comments for a torrent. 22 | - `GET /api/v/comment/get` get a single comment. 23 | - `POST /api/v1/comment/new` publish a new comment. 24 | - `POST /api/v1/comment/edit` edit a comment. 25 | - `POST /api/v1/comment/delete` delete a comment. 26 | - Torrent Comment Frontend 27 | 28 | 29 | ### Changed 30 | - `Template::render()` now returns `HttpResponse` instead of `Template` 31 | - The `format_date` Helper now appends 'UTC' if no specific timezone is provided. 32 | 33 | ## [0.2.0] - 2018-04-30 34 | 35 | ### Added 36 | - Cleanup thread to remove orphaned peers. 37 | - Identity Provider for the API, which uses either the Session ID or a JWT. 38 | - New Setting: `jwt_secret`, the secret key for the JWTs. 39 | - API endpoint to get the own stats (/api/v1/user/stats) 40 | - User Profiles. 41 | - Download NFOs. 42 | - Edit and Delete Torrents. 43 | - Support for user defined timezone and torrents per page settings. 44 | - Own Identity Middleware, more or less a copy of the original one. 45 | - API endpoints for the chat: 46 | - `GET /api/v1/chat/messages` to fetch messages. 47 | - `POST /api/v1/chat/publish` to publish a new message. 48 | - A simple Chat (Shoutbox) with 2 Chatrooms (public and team) 49 | - Show active(last active within 30m) users on the index page. 50 | - Add Images to Torrent uploads. 51 | 52 | ### Changed 53 | - User Stats accounting now collects the time seeded 54 | - Usernames may now only contain letters, numbers, _ and -. And they must begin with a letter and have to be at least 4 characters long. 55 | - Passwords must now be at least 8 characters long. 56 | - Assets (for now just the stylesheets) are now generated at the build process and no longer shipped precompiled. 57 | - A seperate compile script, or something, may be added later. 58 | 59 | ### Fixed 60 | - Uploaded torrents without a specific name, now have the `.torrent` extension removed. 61 | - Custom File Input fields now set the name of the selected file as label. 62 | - Fixed wrong accounting for uploaded data, due to a typo. 63 | - Downloaded torrents now have the `.torrent` suffix appended. 64 | 65 | ### Removed 66 | - bip_bencode in favour for serde_bencode. 67 | 68 | ## [0.1.0] - 2018-04-20 69 | 70 | ### Added 71 | 72 | - Web Portal: minimal functionality 73 | - User Sign Up and Sign In 74 | - Upload Torrents 75 | - Browse / Search Torrents 76 | - View Torrent details 77 | - Download Torrents 78 | - A minimal ACL system 79 | - Tracker 80 | - Tracking Torrents (/tracker/announce/...) with user/torrent stats accounting 81 | - Scraping 82 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! API Handlers 20 | 21 | use super::*; 22 | 23 | use identity::{ApiIdentityPolicy, IdentityService}; 24 | 25 | pub mod chat; 26 | pub mod comment; 27 | pub mod message; 28 | pub mod user; 29 | 30 | #[derive(Serialize)] 31 | #[doc(hidden)] 32 | pub struct JsonErr { 33 | pub error: String, 34 | } 35 | 36 | 37 | pub(crate) fn build(db: Addr, acl: Arc>) -> App { 38 | let settings = SETTINGS.read().unwrap(); 39 | let jwt_secret = util::from_hex(&settings.jwt_secret).unwrap(); 40 | let session_secret = util::from_hex(&settings.session_secret).unwrap(); 41 | let session_name = &settings.session_name[..]; 42 | let session_secure = &settings.https; 43 | let listen = format!( 44 | "http{}://{}", 45 | if settings.https { "s" } else { "" }, 46 | settings.bind 47 | ); 48 | let domain = format!( 49 | "http{}://{}", 50 | if settings.https { "s" } else { "" }, 51 | settings.domain 52 | ); 53 | 54 | App::with_state(State::new(db, acl)) 55 | .middleware(Logger::default()) 56 | .middleware(DefaultHeaders::new().header("X-Version", env!("CARGO_PKG_VERSION"))) 57 | .middleware( 58 | csrf::CsrfFilter::new() 59 | .allow_xhr() 60 | .allowed_origin(listen) 61 | .allowed_origin(domain), 62 | ) 63 | .middleware(SessionStorage::new( 64 | CookieSessionBackend::signed(&session_secret) 65 | .name(session_name) 66 | .secure(*session_secure), 67 | )) 68 | .middleware(IdentityService::new(ApiIdentityPolicy::new( 69 | &jwt_secret, 70 | ))) 71 | .prefix("/api/v1") 72 | .scope("/user", |scope| { 73 | scope.route("/stats", Method::GET, user::stats) 74 | }) 75 | .scope("/chat", |scope| { 76 | scope.route("/messages", Method::GET, chat::messages) 77 | .resource("/publish", |r| r.method(Method::POST).with2(chat::publish)) 78 | }) 79 | .scope("/message", |scope| { 80 | scope.route("/messages", Method::GET, message::messages) 81 | .route("/unread", Method::GET, message::unread) 82 | .route("/read", Method::GET, message::message) 83 | .resource("/send", |r| r.method(Method::POST).with2(message::send)) 84 | .resource("/delete", |r| r.method(Method::POST).with2(message::delete)) 85 | .resource("/mark_read", |r| r.method(Method::POST).with2(message::mark_read)) 86 | }) 87 | .scope("/comment", |scope| { 88 | scope.route("/torrent", Method::GET, comment::torrent) 89 | .route("/get", Method::GET, comment::comment) 90 | .resource("/new", |r| r.method(Method::POST).with2(comment::new)) 91 | .resource("/edit", |r| r.method(Method::POST).with2(comment::edit)) 92 | .resource("/delete", |r| r.method(Method::POST).with2(comment::delete)) 93 | }) 94 | .default_resource(|r| r.method(Method::GET).h(NormalizePath::default())) 95 | } 96 | -------------------------------------------------------------------------------- /templates/index/shoutbox.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 13 |
14 |
15 |
16 | {% for chatroom in chatrooms | default(value=[]) %} 17 |
18 |
19 |
    20 |
    21 |
    22 | 23 |
    24 | 25 |
    26 | 27 |
    28 |
    29 | 30 |
    31 |
    32 | {% endfor %} 33 |
    34 |
    35 |
    36 | 76 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | 21 | use std::sync::{Arc, RwLock, RwLockReadGuard}; 22 | 23 | use db::{DbConn, DbExecutor, Pool}; 24 | use models::acl::Acl; 25 | use template::TemplateContainer; 26 | use template::TemplateSystem; 27 | 28 | #[derive(Clone)] 29 | pub struct AclContainer { 30 | inner: Arc> 31 | } 32 | 33 | impl AclContainer { 34 | pub fn new(acl: Arc>) -> Self { 35 | Self{inner: acl} 36 | } 37 | 38 | pub fn new_empty() -> Self { 39 | Self{inner: Arc::new(RwLock::new(Acl::new()))} 40 | } 41 | 42 | pub fn set_acl(&mut self, acl: Arc>) { 43 | self.inner = acl; 44 | } 45 | 46 | /// Reload the ACL structure from the database 47 | /// 48 | /// # Panics 49 | /// 50 | /// This function panics if the underlying database shits the bed. 51 | #[allow(dead_code)] 52 | pub fn reload(&mut self, db: &PgConnection) { 53 | self.inner.write().unwrap().reload(db) 54 | } 55 | 56 | /// Check if the `user` is allowed to do `perm` in namespace `ns` 57 | /// 58 | /// `user` - the user to test 59 | /// 60 | /// `ns` - acl namespace 61 | /// 62 | /// `perm` - permission to test 63 | pub fn is_allowed(&self, uid: &Uuid, gid: &Uuid, ns: &str, perm: &Permission) -> bool { 64 | self.inner.read().unwrap().is_allowed(uid, gid, ns, perm) 65 | } 66 | 67 | /// Check if the `group` is allowed to do `perm` in namespace `ns` 68 | /// 69 | /// if no explicit rule for the group is found check the parent group(s) recursively until an 70 | /// explicit rule is found. 71 | /// If no rule is found the function returns `false` 72 | pub fn is_group_allowed(&self, gid: &Uuid, ns: &str, perm: &Permission) -> bool { 73 | self.inner.read().unwrap().is_group_allowed(gid, ns, perm) 74 | } 75 | } 76 | 77 | /// State represents the shared state for the application 78 | pub struct State { 79 | db: Addr, 80 | acl: AclContainer, 81 | template: Option, 82 | } 83 | 84 | impl State { 85 | /// Create a new State object 86 | pub fn new(db: Addr, acl: Arc>) -> Self { 87 | State { 88 | db, 89 | acl: AclContainer::new(acl), 90 | template: None, 91 | } 92 | } 93 | 94 | /// Set the template object 95 | pub fn set_template(&mut self, template: TemplateContainer) { 96 | self.template = Some(template); 97 | } 98 | 99 | /// Get the database object 100 | pub fn db(&self) -> &Addr { 101 | &self.db 102 | } 103 | 104 | /// Get the ACL object 105 | pub fn acl(&self) -> &AclContainer { 106 | &self.acl 107 | } 108 | 109 | /// Get the Template object 110 | pub fn template(&self) -> RwLockReadGuard { 111 | match &self.template { 112 | Some(template) => template.read().unwrap(), 113 | None => panic!("template system not initialized"), 114 | } 115 | } 116 | } 117 | 118 | pub fn init_acl(pool: &Pool) -> Arc> { 119 | let mut acl = Acl::new(); 120 | acl.load(&DbConn(pool.get().unwrap())); 121 | 122 | Arc::new(RwLock::new(acl)) 123 | } -------------------------------------------------------------------------------- /src/tracker/scrape.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | 21 | use std::convert::TryFrom; 22 | 23 | use url::percent_encoding::percent_decode; 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct ScrapeRequest { 27 | info_hashes: Vec>, 28 | } 29 | 30 | impl TryFrom> for ScrapeRequest { 31 | type Error = Error; 32 | 33 | fn try_from(req: HttpRequest) -> Result { 34 | trace!("Request: {:#?}", req); 35 | trace!("uri: {:#?}", req.uri()); 36 | let query_str = req.uri().query().ok_or_else(|| "no query")?; 37 | trace!("query: {:#?}", query_str); 38 | let iter = query_str.split('&'); 39 | let mut info_hashes_str: Vec<&str> = Vec::new(); 40 | for part in iter { 41 | if let Some(pos) = part.find('=') { 42 | let key = &part[0..pos]; 43 | if key == "info_hash" { 44 | let value = &part[pos + 1..]; 45 | info_hashes_str.push(value); 46 | } 47 | } 48 | } 49 | trace!("info_hashes: {:#?}", info_hashes_str); 50 | let mut info_hashes: Vec> = Vec::new(); 51 | for info_hash in info_hashes_str { 52 | let info_hash = percent_decode(info_hash.as_bytes()).if_any(); 53 | if let Some(info_hash) = info_hash { 54 | info_hashes.push(info_hash); 55 | } 56 | } 57 | 58 | if info_hashes.is_empty() { 59 | bail!("no info hashes provided"); 60 | } 61 | 62 | Ok(ScrapeRequest{info_hashes}) 63 | } 64 | } 65 | 66 | impl Message for ScrapeRequest { 67 | type Result = Result; 68 | } 69 | 70 | impl Handler for DbExecutor { 71 | type Result = Result; 72 | 73 | fn handle(&mut self, msg: ScrapeRequest, _ctx: &mut Self::Context) -> >::Result { 74 | let conn = self.conn(); 75 | let mut files: Vec = Vec::new(); 76 | 77 | for info_hash in msg.info_hashes { 78 | let (complete, incomplete, downloaded) = models::TorrentList::peer_count_scrape(&info_hash, &conn); 79 | files.push(ScrapeFile{info_hash, complete, incomplete, downloaded}); 80 | } 81 | 82 | Ok(ScrapeResponse{files}) 83 | } 84 | } 85 | 86 | #[derive(Debug)] 87 | pub struct ScrapeResponse { 88 | pub files: Vec, 89 | } 90 | 91 | #[derive(Debug)] 92 | pub struct ScrapeFile { 93 | pub info_hash: Vec, 94 | pub complete: i64, 95 | pub incomplete: i64, 96 | pub downloaded: i32, 97 | } 98 | 99 | impl Serialize for ScrapeFile { 100 | fn serialize(&self, s: S) -> std::result::Result 101 | where 102 | S: Serializer, 103 | { 104 | let mut root = s.serialize_map(Some(3))?; 105 | root.serialize_entry("complete", &self.complete)?; 106 | root.serialize_entry("downloaded", &self.downloaded)?; 107 | root.serialize_entry("incomplete", &self.incomplete)?; 108 | root.end() 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | 116 | #[test] 117 | fn serialize_scrape_file() { 118 | let file = ScrapeFile{ 119 | info_hash: [0u8; 20].to_vec(), 120 | complete: 111, 121 | incomplete: 222, 122 | downloaded: 333, 123 | }; 124 | 125 | let expected = "d8:completei111e10:downloadedi333e10:incompletei222ee"; 126 | let actual = serde_bencode::to_string(&file).unwrap(); 127 | assert_eq!(expected, actual); 128 | } 129 | } -------------------------------------------------------------------------------- /src/models/peer.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! Peer Model 20 | 21 | use super::schema::*; 22 | use super::*; 23 | use super::{torrent::Torrent, user::User}; 24 | use ipnetwork::IpNetwork; 25 | 26 | #[derive(Debug, Queryable, Insertable, AsChangeset, Identifiable, Associations)] 27 | #[table_name = "peers"] 28 | #[primary_key(id)] 29 | #[belongs_to(Torrent)] 30 | #[belongs_to(User)] 31 | pub struct Peer { 32 | pub id: Uuid, 33 | pub torrent_id: Uuid, 34 | pub user_id: Uuid, 35 | pub ip_address: IpNetwork, 36 | pub port: i32, 37 | pub bytes_uploaded: i64, 38 | pub bytes_downloaded: i64, 39 | pub bytes_left: i64, 40 | pub seeder: bool, 41 | pub peer_id: Bytes, 42 | pub user_agent: String, 43 | pub crypto_enabled: bool, 44 | pub crypto_port: Option, 45 | pub offset_uploaded: i64, 46 | pub offset_downloaded: i64, 47 | pub created_at: Timestamp, 48 | pub finished_at: Option, 49 | pub updated_at: Timestamp, 50 | } 51 | 52 | impl Peer { 53 | pub fn find(id: &Uuid, db: &PgConnection) -> Option { 54 | use schema::peers::dsl; 55 | dsl::peers.find(id).first::(db).ok() 56 | } 57 | 58 | /// Returns all peers for a torrent 59 | /// 60 | /// Return value is a Vector of a Tuple (Peer, UserName) 61 | pub fn find_for_torrent(torrent_id: &Uuid, db: &PgConnection) -> Vec<(Self, String)> { 62 | use schema::peers::dsl; 63 | dsl::peers 64 | .filter(dsl::torrent_id.eq(torrent_id)) 65 | .order(dsl::updated_at.desc()) 66 | .load::(db) 67 | .unwrap_or_else(|_| vec![]) 68 | .into_iter() 69 | .map(|peer: Peer| { 70 | let user_name = peer.user_name(db); 71 | (peer, user_name) 72 | }) 73 | .collect() 74 | } 75 | 76 | pub fn seeder_for_torrent(torrent_id: &Uuid, limit: i64, db: &PgConnection) -> Vec { 77 | Self::peers_for_torrent(torrent_id, true, limit, db) 78 | } 79 | 80 | pub fn leecher_for_torrent(torrent_id: &Uuid, limit: i64, db: &PgConnection) -> Vec { 81 | Self::peers_for_torrent(torrent_id, false, limit, db) 82 | } 83 | 84 | pub fn peers_for_torrent(torrent_id: &Uuid, seeder: bool, limit: i64, db: &PgConnection) -> Vec { 85 | use schema::peers::dsl; 86 | dsl::peers 87 | .filter(dsl::torrent_id.eq(torrent_id)) 88 | .filter(dsl::seeder.eq(seeder)) 89 | .order(dsl::updated_at.desc()) 90 | .limit(limit) 91 | .load::(db) 92 | .unwrap_or_else(|_| Vec::new()) 93 | } 94 | 95 | pub fn find_for_announce(torrent_id: &Uuid, user_id: &Uuid, peer_id: &[u8], db: &PgConnection) -> Option { 96 | use schema::peers::dsl; 97 | dsl::peers 98 | .filter(dsl::torrent_id.eq(torrent_id)) 99 | .filter(dsl::user_id.eq(user_id)) 100 | .filter(dsl::peer_id.eq(peer_id)) 101 | .first::(db) 102 | .ok() 103 | } 104 | 105 | pub fn save(&self, db: &PgConnection) -> Result { 106 | let query = diesel::insert_into(peers::table) 107 | .values(self) 108 | .on_conflict(on_constraint("peers_tup_key")) 109 | .do_update() 110 | .set(self); 111 | trace!("query: {}", diesel::debug_query::(&query)); 112 | query.execute(db) 113 | .chain_err(|| "peer update failed") 114 | } 115 | 116 | pub fn delete(&self, db: &PgConnection) -> Result { 117 | diesel::delete(self) 118 | .execute(db) 119 | .chain_err(|| "peer delete failed") 120 | } 121 | } 122 | 123 | impl HasUser for Peer { 124 | fn user_id(&self) -> &Uuid { 125 | &self.user_id 126 | } 127 | } 128 | 129 | 130 | -------------------------------------------------------------------------------- /templates/message/show.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
    4 |
    5 |
    6 | 7 |
    8 |
    {{message.subject}}
    9 |
    10 |
    11 | {% if message.folder_name != "sent" %} 12 |
    From:
    13 |
    {{message.sender_name}}
    14 | {% else %} 15 |
    To:
    16 |
    {{message.receiver_name}}
    17 | {% endif %} 18 |
    Date:
    19 |
    {{message.created_at | format_date }}
    20 |
    Subject:
    21 |
    {{ message.subject }}
    22 |
    23 |
    24 | {{ message.body | safe | markdown }} 25 |
    26 | 27 |
    28 | Back 29 | {% if message.folder_name == "inbox" %} 30 | Reply 31 | {% endif %} 32 | {% if not message.is_read %} 33 | 34 | {% endif %} 35 | 36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 | Inbox 43 | Sent 44 | System Messages 45 | New Message 46 |
    47 |
    48 |
    49 |
    50 | 83 | {% endblock %} 84 | {% block title %}Message: {{message.subject}}{% endblock title %} -------------------------------------------------------------------------------- /src/handlers/chat.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! Chat Handlers 20 | 21 | use super::*; 22 | 23 | use actix_web::FromRequest; 24 | use identity::RequestIdentity; 25 | use models::chat::{ChatMessageWithUser, ChatRoom, NewChatMessage}; 26 | use models::acl::Subject; 27 | 28 | /// Loads chat messages from the database backend 29 | pub struct LoadChatMessagesMsg { 30 | chat: ChatRoom, 31 | since: Option>, 32 | limit: i64, 33 | user: UserSubjectMsg, 34 | } 35 | 36 | impl LoadChatMessagesMsg { 37 | /// Construct a new `LoadChatMessagesMsg` instance 38 | pub fn new(chat: ChatRoom, since: Option>, limit: i64, user: UserSubjectMsg) -> Self { 39 | Self{chat, since, limit, user} 40 | } 41 | } 42 | 43 | impl<'req> TryFrom<&'req HttpRequest> for LoadChatMessagesMsg { 44 | type Error = Error; 45 | 46 | fn try_from(req: &HttpRequest) -> Result { 47 | let query = Query::>::extract(&req) 48 | .map_err(|e| format!("failed to extract query: {}", e))?; 49 | 50 | let default_chat = "1".to_string(); 51 | let default_limit = "50".to_string(); 52 | let chat: i16 = query.get("chat").unwrap_or_else(|| &default_chat).parse()?; 53 | let chat = ChatRoom::try_from(chat)?; 54 | let since = match query.get("since") { 55 | Some(s) => { 56 | let ts: i64 = s.parse()?; 57 | Some(Utc.timestamp(ts, 0)) 58 | }, 59 | None => None, 60 | }; 61 | 62 | let limit: i64 = query.get("limit").unwrap_or_else(|| &default_limit).parse()?; 63 | let mut identity = req.credentials(); 64 | let (user_id, group_id) = identity.take().unwrap(); 65 | let user = UserSubjectMsg::new(*user_id, *group_id, req.state().acl().clone()); 66 | 67 | Ok(LoadChatMessagesMsg::new(chat, since, limit, user)) 68 | } 69 | } 70 | 71 | impl Message for LoadChatMessagesMsg { 72 | type Result = Result>; 73 | } 74 | 75 | impl Handler for DbExecutor { 76 | type Result = Result>; 77 | 78 | fn handle(&mut self, msg: LoadChatMessagesMsg, _: &mut Self::Context) -> >::Result { 79 | let conn = self.conn(); 80 | let subj = UserSubject::from(&msg.user); 81 | if !subj.may_read(&msg.chat) { 82 | bail!("not allowed"); 83 | } 84 | 85 | Ok(ChatMessageWithUser::messages_for_chat(msg.chat.into(), msg.since, msg.limit, &conn)) 86 | } 87 | } 88 | 89 | /// Publishes a new chat message. 90 | pub struct PublishChatMessagesMsg { 91 | chat: ChatRoom, 92 | message: String, 93 | user: UserSubjectMsg, 94 | } 95 | 96 | impl PublishChatMessagesMsg { 97 | /// Construct a new `PublishChatMessagesMsg` instance 98 | pub fn new>(chat: T, message: String, user: UserSubjectMsg) -> Self { 99 | Self{ 100 | chat: chat.into(), 101 | message, 102 | user, 103 | } 104 | } 105 | } 106 | 107 | impl Message for PublishChatMessagesMsg { 108 | type Result = Result; 109 | } 110 | 111 | impl Handler for DbExecutor { 112 | type Result = Result; 113 | 114 | fn handle(&mut self, msg: PublishChatMessagesMsg, _: &mut Self::Context) -> >::Result { 115 | let conn = self.conn(); 116 | let subj = UserSubject::from(&msg.user); 117 | if !subj.may_write(&msg.chat) { 118 | bail!("not allowed"); 119 | } 120 | 121 | let chat: i16 = msg.chat.into(); 122 | let new_message = NewChatMessage::new(&msg.user.uid, &chat, &msg.message); 123 | let mut message = ChatMessageWithUser::from(new_message.save(&conn)?); 124 | message.user_name = models::username(&message.user_id, &conn).unwrap(); 125 | message.user_group = *subj.group_id(); 126 | Ok(message) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /templates/message/new.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
    4 |
    5 |
    6 | 7 |
    8 |
    New Message
    9 |
    10 |
    11 | {% if message is defined %} 12 | 13 | {% endif %} 14 |
    15 |
    16 | 17 | 18 |
    19 |
    20 |
    21 |
    22 | 23 | 24 |
    25 |
    26 |
    27 |
    28 | 29 | 34 |
    35 |
    36 |
    37 |
    38 |
    39 | {% if message is defined %} 40 | Back 41 | {% endif %} 42 | 43 |
    44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    50 |
    51 |
    52 | Inbox 53 | Sent 54 | System Messages 55 | New Message 56 |
    57 |
    58 |
    59 |
    60 | 92 | {% endblock %} 93 | {% block title %}New Message{% endblock title %} -------------------------------------------------------------------------------- /src/app/message.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | use actix_web::AsyncResponder; 21 | use actix_web::FromRequest; 22 | use handlers::message::{LoadMessageMsg, LoadMessagesMsg}; 23 | 24 | #[derive(Deserialize)] 25 | pub struct NewMessage { 26 | pub receiver: Uuid, 27 | pub subject: String, 28 | pub body: String, 29 | pub reply_to: Option, 30 | } 31 | 32 | pub fn messages(mut req: HttpRequest) -> FutureResponse { 33 | let user_id = match session_creds(&mut req) { 34 | Some((u, _)) => u, 35 | None => return async_redirect("/login"), 36 | }; 37 | 38 | let folder = req.match_info().get("folder").unwrap_or_else(|| "inbox").to_string(); 39 | let loadmsg = LoadMessagesMsg::new(folder.clone(), false, user_id); 40 | 41 | req.clone().state() 42 | .db() 43 | .send(loadmsg) 44 | .from_err() 45 | .and_then(move |result| match result { 46 | Ok(messages) => { 47 | let mut ctx = Context::new(); 48 | ctx.insert("messages", &messages); 49 | ctx.insert("folder", &folder); 50 | Template::render_with_user(&req, "message/list.html", &mut ctx) 51 | }, 52 | Err(e) => Ok(HttpResponse::InternalServerError().body(e.to_string())), 53 | }) 54 | .responder() 55 | } 56 | 57 | pub fn message(mut req: HttpRequest) -> FutureResponse { 58 | let user_id = match session_creds(&mut req) { 59 | Some((u, _)) => u, 60 | None => return async_redirect("/login"), 61 | }; 62 | 63 | let id = { 64 | let mut id = req.match_info().get("id"); 65 | if id.is_none() { 66 | return Box::new(FutErr(ErrorNotFound("no message id"))); 67 | } 68 | let id = Uuid::parse_str(id.take().unwrap()); 69 | if id.is_err() { 70 | return Box::new(FutErr(ErrorNotFound("invalid message id"))); 71 | } 72 | id.unwrap() 73 | }; 74 | let mut loadmsg = LoadMessageMsg::new(id, user_id); 75 | loadmsg.mark_as_read(true); 76 | 77 | req.clone().state() 78 | .db() 79 | .send(loadmsg) 80 | .from_err() 81 | .and_then(move |result| match result { 82 | Ok(message) => { 83 | let mut ctx = Context::new(); 84 | ctx.insert("message", &message); 85 | Template::render_with_user(&req, "message/show.html", &mut ctx) 86 | }, 87 | Err(e) => Ok(HttpResponse::InternalServerError().body(e.to_string())), 88 | }) 89 | .responder() 90 | } 91 | 92 | pub fn new(req: HttpRequest) -> SyncResponse { 93 | let mut ctx = Context::new(); 94 | let query = Query::>::extract(&req)?; 95 | if let Some(receiver) = query.get("receiver") { 96 | ctx.insert("receiver", &receiver); 97 | } 98 | Template::render_with_user(&req, "message/new.html", &mut ctx) 99 | } 100 | 101 | pub fn reply(mut req: HttpRequest) -> FutureResponse { 102 | let user_id = match session_creds(&mut req) { 103 | Some((u, _)) => u, 104 | None => return async_redirect("/login"), 105 | }; 106 | 107 | let id = { 108 | let mut id = req.match_info().get("id"); 109 | if id.is_none() { 110 | return Box::new(FutErr(ErrorNotFound("no message id"))); 111 | } 112 | let id = Uuid::parse_str(id.take().unwrap()); 113 | if id.is_err() { 114 | return Box::new(FutErr(ErrorNotFound("invalid message id"))); 115 | } 116 | id.unwrap() 117 | }; 118 | let mut loadmsg = LoadMessageMsg::new(id, user_id); 119 | loadmsg.mark_as_read(true); 120 | 121 | req.clone().state() 122 | .db() 123 | .send(loadmsg) 124 | .from_err() 125 | .and_then(move |result| match result { 126 | Ok(message) => { 127 | let mut ctx = Context::new(); 128 | ctx.insert("message", &message); 129 | Template::render_with_user(&req, "message/new.html", &mut ctx) 130 | }, 131 | Err(e) => Ok(HttpResponse::InternalServerError().body(e.to_string())), 132 | }) 133 | .responder() 134 | } 135 | -------------------------------------------------------------------------------- /src/app/signup.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | use handlers::user::{ConfirmMsg, SignupForm}; 21 | use actix_web::AsyncResponder; 22 | use actix_web::HttpMessage; 23 | 24 | pub fn signup(req: HttpRequest) -> SyncResponse { 25 | let mut ctx = Context::new(); 26 | ctx.insert("username", ""); 27 | ctx.insert("email", ""); 28 | ctx.insert("error", ""); 29 | ctx.insert("confirm_id", ""); 30 | Template::render(&req.state().template(), "signup/signup.html", &ctx) 31 | } 32 | 33 | pub fn take_signup(req: HttpRequest) -> FutureResponse { 34 | let cloned = req.clone(); 35 | let form = match cloned.urlencoded::().wait() { 36 | Ok(form) => form, 37 | Err(e) => return Box::new(future::err(actix_web::error::ErrorInternalServerError(format!("{}", e)))) 38 | }; 39 | 40 | let cloned = req.clone(); 41 | cloned.state() 42 | .db() 43 | .send(form.clone()) 44 | .from_err() 45 | .and_then(move |r| { 46 | let mut ctx = HashMap::new(); 47 | let mut fail = true; 48 | 49 | match r { 50 | Ok(confirm_id) => { 51 | fail = false; 52 | let settings = match SETTINGS.read() { 53 | Ok(s) => s, 54 | Err(e) => return Err(actix_web::error::ErrorInternalServerError(format!("{}", e))), 55 | }; 56 | 57 | if settings.email.enabled { 58 | // send confirmation mail 59 | ctx.insert("confirm_id", "".to_string()); 60 | } else { 61 | ctx.insert("confirm_id", confirm_id); 62 | } 63 | } 64 | Err(e) => { 65 | ctx.insert("error", format!("{}", e)); 66 | } 67 | } 68 | 69 | let tpl = if fail { 70 | ctx.insert("username", form.username); 71 | ctx.insert("email", form.email); 72 | "signup/signup.html" 73 | } else { 74 | "signup/signup_complete.html" 75 | }; 76 | 77 | let template = req.state().template(); 78 | Template::render(&template, tpl, ctx) 79 | }) 80 | .responder() 81 | } 82 | 83 | pub fn confirm(req: HttpRequest) -> FutureResponse { 84 | let id = match req.match_info().query("id") { 85 | Ok(id) => id, 86 | Err(e) => return Box::new(future::err(actix_web::error::ErrorInternalServerError(format!("{}", e)))) 87 | }; 88 | let ip_address = match req.peer_addr() { 89 | Some(sock_addr) => sock_addr.ip(), 90 | None => return Box::new(future::err(actix_web::error::ErrorInternalServerError("failed to get peer address"))) 91 | }; 92 | let confirm = ConfirmMsg { 93 | id, 94 | ip_address, 95 | }; 96 | 97 | let cloned = req.clone(); 98 | cloned.state() 99 | .db() 100 | .send(confirm) 101 | .from_err() 102 | .and_then(move |res| { 103 | let mut ctx = HashMap::new(); 104 | let mut fail = true; 105 | 106 | match res { 107 | Ok(user) => { 108 | match req.session().set("user_id", user.id) { 109 | Ok(_) => {}, 110 | Err(e) => return Err(actix_web::error::ErrorInternalServerError(format!("{}", e))), 111 | }; 112 | match req.session().set("group_id", user.group_id) { 113 | Ok(_) => {}, 114 | Err(e) => return Err(actix_web::error::ErrorInternalServerError(format!("{}", e))), 115 | }; 116 | fail = false; 117 | }, 118 | Err(e) => { 119 | ctx.insert("error", format!("{}", e)); 120 | } 121 | } 122 | 123 | let tpl = if fail { 124 | "signup/confirm_fail.html" 125 | } else { 126 | "signup/confirm_complete.html" 127 | }; 128 | 129 | let template = req.state().template(); 130 | Template::render(&template, tpl, ctx) 131 | }) 132 | .responder() 133 | } 134 | -------------------------------------------------------------------------------- /templates/signup/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_public.html" %} 2 | {% block title %}Sign Up{% endblock title %} 3 | {% block content %} 4 |
    5 |
    6 |
    Sign Up
    7 |
    8 | {% if error %} 9 |

    {{error}}

    10 | {% endif %} 11 |
    12 |
    13 | 14 |
    15 | 16 | 17 | Your username may only contain letters, numbers, _ and -. It must begin with a letter and has to be at least 4 characters long. 18 | 19 |
    20 | Please choose a Username. 21 |
    22 |
    23 |
    24 |
    25 | 26 |
    27 | 28 |
    29 | Please provide an Email address. 30 |
    31 |
    32 |
    33 |
    34 | 35 |
    36 | 37 | 38 | Your password must be at least 8 characters long. 39 | 40 |
    41 | Please choose a password. 42 |
    43 |
    44 |
    45 | 46 |
    47 | Please confirm the password. 48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 | 55 | 58 |
    59 | You must agree before submitting. 60 |
    61 |
    62 |
    63 |
    64 |
    65 |
    66 | 67 |
    68 |
    69 |
    70 |
    71 |
    72 |
    73 | 74 | 94 | {% endblock content %} -------------------------------------------------------------------------------- /templates/torrent/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
    4 | {% if error %} 5 | 8 | {% endif %} 9 |
    10 |
    11 |
    12 |
    {{torrent.name}}
    13 |
    14 |
    15 |
    16 |
    17 | 18 | 19 |
    20 |
    21 |
    22 |
    23 | 24 |
    25 | 26 | 27 |
    28 |
    29 |
    30 | 31 | 37 |
    38 |
    39 |
    40 |
    41 | 42 |
    43 | 44 | 45 |
    46 |
    47 |
    48 | 49 |
    50 | 51 | 52 |
    53 |
    54 |
    55 |
    56 |
    57 |
    58 | 59 | 60 |
    61 |
    62 |
    63 |
    64 |
    65 | 66 | 67 | 68 | Markdown is supported 69 | 70 |
    71 |
    72 |
    73 |
    74 | Back 75 | 76 | Delete Torrent 77 |
    78 |
    79 |
    80 | 81 |
    82 |
    83 |
    84 |
    85 | 86 | {% endblock content %} 87 | {% block title %}Edit Torrent: {{torrent.name}}{% endblock title %} -------------------------------------------------------------------------------- /assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | // Your variable overrides 2 | $body-bg: #222222; 3 | $body-color: #eeeeee; 4 | $primary: #468cbf; 5 | 6 | $dropdown-bg: $body-bg; 7 | $dropdown-link-color: rgba(255, 255, 255, 0.5); 8 | $dropdown-link-hover-bg: lighten($dropdown-bg, 20%); 9 | 10 | //$spacer: 0.1rem; 11 | 12 | $navbar-padding-y: 0.05rem; 13 | $navbar-padding-x: 0.1rem; 14 | 15 | $theme-colors: ( 16 | "primary": #468cbf, 17 | ); 18 | 19 | $nav-tabs-link-active-border-color: #333; 20 | $nav-tabs-link-hover-border-color: $nav-tabs-link-active-border-color; 21 | $nav-tabs-link-active-color: $primary; 22 | 23 | $font-size-base: 0.8rem; 24 | 25 | $table-dark-color: $body-color; 26 | 27 | $pre-color: #A9B7C6; 28 | 29 | $list-group-bg: #343a40; 30 | 31 | @import "../../bootstrap/scss/bootstrap"; 32 | 33 | /* header and footer */ 34 | body > footer { 35 | padding: 0; 36 | overflow: hidden; 37 | //margin: 40px 0 0; 38 | color: $link-color; 39 | background-color: $dark; 40 | //border-top: solid 1px $navbar-dark-toggler-border-color; 41 | 42 | h1 { 43 | margin: 0; 44 | padding: 0; 45 | font-size: 1.1em; 46 | 47 | a { 48 | color: theme-color("primary"); 49 | display: inline-block; 50 | padding: 7px 10px; 51 | } 52 | 53 | a:hover { 54 | color: black; 55 | } 56 | 57 | a .name { 58 | opacity: 0.65; 59 | } 60 | 61 | a:hover .name { 62 | opacity: 1; 63 | } 64 | 65 | a:hover .name { 66 | text-decoration: none; 67 | } 68 | } 69 | } 70 | 71 | /* main */ 72 | body > .main { 73 | //padding: 10px 10px; 74 | } 75 | 76 | #sidebar { 77 | background-color: $dark; 78 | 79 | .list-group-item { 80 | border-radius: 0; 81 | background-color: $dark; 82 | color: $navbar-dark-color; 83 | border-left: 0; 84 | border-right: 0; 85 | border-color: $navbar-dark-toggler-border-color; 86 | white-space: nowrap; 87 | } 88 | 89 | // closed state 90 | .list-group-item[aria-expanded="false"]::after { 91 | content: " \f0d7"; 92 | font-family: 'Font Awesome 5 Free'; 93 | display: inline; 94 | text-align: right; 95 | padding-left: 5px; 96 | } 97 | 98 | // open state 99 | .list-group-item[aria-expanded="true"] { 100 | background-color: darken($dark, 5%); 101 | } 102 | 103 | .list-group-item[aria-expanded="true"]::after { 104 | content: " \f0da"; 105 | font-family: 'Font Awesome 5 Free'; 106 | display: inline; 107 | text-align: right; 108 | padding-left: 5px; 109 | } 110 | 111 | // highlight active menu 112 | .list-group-item:not(.collapsed) { 113 | background-color: darken($dark, 5%); 114 | } 115 | 116 | #sidebar .list-group .collapse { 117 | // level 1 118 | .list-group-item { 119 | padding-left: 20px; 120 | } 121 | 122 | // level 2 123 | > .collapse .list-group-item { 124 | padding-left: 30px; 125 | 126 | // level 3 127 | > .collapse .list-group-item { 128 | padding-left: 40px; 129 | } 130 | } 131 | } 132 | } 133 | 134 | @media (max-width: 48em) { 135 | // overlay sub levels on small screens 136 | #sidebar .list-group .collapse.in, #sidebar .list-group .collapsing { 137 | position: absolute; 138 | z-index: 1; 139 | width: 190px; 140 | } 141 | #sidebar .list-group > .list-group-item { 142 | text-align: center; 143 | padding: .75rem .5rem; 144 | } 145 | // hide caret icons of top level when collapsed 146 | #sidebar .list-group > .list-group-item[aria-expanded="true"]::after, 147 | #sidebar .list-group > .list-group-item[aria-expanded="false"]::after { 148 | display: none; 149 | } 150 | } 151 | 152 | // change transition animation to width when entire sidebar is toggled 153 | #sidebar.collapse { 154 | -webkit-transition-timing-function: ease; 155 | -o-transition-timing-function: ease; 156 | transition-timing-function: ease; 157 | -webkit-transition-duration: .2s; 158 | -o-transition-duration: .2s; 159 | transition-duration: .2s; 160 | } 161 | 162 | #sidebar.collapsing { 163 | opacity: 0.8; 164 | width: 0; 165 | -webkit-transition-timing-function: ease-in; 166 | -o-transition-timing-function: ease-in; 167 | transition-timing-function: ease-in; 168 | -webkit-transition-property: width; 169 | -o-transition-property: width; 170 | transition-property: width; 171 | 172 | } 173 | 174 | .home-panel:nth-child(n+2) { 175 | margin-top: 1rem; 176 | } 177 | 178 | pre.nfo { 179 | color: $white; 180 | background-color: $black; 181 | font-family: "Lucida ConsoleP", "Terminal", monospace; 182 | line-height: 1.0em; 183 | font-size: 8pt; 184 | min-width: 600px; 185 | width: 600px; 186 | } 187 | 188 | .shoutbox { 189 | height: 400px; 190 | max-height: 400px; 191 | overflow: auto; 192 | .shoutbox-line { 193 | font-family: $font-family-monospace; 194 | font-size: $font-size-sm; 195 | 196 | .shoutbox-message { 197 | font-family: $font-family-base; 198 | font-size: $font-size-sm; 199 | } 200 | } 201 | } 202 | 203 | .user-group { 204 | font-weight: bold; 205 | } 206 | 207 | .user-group-0eb8ac8f-01f4-4bf9-bb0d-e3ac0ecb15f9, 208 | .user-group-user { 209 | color: $body-color; 210 | } 211 | .user-group-91c1ba93-6153-4913-9993-18ba638452d2, 212 | .user-group-moderator { 213 | color: #20c000; 214 | } 215 | .user-group-5a4517e3-f615-43f3-8852-9bb310ae688e, 216 | .user-group-administrator { 217 | color: #d08200; 218 | } 219 | .user-group-7ad31559-5be8-40e0-9656-8b50ad1cdb39, 220 | .user-group-sysop { 221 | color: #d10c00; 222 | } 223 | 224 | .index-user-list a:not(:last-child)::after { 225 | content: ', '; 226 | } 227 | 228 | .message-body { 229 | background-color: lighten($dark, 10%); 230 | } 231 | 232 | .flair { 233 | vertical-align: super; 234 | font-size: $font-size-sm; 235 | } -------------------------------------------------------------------------------- /templates/torrent/new.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
    4 |
    5 |
    Upload new Torrent
    6 |
    7 | {% if error %} 8 |

    {{error}}

    9 | {% endif %} 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 | 40 |
    41 |
    42 |
    43 |
    44 | 45 |
    46 | 47 | 48 |
    49 |
    50 |
    51 | 52 |
    53 | 54 | 55 |
    56 |
    57 |
    58 |
    59 |
    60 | 61 | 62 | 63 | Markdown is supported 64 | 65 |
    66 | 67 | 68 |
    69 | 70 |
    71 |
    72 |
    73 | 74 |
    75 |
    76 |
    77 |
    78 |
    79 | 104 | {% endblock content %} 105 | {% block title %}Upload{% endblock title %} -------------------------------------------------------------------------------- /src/api/chat.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! Chat API 20 | //! 21 | //! [**ChatRoom**](../../models/chat/enum.ChatRoom.html) is used to identify the chatroom. 22 | //! 23 | //! [**ChatMessageWithUser**](../../models/chat/struct.ChatMessageWithUser.html) is used whenever a message should be returned. 24 | 25 | use super::*; 26 | 27 | use actix_web::AsyncResponder; 28 | use actix_web::Json; 29 | use identity::RequestIdentity; 30 | use handlers::chat::{LoadChatMessagesMsg, PublishChatMessagesMsg}; 31 | use handlers::UserSubjectMsg; 32 | use models::chat::{ChatMessageWithUser, ChatRoom}; 33 | use std::convert::TryFrom; 34 | 35 | /// Fetch chat messages 36 | /// 37 | /// `GET /api/v1/chat/messages` 38 | /// 39 | /// # Parameters 40 | /// 41 | /// | Parameter | Type | Description | 42 | /// |-----------|-------|-------------| 43 | /// | `chat` | `i16` | Which chatroom to use. 1: [**Public**](../../models/chat/enum.ChatRoom.html#variant.Public), 2: [**Team**](../../models/chat/enum.ChatRoom.html#variant.Team) | 44 | /// | `since` | `i64` | Unix Timestamp. Fetch only messages newer than this Timestamp. | 45 | /// | `limit` | `i64` | Limit response to the latest `limit` messages. | 46 | /// 47 | /// # Returns 48 | /// 49 | /// If successful, `messages` returns a list of [**Messages**](../../models/chat/struct.ChatMessageWithUser.html). 50 | /// 51 | /// Each message consists of: 52 | /// 53 | /// | Field | Type | Description | 54 | /// |--------------|-----------------|-------------| 55 | /// | `id` | `Uuid` | Unique Message ID | 56 | /// | `user_id` | `Uuid` | User ID | 57 | /// | `chat` | `i16` | Chatroom | 58 | /// | `message` | `String` | The actual message content | 59 | /// | `created_at` | `Datetime` | Timestamp when the message was created | 60 | /// | `user_name` | `String` | Name of the User | 61 | /// | `user_group` | `Uuid` | User Group ID | 62 | /// 63 | /// # Errors 64 | /// 65 | /// - `ErrorUnauthorized` if the client is not authorized. 66 | /// - `ErrorBadRequest` if the request parameters are invalid. 67 | /// - `ErrorForbidden` if the client is not allowed to read the given chatroom. 68 | pub fn messages(req: HttpRequest) -> FutureResponse { 69 | if req.credentials().is_none() { 70 | return Box::new(FutErr(ErrorUnauthorized("unauthorized"))); 71 | } 72 | let msg = match LoadChatMessagesMsg::try_from(&req) { 73 | Ok(msg) => msg, 74 | Err(e) => return Box::new(FutErr(ErrorBadRequest(e.to_string()))), 75 | }; 76 | 77 | req.state() 78 | .db() 79 | .send(msg) 80 | .from_err() 81 | .and_then(|result: Result>| match result { 82 | Ok(messages) => Ok(HttpResponse::Ok().json(messages)), 83 | Err(e) => Err(ErrorForbidden(e.to_string())), 84 | }) 85 | .responder() 86 | } 87 | 88 | /// Publish Message Payload 89 | #[derive(Deserialize)] 90 | pub struct PublishMessage { 91 | /// In which chatroom should the message be posted 92 | pub chat: i16, 93 | /// The actual chat message 94 | pub message: String, 95 | } 96 | 97 | /// Publish a new chat message 98 | /// 99 | /// `POST /api/v1/chat/publish` 100 | /// 101 | /// # Payload 102 | /// 103 | /// [**PublishMessage**](struct.PublishMessage.html) as JSON. 104 | /// 105 | /// # Returns 106 | /// 107 | /// If successful, publish returns the [posted message](../../models/chat/struct.ChatMessageWithUser.html). 108 | /// 109 | /// | Field | Type | Description | 110 | /// |--------------|-----------------|-------------| 111 | /// | `id` | `Uuid` | Unique Message ID | 112 | /// | `user_id` | `Uuid` | User ID | 113 | /// | `chat` | `i16` | Chatroom | 114 | /// | `message` | `String` | The actual message content | 115 | /// | `created_at` | `Datetime` | Timestamp when the message was created | 116 | /// | `user_name` | `String` | Name of the User | 117 | /// | `user_group` | `Uuid` | User Group ID | 118 | /// 119 | /// # Errors 120 | /// 121 | /// - `ErrorUnauthorized` if the client is not authorized. 122 | /// - `ErrorBadRequest` if the request payload is invalid. 123 | /// - `ErrorForbidden` if the client is not allowed to write to the given chatroom. 124 | pub fn publish(req: HttpRequest, data: Json) -> FutureResponse { 125 | let mut credentials = req.credentials(); 126 | if credentials.is_none() { 127 | return Box::new(FutErr(ErrorUnauthorized("unauthorized"))); 128 | } 129 | let (user_id, group_id) = credentials.take().unwrap(); 130 | 131 | let PublishMessage { chat, message } = data.into_inner(); 132 | let user = UserSubjectMsg::new(*user_id, *group_id, req.state().acl().clone()); 133 | let chat = match ChatRoom::try_from(chat) { 134 | Ok(chat) => chat, 135 | Err(e) => return Box::new(FutErr(ErrorBadRequest(e.to_string()))), 136 | }; 137 | let msg = PublishChatMessagesMsg::new(chat, message, user); 138 | 139 | req.state() 140 | .db() 141 | .send(msg) 142 | .from_err() 143 | .and_then(move |result: Result| match result { 144 | Ok(message) => Ok(HttpResponse::Ok().json(message)), 145 | Err(e) => Err(ErrorForbidden(e.to_string())), 146 | }) 147 | .responder() 148 | } 149 | -------------------------------------------------------------------------------- /src/app/static_content.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | use super::*; 20 | use actix_web::AsyncResponder; 21 | use actix_web::Form; 22 | use handlers::UserSubjectMsg; 23 | use handlers::static_content::{LoadStaticContentMsg, UpdateStaticContentMsg}; 24 | use models::static_content::Content; 25 | use models::acl::Subject; 26 | 27 | #[derive(Deserialize)] 28 | pub struct ContentForm { 29 | id: String, 30 | title: String, 31 | content: String, 32 | content_type: String, 33 | } 34 | 35 | impl Into for ContentForm { 36 | fn into(self) -> UpdateStaticContentMsg { 37 | let ContentForm{id, title, content, content_type} = self; 38 | UpdateStaticContentMsg::new(id, title, content, content_type) 39 | } 40 | } 41 | 42 | impl From for Context { 43 | fn from(c: Content) -> Self { 44 | let mut ctx = Context::new(); 45 | ctx.insert("id", &c.id); 46 | ctx.insert("title", &c.title); 47 | ctx.insert("updated_at", &c.updated_at); 48 | 49 | ctx 50 | } 51 | } 52 | 53 | fn view(mut req: HttpRequest, id: String) -> FutureResponse { 54 | let (user_id, group_id) = match session_creds(&mut req) { 55 | Some((u, g)) => (u, g), 56 | None => return async_redirect("/login"), 57 | }; 58 | 59 | let msg = LoadStaticContentMsg::new(id); 60 | req.clone().state().db().send(msg) 61 | .from_err() 62 | .and_then(move |result| { 63 | match result { 64 | Ok(c) => { 65 | let subj = UserSubject::new(&user_id, &group_id, req.state().acl()); 66 | let may_edit = subj.may_write(&c); 67 | let content = c.render(); 68 | let mut ctx = Context::from(c); 69 | ctx.insert("may_edit", &may_edit); 70 | ctx.insert("content", &content); 71 | Template::render_with_user(&req, "static_content/view.html", &mut ctx) 72 | }, 73 | Err(e) => Ok(HttpResponse::InternalServerError().body(e.to_string())), 74 | } 75 | }) 76 | .responder() 77 | } 78 | 79 | pub fn faq(req: HttpRequest) -> FutureResponse { 80 | view(req, "faq".to_string()) 81 | } 82 | 83 | pub fn rules(req: HttpRequest) -> FutureResponse { 84 | view(req, "rules".to_string()) 85 | } 86 | 87 | pub fn edit(mut req: HttpRequest) -> FutureResponse { 88 | let (user_id, group_id) = match session_creds(&mut req) { 89 | Some((u, g)) => (u, g), 90 | None => return async_redirect("/login"), 91 | }; 92 | 93 | let id = { 94 | let id = req.match_info().get("id"); 95 | if id.is_none() { 96 | return Box::new(FutErr(ErrorNotFound("no content id"))); 97 | } 98 | id.unwrap().to_string() 99 | }; 100 | 101 | let msg = LoadStaticContentMsg::new(id); 102 | req.clone().state().db().send(msg) 103 | .from_err() 104 | .and_then(move |result| { 105 | match result { 106 | Ok(c) => { 107 | let subj = UserSubject::new(&user_id, &group_id, req.state().acl()); 108 | let may_edit = subj.may_write(&c); 109 | let content = c.render(); 110 | let mut ctx = Context::from(c); 111 | ctx.insert("may_edit", &may_edit); 112 | ctx.insert("content", &content); 113 | if may_edit { 114 | Template::render_with_user(&req, "static_content/edit.html", &mut ctx) 115 | } else { 116 | sync_redirect("/") 117 | } 118 | }, 119 | Err(e) => Ok(HttpResponse::InternalServerError().body(e.to_string())), 120 | } 121 | }) 122 | .responder() 123 | } 124 | 125 | pub fn update(mut req: HttpRequest, data: Form) -> FutureResponse { 126 | let (user_id, group_id) = match session_creds(&mut req) { 127 | Some((u, g)) => (u, g), 128 | None => return async_redirect("/login"), 129 | }; 130 | 131 | 132 | let id = { 133 | let id = req.match_info().get("id"); 134 | if id.is_none() { 135 | return Box::new(FutErr(ErrorNotFound("no content id"))); 136 | } 137 | id.unwrap().to_string() 138 | }; 139 | 140 | let data = data.into_inner(); 141 | let mut msg: UpdateStaticContentMsg = data.into(); 142 | let subj = UserSubjectMsg::new(user_id, group_id, req.state().acl().clone()); 143 | msg.set_acl(subj); 144 | req.clone().state().db().send(msg) 145 | .from_err() 146 | .and_then(move |result| { 147 | match result { 148 | Ok(c) => { 149 | let may_edit = true; 150 | let content = c.render(); 151 | let mut ctx = Context::from(c); 152 | ctx.insert("may_edit", &may_edit); 153 | ctx.insert("content", &content); 154 | Template::render_with_user(&req, "static_content/view.html", &mut ctx) 155 | }, 156 | Err(e) => { 157 | let mut ctx = Context::new(); 158 | ctx.insert("error", &e.to_string()); 159 | ctx.insert("back_link", &format!("/content/edit/{}", id)); 160 | ctx.insert("title", "Edit failed"); 161 | 162 | Template::render_with_user(&req, "static_content/edit_failed.html", &mut ctx) 163 | }, 164 | } 165 | }) 166 | .responder() 167 | } 168 | -------------------------------------------------------------------------------- /templates/torrent/list.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base_authenticated.html" %} 2 | {% block content %} 3 |
    4 |
    5 |
    6 |
    7 | 8 |
    9 |
    10 | 11 | 17 |
    18 |
    19 | 20 | 25 |
    26 |
    27 |
    28 |
    29 | 30 |
    31 |
    32 | 33 |
    34 |
    35 |
    36 |
    37 |
    38 |
    39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {% for torrent in list %} 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {% endfor %} 73 | 74 |
    TypNameAddedLast ActionLast SeederCommentsFilesSizeCompletedSeederLeecherUploader
    {{torrent.category_name}}{{torrent.name}}{{torrent.created_at | format_date(timezone=timezone)}}{{torrent.last_action | format_date(timezone=timezone)}}{{torrent.last_seeder | format_date(timezone=timezone)}}{{torrent.comments}}{{torrent.files}}{{torrent.size | data_size}}{{torrent.completed}}{{torrent.seeder}}{{torrent.leecher}}{{torrent.user_name}}
    75 | {%if pages > 1 %} 76 |
    77 | 78 | 79 | 80 | 113 |
    114 | {% endif %} 115 |
    116 |
    117 | {% endblock content %} 118 | {% block title %}Browse Torrents{% endblock title %} -------------------------------------------------------------------------------- /src/models/chat.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! Chat models 20 | //! 21 | //! [**ChatRoom**](enum.Chatroom.html) is used to identify the chatroom. 22 | //! 23 | //! [**ChatMessage**](struct.ChatMessage.html) is the data struct, which represents a row in the database. 24 | //! 25 | //! [**ChatMessageWithUser**](struct.ChatMessageWithUser.html) is the same as `ChatMessage`, but with the user name and user group added. 26 | //! 27 | //! [**NewChatMessage**](struct.NewChatMessage.html) can be used to create new messages. 28 | 29 | use super::*; 30 | 31 | use schema::chat_messages; 32 | use schema::chat_messages::dsl as cm; 33 | use std::convert::TryFrom; 34 | 35 | /// The chatroom 36 | /// 37 | /// Can be used with the [**UserSubject**](../../models/acl/struct.UserSubject.html) object to check the permissions of the user. 38 | #[derive(PartialEq)] 39 | pub enum ChatRoom { 40 | /// Public (all registered users) chatroom. 41 | /// When converted to `i16` it has the value `1`. 42 | Public, 43 | /// Team (moderator+) only chatroom. 44 | /// When converted to `i16` it has the value `2`. 45 | Team, 46 | } 47 | 48 | impl TryFrom for ChatRoom { 49 | type Error = Error; 50 | 51 | fn try_from(value: i16) -> Result { 52 | match value { 53 | 1 => Ok(ChatRoom::Public), 54 | 2 => Ok(ChatRoom::Team), 55 | _ => bail!("no chatroom with this id") 56 | } 57 | } 58 | } 59 | 60 | impl Into for ChatRoom { 61 | fn into(self) -> i16 { 62 | match self { 63 | ChatRoom::Public => 1, 64 | ChatRoom::Team => 2, 65 | } 66 | } 67 | } 68 | 69 | impl PartialEq for ChatRoom { 70 | fn eq(&self, other: &i16) -> bool { 71 | match ChatRoom::try_from(*other) { 72 | Ok(other) => other == *self, 73 | Err(_) => false, 74 | } 75 | } 76 | } 77 | 78 | impl ToString for ChatRoom { 79 | fn to_string(&self) -> String { 80 | match self { 81 | ChatRoom::Public => "public".to_string(), 82 | ChatRoom::Team => "team".to_string(), 83 | } 84 | } 85 | } 86 | 87 | /// Chat message data structure 88 | /// 89 | /// Each instance represents a row in the database. 90 | #[derive(Clone, Debug, Queryable, Identifiable, Insertable, Associations, Serialize)] 91 | #[table_name = "chat_messages"] 92 | #[belongs_to(User)] 93 | pub struct ChatMessage { 94 | /// the unique message id 95 | pub id: Uuid, 96 | /// user id 97 | pub user_id: Uuid, 98 | /// chatroom. Can be converted to [**ChatRoom**](enum.Chatroom.html). 99 | pub chat: i16, 100 | /// the actual message 101 | pub message: String, 102 | /// timestamp when the message was created 103 | pub created_at: Timestamp, 104 | } 105 | 106 | /// Chat message data structure 107 | /// 108 | /// is the same as `ChatMessage`, but with the user name and user group added. 109 | /// 110 | /// Used whenever a chat message needs to be returned to the user. 111 | #[derive(Clone, Debug, Queryable, Identifiable, Associations, Serialize)] 112 | #[table_name = "chat_messages"] 113 | #[belongs_to(User)] 114 | pub struct ChatMessageWithUser { 115 | /// the unique message id 116 | pub id: Uuid, 117 | /// user id 118 | pub user_id: Uuid, 119 | /// chatroom. Can be converted to [**ChatRoom**](enum.Chatroom.html). 120 | pub chat: i16, 121 | /// the actual message 122 | pub message: String, 123 | /// timestamp when the message was created 124 | pub created_at: Timestamp, 125 | /// user name 126 | pub user_name: String, 127 | /// user group 128 | pub user_group: Uuid, 129 | } 130 | 131 | impl ChatMessageWithUser { 132 | /// Fetch all messages for a chatroom 133 | /// 134 | /// Filters by `chat`. 135 | /// 136 | /// If `since` is `Some` Timestamp, it only returns message newer than the timestamp. 137 | /// 138 | /// Returns at most `limit` messages 139 | pub fn messages_for_chat(chat: i16, since: Option, limit: i64, db: &PgConnection) -> Vec { 140 | use schema::users; 141 | use schema::users::dsl as u; 142 | 143 | let mut query = chat_messages::table.into_boxed() 144 | .inner_join(users::table) 145 | .select((cm::id, cm::user_id, cm::chat, cm::message, cm::created_at, u::name, u::group_id)) 146 | .filter(cm::chat.eq(chat)); 147 | 148 | if let Some(since) = since { 149 | query = query.filter(cm::created_at.gt(since)) 150 | } 151 | 152 | query.order_by(cm::created_at.desc()) 153 | .limit(limit) 154 | .load::(db) 155 | .unwrap_or_default() 156 | } 157 | } 158 | 159 | impl From for ChatMessageWithUser { 160 | fn from(msg: ChatMessage) -> Self { 161 | ChatMessageWithUser{ 162 | id: msg.id, 163 | user_id: msg.user_id, 164 | chat: msg.chat, 165 | message: msg.message, 166 | created_at: msg.created_at, 167 | user_name: String::default(), 168 | user_group: Uuid::default(), 169 | } 170 | } 171 | } 172 | 173 | /// A new chat message 174 | #[derive(Insertable)] 175 | #[table_name = "chat_messages"] 176 | pub struct NewChatMessage<'a> { 177 | id: Uuid, 178 | user_id: &'a Uuid, 179 | chat: &'a i16, 180 | message: &'a str, 181 | } 182 | 183 | impl<'a> NewChatMessage<'a> { 184 | /// Constructs a new `NewChatMessage` instance. 185 | pub fn new(user_id: &'a Uuid, chat: &'a i16, message: &'a str) -> NewChatMessage<'a> { 186 | let id = Uuid::new_v4(); 187 | NewChatMessage { 188 | id, 189 | user_id, 190 | chat, 191 | message, 192 | } 193 | } 194 | 195 | /// Save the message into the database. 196 | pub fn save(&self, db: &PgConnection) -> Result { 197 | self.insert_into(chat_messages::table) 198 | .get_result::(db) 199 | .map_err(|e| format!("failed to create chat message: {}", e).into()) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/template/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * ripalt 3 | * Copyright (C) 2018 Daniel Müller 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | //! Template system 20 | //! 21 | //! This template system uses [Tera](https://tera.netlify.com/) 22 | 23 | use super::*; 24 | 25 | use std::sync::{mpsc, Arc, RwLock}; 26 | use std::time::Duration; 27 | 28 | use tera::{Context, Tera}; 29 | 30 | use notify::{RecommendedWatcher, RecursiveMode, Watcher}; 31 | use serde::Serialize; 32 | 33 | pub type TemplateContainer = Arc>; 34 | pub type TemplateSystem = Tera; 35 | 36 | mod helper; 37 | 38 | /// Initialize the Tera template system 39 | pub fn init_tera(acl: Arc>) -> TemplateContainer { 40 | let mut tera = match Tera::new("templates/**/*") { 41 | Ok(t) => t, 42 | Err(e) => { 43 | error!("{}", e); 44 | error!("{:#?}", e); 45 | panic!("failed to load templates {}", e); 46 | } 47 | }; 48 | 49 | let acl_container = AclContainer::new(acl); 50 | 51 | tera.register_filter("data_size", helper::data_size); 52 | tera.register_filter("format_date", helper::format_date); 53 | tera.register_filter("duration", helper::duration); 54 | tera.register_filter("markdown", helper::markdown); 55 | tera.register_filter("quote", helper::quote); 56 | tera.register_global_function("is_allowed", helper::is_allowed(acl_container.clone())); 57 | 58 | Arc::new(RwLock::new(tera)) 59 | } 60 | 61 | /// Watcher function to detect changed templates 62 | /// 63 | /// If any file in the template fonder is changed, the handlebars registry is flushed and all 64 | /// templates are re-registered again. 65 | /// 66 | /// `main_rx` is the communication channel with the calling function, if it receives any value 67 | /// the watcher function will terminate. 68 | /// 69 | /// # Panics 70 | /// This function panics if the `Watcher` can not be created, can not watch the template directory 71 | /// or if it fails to acquire a Write-Lock on the `tera` object. 72 | pub fn template_file_watcher(tpl: TemplateContainer, main_rx: &mpsc::Receiver) { 73 | info!("started template watcher"); 74 | // Create a channel to receive the events. 75 | let (tx, rx) = mpsc::channel(); 76 | 77 | // Automatically select the best implementation for your platform. 78 | // You can also access each implementation directly e.g. INotifyWatcher. 79 | let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2)).unwrap(); 80 | 81 | let path = concat!(env!("CARGO_MANIFEST_DIR"), "/templates/"); 82 | debug!("watching {} for changes", path); 83 | // Add a path to be watched. All files and directories at that path and 84 | // below will be monitored for changes. 85 | watcher.watch(path, RecursiveMode::Recursive).unwrap(); 86 | 87 | loop { 88 | // try to receive from the main_rx in order to terminate 89 | if main_rx.try_recv().is_ok() { 90 | info!("shutting down template watcher"); 91 | return; 92 | } 93 | 94 | // receive with a timeout from the watcher channel. 95 | // the timeout is necessary to read from the main->watcher channel in order 96 | // to shut down. 97 | match rx.recv_timeout(Duration::from_secs(2)) { 98 | Ok(event) => { 99 | info!("reloading templates: {:?}", event); 100 | let mut tera = tpl.write().unwrap(); 101 | match tera.full_reload() { 102 | Ok(_) => info!("templates reloaded"), 103 | Err(e) => error!("failed to reload templates: {}", e), 104 | } 105 | } 106 | Err(mpsc::RecvTimeoutError::Timeout) => {} 107 | Err(e) => warn!("watch error: {:?}", e), 108 | } 109 | } 110 | } 111 | 112 | /// Container for the rendered template data 113 | /// 114 | /// Template can be used as a `Responder` or `HttpResponse` for handler functions. 115 | pub struct Template { 116 | pub body: String, 117 | pub content_type: String, 118 | } 119 | 120 | impl Template { 121 | /// Create a new Template container 122 | #[allow(dead_code)] 123 | pub fn new(body: String, content_type: String) -> Self { 124 | Template { body, content_type } 125 | } 126 | 127 | /// Render a registered template 128 | /// 129 | /// render returns a `Result`, which is suitable to be returned by a handler function. 130 | /// If the template fails to render, `Error` is set to `ErrorInternalServerError` 131 | pub fn render( 132 | tpl: &TemplateSystem, 133 | name: &str, 134 | ctx: T, 135 | ) -> actix_web::Result 136 | where 137 | T: Serialize, 138 | { 139 | let s = tpl.render(name, &ctx).map_err(|e| { 140 | error!("{:#?}", e); 141 | ErrorInternalServerError(format!("{}", e)) 142 | })?; 143 | 144 | let mut tpl = Template::default(); 145 | tpl.body = s; 146 | 147 | let resp = HttpResponse::build(StatusCode::OK) 148 | .content_type(&tpl.content_type[..]) 149 | .body(tpl.body); 150 | Ok(resp) 151 | } 152 | 153 | /// Render a registered template and add the current user to the context 154 | /// 155 | /// The current user is added as `current_user` 156 | /// 157 | /// render returns a `Result`, which is suitable to be returned by a handler function. 158 | /// If the template fails to render, `Error` is set to `ErrorInternalServerError` 159 | pub fn render_with_user(req: &HttpRequest, name: &str, ctx: &mut Context) -> actix_web::Result 160 | { 161 | let user = req.current_user(); 162 | ctx.insert("current_user", &user); 163 | Self::render(&req.state().template(), name, ctx) 164 | } 165 | } 166 | 167 | impl Default for Template { 168 | fn default() -> Self { 169 | Template { 170 | body: Default::default(), 171 | content_type: String::from("text/html; charset=utf-8"), 172 | } 173 | } 174 | } 175 | 176 | impl Into for Template { 177 | fn into(self) -> HttpResponse { 178 | HttpResponse::build(StatusCode::OK) 179 | .content_type(&self.content_type[..]) 180 | .body(self.body) 181 | } 182 | } 183 | 184 | impl Responder for Template { 185 | type Item = HttpResponse; 186 | type Error = actix_web::Error; 187 | 188 | fn respond_to( 189 | self, 190 | _req: &HttpRequest, 191 | ) -> actix_web::Result<::Item, ::Error> { 192 | Ok(self.into()) 193 | } 194 | } 195 | --------------------------------------------------------------------------------