Proper welcome modal implementation

rust
Elnu 1 year ago
parent 50dce8da96
commit 6b6c047b68

1
Cargo.lock generated

@ -2512,6 +2512,7 @@ dependencies = [
"rocket_codegen 0.5.0-rc.3",
"rocket_http 0.5.0-rc.3",
"serde",
"serde_json",
"state 0.5.3",
"tempfile",
"time 0.3.21",

@ -14,7 +14,7 @@ dotenv = "0.15.0"
gettext = "0.4.0"
poise = "0.5.5"
reqwest = "0.11.18"
rocket = { version = "=0.5.0-rc.3", features = ["secrets"] }
rocket = { version = "=0.5.0-rc.3", features = ["secrets", "json"] }
rocket_contrib = { version = "0.4.11", features = ["templates"] }
rocket_dyn_templates = { version = "0.1.0-rc.3", features = ["tera"] }
sass-rocket-fairing = "0.2.0"

@ -9,3 +9,4 @@ pub mod user {
pub const USER_AVATAR_COOKIE: &str = "user_avatar";
}
pub const LANG_COOKIE: &str = "lang";
pub const WELCOMED_COOKIE: &str = "welcomed";

@ -42,7 +42,7 @@ async fn rocket() -> _ {
.manage(http)
.mount(
"/",
routes![get_challenge, login, post_login, success, logout, testing],
routes![get_challenge, get_guilds, login, post_login, success, logout, testing],
)
.mount("/css", FileServer::from(relative!("styles/css")))
.mount("/", FileServer::from(relative!("static")).rank(1))

@ -0,0 +1,35 @@
use std::collections::HashMap;
use poise::serenity_prelude::Http;
use rocket::{http::{CookieJar, Status}, State};
use rocket::serde::json::Json;
use crate::{cookies::user::USER_ID_COOKIE, models::Settings};
// TODO: Incrementally send guilds
#[get("/get_guilds")]
pub async fn get_guilds(
cookies: &CookieJar<'_>,
settings: &State<Settings>,
http: &State<Http>,
) -> Result<Json<HashMap<u64, bool>>, Status> {
let user_id: u64 = match cookies.get_private(USER_ID_COOKIE) {
Some(id) => match id.value().parse() {
Ok(id) => id,
Err(_) => return Err(Status::BadRequest),
},
None => return Err(Status::Unauthorized),
};
Ok(Json({
let mut guild_statuses = HashMap::with_capacity(settings.guilds.len());
for guild in &settings.guilds {
guild_statuses.insert(guild.id, {
if guild.hidden {
continue;
}
http.get_member(guild.id, user_id).await.is_ok()
});
}
guild_statuses
}))
}

@ -6,7 +6,7 @@ use rocket::response::Redirect;
pub fn login() -> Redirect {
Redirect::to(format!(
// Switch from response_type=code to response_type=token from URL generator
"https://discord.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=token&scope=identify%20guilds.join%20guilds",
"https://discord.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=token&scope=identify",
client_id = env::var("CLIENT_ID").unwrap(),
redirect_uri = format!("{}success", env::var("DOMAIN").unwrap()),
))

@ -2,6 +2,10 @@
mod _get_challenge;
pub use _get_challenge::get_challenge;
#[path = "get_guilds.rs"]
mod _get_guilds;
pub use _get_guilds::get_guilds;
#[path = "login.rs"]
mod _login;
pub use _login::login;

@ -5,7 +5,7 @@ use rocket::{
response::Redirect,
};
use crate::{cookies::token::TOKEN_EXPIRE_COOKIE, models::User};
use crate::{cookies::{token::TOKEN_EXPIRE_COOKIE, WELCOMED_COOKIE}, models::User};
#[derive(FromForm)]
pub struct Login<'r> {
@ -17,7 +17,7 @@ pub struct Login<'r> {
#[post("/login", data = "<login>")]
pub async fn post_login(login: Form<Login<'_>>, cookies: &CookieJar<'_>) -> Redirect {
if (login.token_type != "Bearer" || login.scope != "guilds.join+identify+guilds")
if (login.token_type != "Bearer" || login.scope.split("+").any(|scope| scope == "identify"))
&& User::init(login.access_token, cookies).await.is_ok()
{
cookies.add(Cookie::new(
@ -26,6 +26,7 @@ pub async fn post_login(login: Form<Login<'_>>, cookies: &CookieJar<'_>) -> Redi
.timestamp()
.to_string(),
));
cookies.remove(Cookie::named(WELCOMED_COOKIE));
}
Redirect::to("/")
}

@ -164,14 +164,20 @@ a {
flex-grow: 1;
align-items: center;
}
a {
.joinButton {
display: inline;
background: seagreen;
color: white;
font: inherit;
font-weight: bold;
border: none;
outline: none;
padding: 0.25em 0.5em 0.25em 0.5em;
border-radius: 2px;
&:disabled {
background: gray;
}
}
}
.recommended {
@ -194,4 +200,9 @@ a {
}
}
}
}
.icon {
height: 1.5em;
vertical-align: middle;
}

@ -9,7 +9,9 @@
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
{% if user %}
{% include "modal" %}
{% endif %}
<div id="content">
<nav>
<a class="link" href="/{{ challenge - 1 }}"><svg class="svg-inline" aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 278.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"></path></svg></a>
@ -26,8 +28,21 @@
<span>{{ user.username }}</span> <img src="https://cdn.discordapp.com/avatars/{{ user.id }}/{{ user.avatar }}.webp?size=1024">
</a>
<nav class="dropdown-content">
<span class="link" onclick="showServers()">
<span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
</svg>
Join servers
</span>
</span>
<a href="/logout" class="link">
<span>Log out</span>
<span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg>
Log out
</span>
</a>
</nav>
</div>
@ -36,7 +51,7 @@
{% endif %}
<div class="dropdown">
<span class="link">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" style="height: 1.5em">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 21l5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 016-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 01-3.827-5.802" />
</svg>
</span>

@ -1,14 +1,16 @@
<dialog>
<h1>Welcome to {{ "title" | i18n(lang=lang) }}!</h1>
<p>In order to participate in challenges, you must be a member of a participating Discord server.</p>
<h2>Join a participating server</h2>
<div class="welcome">
<h1>Welcome to {{ "title" | i18n(lang=lang) }}!</h1>
<p>In order to participate in challenges, you must be a member of a participating Discord server.</p>
<h2>Join a participating server</h2>
</div>
<div class="servers">
{% for guild in settings.guilds %}
{% if guild.hidden or not guild.invite %}{% continue %}{% endif %}
<div{% if guild.recommended %} class="recommended"{% endif %}>
<div id="{{ guild.id }}" {% if guild.recommended %} class="recommended"{% endif %}>
<img src="https://cdn.discordapp.com/icons/{{ guild.id }}/{{ guild.icon }}.webp?size=96" alt="Server icon">
<div class="name">{{ guild.name }}</div>
<a href="https://discord.gg/{{ guild.invite }}">Join</a>
<a href="https://discord.gg/{{ guild.invite }}" class="joinButton">Join</a>
{% if guild.recommended %}
<div class="label-wrapper">
<div class="label">Recommended</div>
@ -19,5 +21,68 @@
</div>
</dialog>
<script>
document.querySelector("dialog").showModal();
const dialog = document.querySelector("dialog");
// Fetch guilds
let guilds = null;
const getGuilds = async () => {
if (guilds === null) {
const response = await fetch("/get_guilds");
const raw_json = await response.text()
guilds = JSON.parse(raw_json);
}
return guilds;
}
// Show server modal, both for welcome and server list
const showServers = async () => {
dialog.showModal();
// If the user is logged in, load the guild list
// and check if they have joined the available servers
const guilds = await getGuilds();
Object.keys(guilds).forEach(id => {
if (guilds[id]) {
const element = document.getElementById(id);
if (element !== null) {
const button = document.createElement("button");
button.tagName = "button";
button.className = "joinButton";
button.disabled = true;
button.innerHTML = "Joined"
element.querySelector("a").replaceWith(button);
}
}
})
};
// Check for welcomed cookie
const welcomed = () => document.cookie.split("; ").includes("welcomed=true");
// Load welcome modal, check if it is necessary
const loadServers = async () => {
if (welcomed()) {
// If already welcomed, remove welcome text
// in case user pressed Join servers button
document.querySelector("dialog .welcome").style.display = "none";
} else {
let is_in_participating_server = false;
const guilds = await getGuilds();
const ids = Object.keys(guilds);
for (let i = 0; i < ids.length; i++) {
if (guilds[ids[i]]) {
is_in_participating_server = true;
break;
}
}
if (!is_in_participating_server) {
showServers();
}
// Even if already in participating server,
// set welcomed cookie so we don't have to run this request again
document.cookie = 'welcomed=true; expires=Tue, 19 Jan 2038 03:14:07 UTC; SameSite=Strict; path=/';
}
};
if (welcomed()) {
// TODO: Remove welcome text via cookie check in template?
document.querySelector("dialog .welcome").style.display = "none";
} else {
// If not welcomed, see if welcome is necessary
loadServers();
}
</script>
Loading…
Cancel
Save