Skip to content

Commit bb9158c

Browse files
committed
feat: guild selection
1 parent 3ad6fbe commit bb9158c

File tree

9 files changed

+310
-24
lines changed

9 files changed

+310
-24
lines changed

api/src/dynamo.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,13 @@ pub fn as_string_vec(val: Option<&AttributeValue>) -> Vec<String> {
5252
}
5353

5454
pub fn as_map_vec(val: Option<&AttributeValue>) -> Vec<&HashMap<String, AttributeValue>> {
55+
const MAX_LIST_SIZE: usize = 1000; // Prevent stack overflow from huge lists
5556
if let Some(val) = val {
5657
if let Ok(val) = val.as_l() {
58+
if val.len() > MAX_LIST_SIZE {
59+
lambda_http::tracing::error!("DynamoDB list too large: {} items", val.len());
60+
return vec![];
61+
}
5762
return val
5863
.iter()
5964
.filter_map(|v| v.as_m().ok())
@@ -71,4 +76,13 @@ pub fn as_bool(val: Option<&AttributeValue>, default: bool) -> bool {
7176
}
7277
}
7378
default
79+
}
80+
81+
pub fn as_map(val: Option<&AttributeValue>) -> Option<&HashMap<String, AttributeValue>> {
82+
if let Some(val) = val {
83+
if let Ok(val) = val.as_m() {
84+
return Some(val);
85+
}
86+
}
87+
None
7488
}

api/src/guilds/mod.rs

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use lambda_http::tracing::warn;
88
use twilight_model::guild::Permissions;
99
use twilight_model::id::marker::GuildMarker;
1010
use twilight_model::id::Id;
11+
use crate::guilds::models::{Guild, Verify};
1112

1213
pub mod verify;
1314
mod models;
@@ -22,19 +23,36 @@ pub fn router() -> Router<AppState> {
2223
}
2324

2425
async fn get_guilds(
25-
) -> Json<Value> {
26-
todo!()
27-
}
28-
29-
30-
async fn get_guilds_id(
31-
Path(guild_id): Path<Id<GuildMarker>>,
3226
Extension(discord_user): Extension<Arc<twilight_http::Client>>,
3327
State(app_state): State<AppState>
3428
) -> Result<Json<Value>, StatusCode> {
29+
let user_guilds = discord_user.current_user_guilds().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?.models().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
30+
let user_guilds_filtered = user_guilds.iter().filter(|g| g.permissions.contains(Permissions::ADMINISTRATOR)).collect::<Vec<_>>();
31+
let mut guilds = Vec::new();
32+
for guild in user_guilds_filtered {
33+
let db_guild = Guild::from_db(&guild.id.to_string(), &app_state.dynamo).await;
34+
if let Some(g) = db_guild {
35+
guilds.push(g);
36+
} else {
37+
// If the guild is not in the database, create a new one with default values
38+
let new_guild = Guild {
39+
guild_id: guild.id.to_string(),
40+
verify: Verify { roles: vec![] },
41+
};
42+
new_guild.save(&app_state.dynamo).await;
43+
guilds.push(new_guild);
44+
}
45+
}
46+
Ok(Json(json!(guilds)))
47+
}
48+
49+
async fn verify_user_admin(
50+
guild_id: &Id<GuildMarker>,
51+
discord_user: &Arc<twilight_http::Client>,
52+
app_state: &AppState) -> Result<(), StatusCode> {
3553
let logged_in_user = discord_user.current_user().await.unwrap().model().await.unwrap();
3654

37-
let guild = match app_state.discord_bot.guild(guild_id).await {
55+
let guild = match app_state.discord_bot.guild(*guild_id).await {
3856
Ok(g) => g.model().await.unwrap(),
3957
Err(e) => {
4058
warn!("Error fetching guild: {:?}", e);
@@ -45,7 +63,7 @@ async fn get_guilds_id(
4563
let user_is_owner = guild.owner_id == logged_in_user.id;
4664

4765
if !user_is_owner {
48-
let member = match app_state.discord_bot.guild_member(guild_id, logged_in_user.id).await {
66+
let member = match app_state.discord_bot.guild_member(*guild_id, logged_in_user.id).await {
4967
Ok(gm) => gm.model().await.unwrap(),
5068
Err(e) => {
5169
warn!("Error fetching guild member: {:?}", e);
@@ -60,6 +78,27 @@ async fn get_guilds_id(
6078
return Err(StatusCode::NOT_FOUND);
6179
}
6280
}
81+
Ok(())
82+
}
6383

64-
Ok(Json(json!(guild)))
84+
async fn get_guilds_id(
85+
Path(guild_id): Path<Id<GuildMarker>>,
86+
Extension(discord_user): Extension<Arc<twilight_http::Client>>,
87+
State(app_state): State<AppState>
88+
) -> Result<Json<Value>, StatusCode> {
89+
verify_user_admin(&guild_id, &discord_user, &app_state).await?;
90+
91+
let guild = Guild::from_db(&guild_id.to_string(), &app_state.dynamo).await;
92+
93+
Ok(Json(json!(match guild {
94+
Some(g) => g,
95+
None => {
96+
let new_guild = Guild {
97+
guild_id: guild_id.to_string(),
98+
verify: Verify { roles: vec![] },
99+
};
100+
new_guild.save(&app_state.dynamo).await;
101+
new_guild
102+
}
103+
})))
65104
}

api/src/guilds/models.rs

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,123 @@
1+
use std::collections::HashMap;
2+
use aws_sdk_dynamodb::types::AttributeValue;
3+
use http::StatusCode;
4+
use lambda_http::tracing::{error, info};
15
use serde::{Deserialize, Serialize};
6+
use crate::dynamo::{as_map, as_map_vec, as_string};
7+
8+
#[derive(Clone, Serialize, Deserialize)]
9+
pub struct VerifyRole {
10+
pub role_id: String,
11+
pub role_name: String,
12+
pub pattern: String,
13+
pub member_count: u32,
14+
}
15+
16+
#[derive(Clone, Serialize, Deserialize)]
17+
pub struct Verify {
18+
pub roles: Vec<VerifyRole>
19+
}
220

321
#[derive(Clone, Serialize, Deserialize)]
422
pub struct Guild {
5-
guild_id: String,
6-
verify: String
23+
pub guild_id: String,
24+
pub verify: Verify
25+
}
26+
27+
impl From<&HashMap<String, AttributeValue>> for Guild {
28+
fn from(item: &HashMap<String, AttributeValue>) -> Self {
29+
Guild {
30+
guild_id: as_string(item.get("guild_id"), &"".to_string()),
31+
verify: as_map(item.get("verify")).map(
32+
|m| Verify {
33+
roles: as_map_vec(m.get("roles"))
34+
.into_iter()
35+
.map(|role_map| VerifyRole {
36+
role_id: as_string(role_map.get("role_id"), &"".to_string()),
37+
role_name: as_string(role_map.get("role_name"), &"".to_string()),
38+
pattern: as_string(role_map.get("pattern"), &"".to_string()),
39+
member_count: role_map.get("member_count")
40+
.and_then(|v| v.as_n().ok())
41+
.and_then(|n| n.parse::<u32>().ok())
42+
.unwrap_or(0),
43+
})
44+
.collect(),
45+
}).unwrap_or(Verify { roles: vec![] }),
46+
}
47+
}
48+
}
49+
50+
impl Into<HashMap<String, AttributeValue>> for Guild {
51+
fn into(self) -> HashMap<String, AttributeValue> {
52+
let mut item = HashMap::new();
53+
item.insert("guild_id".to_string(), AttributeValue::S(self.guild_id));
54+
55+
let roles: Vec<AttributeValue> = self.verify.roles.into_iter().map(|role| {
56+
let mut role_map = HashMap::new();
57+
role_map.insert("role_id".to_string(), AttributeValue::S(role.role_id));
58+
role_map.insert("role_name".to_string(), AttributeValue::S(role.role_name));
59+
role_map.insert("pattern".to_string(), AttributeValue::S(role.pattern));
60+
role_map.insert("member_count".to_string(), AttributeValue::N(role.member_count.to_string()));
61+
AttributeValue::M(role_map)
62+
}).collect();
63+
64+
item.insert("verify".to_string(), AttributeValue::M(HashMap::from([("roles".to_string(), AttributeValue::L(roles))])));
65+
66+
item
67+
}
68+
}
69+
70+
impl Guild {
71+
pub async fn from_db(guild_id: &str, dynamo: &aws_sdk_dynamodb::Client) -> Option<Guild> {
72+
info!("a");
73+
let a = dynamo.query().table_name(format!("kb2_guilds_{}",std::env::var("DEPLOYMENT_ENV").expect("DEPLOYMENT_ENV must be set"),));
74+
info!("b");
75+
let b = a.key_condition_expression("#uid = :uid");
76+
info!("c");
77+
let c = b.expression_attribute_names("#uid", "guild_id");
78+
info!("d");
79+
let d = c.expression_attribute_values(":uid", AttributeValue::S(guild_id.to_string()));
80+
info!("e");
81+
let e = d.send();
82+
info!("f");
83+
let f = e.await;
84+
info!("g");
85+
86+
match f
87+
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
88+
.and_then(|resp| {
89+
info!("100");
90+
let items = resp.items.unwrap_or_default();
91+
info!("200");
92+
if items.is_empty() {
93+
return Err(StatusCode::NOT_FOUND);
94+
}
95+
let guild: Guild = (&items[0]).into();
96+
Ok(guild)
97+
}) {
98+
Ok(guild) => Some(guild),
99+
Err(e) => {
100+
error!("Error fetching guild from DynamoDB: {}", e);
101+
None
102+
}
103+
}
104+
105+
}
106+
107+
pub async fn save(&self, dynamo: &aws_sdk_dynamodb::Client) {
108+
109+
match dynamo
110+
.put_item()
111+
.table_name(format!("kb2_guilds_{}", std::env::var("DEPLOYMENT_ENV").expect("DEPLOYMENT_ENV must be set")))
112+
.set_item(Some(self.clone().into()))
113+
.send()
114+
.await
115+
{
116+
Ok(_) => (),
117+
Err(e) => {
118+
error!("DynamoDB write error: {}", e);
119+
panic!("Failed to save guild to DynamoDB");
120+
}
121+
}
122+
}
7123
}

ui/src/components/auth/DiscordAuthButton.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const authFlow = new AuthorizationFlowPKCE(
2121
"",
2222
"/auth/discord/callback",
2323
"https://discord.com/api/v10/oauth2/token",
24-
'identify email'
24+
'identify email guilds'
2525
)
2626
2727
const userRef = ref(getUser())

ui/src/helpers/discordapi.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import axios from "axios";
2+
import {getUser} from "../stores/auth.js";
3+
4+
let user = getUser();
5+
6+
export async function getUserAdminGuilds() {
7+
let guilds = (await axios.get('https://discord.com/api/v10/users/@me/guilds', {
8+
headers: {
9+
'Authorization': 'Bearer ' + user.token.accessToken
10+
}
11+
})).data
12+
//convert to map of id to obj
13+
return guilds.filter(g => (Number(g.permissions) & (1 << 3)) === (1 << 3))
14+
.reduce((acc, g) => {
15+
acc[g.id] = g;
16+
return acc;
17+
}, {});
18+
}

ui/src/helpers/kbguild.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import axios from "axios";
2+
import {getUser} from "../stores/auth.js";
3+
4+
const VITE_KB_API_URL = import.meta.env.VITE_KB_API_URL
5+
6+
let user = getUser();
7+
8+
export async function getGuilds() {
9+
let resp = await axios.get(`${VITE_KB_API_URL}/guilds`, {
10+
headers: {
11+
'Authorization': 'Discord ' + user.token.accessToken
12+
}
13+
});
14+
return resp.data;
15+
}
16+
17+
export async function getGuild(guild_id) {
18+
return (await axios.get(`${VITE_KB_API_URL}/guilds/${guild_id}`, {
19+
headers: {
20+
'Authorization': 'Discord ' + user.token.accessToken
21+
}
22+
})).data;
23+
}

ui/src/helpers/redirect.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
const { VITE_DISCORD_CLIENT_ID } = import.meta.env;
2+
3+
export const INVITE_URL = `https://discord.com/api/oauth2/authorize?client_id=${VITE_DISCORD_CLIENT_ID}&permissions=8&scope=bot%20applications.commands`;
4+
15
export function formatInternalRedirect(path) {
26
return window.location.protocol + '//' + window.location.host + path
37
}

ui/src/pages/HomeView.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import KoalaMonoIcon from "../components/icons/KoalaMonoIcon.vue";
44
import ThemeToggle from "../components/ThemeToggle.vue";
55
import SwanseaIcon from "../components/icons/SwanseaIcon.vue";
6-
import {redirectTo} from "../helpers/redirect.js";
6+
import {INVITE_URL, redirectTo} from "../helpers/redirect.js";
77
import MainWithFooter from "../components/MainWithFooter.vue";
88
99
</script>
@@ -40,7 +40,7 @@ import MainWithFooter from "../components/MainWithFooter.vue";
4040
<div class="card-body flex flex-col justify-center h-max">
4141
<h2 class="card-title justify-center">For Admins</h2>
4242
<div class="card-actions join flex flex-row mx-5 justify-between">
43-
<button class="btn btn-primary join-item w-full" @click="redirectTo('https://discord.com/oauth2/authorize?client_id=1014995724888444998&permissions=0&integration_type=0&scope=bot')">+ Add to Discord</button>
43+
<button class="btn btn-primary join-item w-full" @click="redirectTo(INVITE_URL)">+ Add to Discord</button>
4444
<button class="btn btn-secondary join-item w-full" @click="redirectTo('/dashboard')">My Dashboard</button>
4545
</div>
4646
</div>

0 commit comments

Comments
 (0)