Skip to content

Commit 28ddaa3

Browse files
committed
chore: add meta
1 parent d3e1070 commit 28ddaa3

10 files changed

Lines changed: 241 additions & 31 deletions

File tree

api/src/discord.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,8 @@ pub async fn remove_guild_member_role(guild_id: Id<GuildMarker>, user_id: Id<Use
7272
Ok(())
7373
}
7474

75-
#[cached(time = 60, key = "String", convert = r##"{ format!("{:?}", client.token().unwrap()) }"##)]
75+
#[cached(time = 3600, key = "String", convert = r##"{ format!("{:?}", client.token().unwrap()) }"##)]
7676
pub async fn get_current_user(client: &Client) -> Result<CurrentUser,StatusCode> {
77-
info!("Getting current user...");
7877
retry_on_rl(|| async { client.current_user().await}).await.map_err(as_http_err)?.model().await.map_err(ise)
7978
}
8079

api/src/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod middleware;
1616
mod users;
1717
mod utils;
1818
mod discord;
19+
mod meta;
1920

2021
#[derive(Clone)]
2122
pub struct AppState {
@@ -66,18 +67,19 @@ async fn main() -> Result<(), Error> {
6667
.token(std::env::var("DISCORD_BOT_TOKEN").expect("DISCORD_BOT_TOKEN must be set"))
6768
.build()
6869
);
69-
70+
7071
let app_state = AppState {
7172
dynamo,
7273
discord_bot,
7374
reqwest: Arc::new(reqwest::Client::new()),
7475
};
7576

7677
// guilds::tasks::update_guilds(&app_state.discord_bot, &app_state.dynamo).await;
77-
78+
setup(app_state.discord_bot.clone());
7879
let app = Router::new()
7980
.nest("/users", users::router())
8081
.nest("/guilds", guilds::router())
82+
.nest("/meta", meta::router())
8183
.route_layer(axum::middleware::from_fn(middleware::auth_middleware))
8284
.nest("/interactions", interactions::router())
8385
.route_layer(axum::middleware::from_fn(middleware::log_middleware))
@@ -88,3 +90,8 @@ async fn main() -> Result<(), Error> {
8890

8991
run(app).await
9092
}
93+
94+
95+
fn setup(discord_bot: Arc<twilight_http::Client>) {
96+
meta::setup(discord_bot);
97+
}

api/src/meta/mod.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use std::sync::Arc;
2+
use std::time::Duration;
3+
use axum::{Extension, Json, Router};
4+
use axum::extract::State;
5+
use axum::routing::get;
6+
use http::StatusCode;
7+
use lambda_http::tracing::{error, info};
8+
use serde::{Deserialize, Serialize};
9+
use serde_json::{json, Value};
10+
use tokio::time::sleep;
11+
use tower_http::cors::CorsLayer;
12+
use twilight_http::Client;
13+
use twilight_model::guild::{Guild, Role};
14+
use twilight_model::id::Id;
15+
use twilight_model::id::marker::GuildMarker;
16+
use twilight_model::util::ImageHash;
17+
use crate::AppState;
18+
use crate::discord::{get_current_user_guilds_no_cache, get_current_user_guilds_prime_cache, get_guild, get_guild_prime_cache};
19+
use crate::utils::member_guilds;
20+
21+
pub fn router() -> Router<AppState> {
22+
Router::new()
23+
.route("/guilds", get(get_guilds_meta))
24+
.layer(CorsLayer::permissive())
25+
}
26+
27+
pub fn setup(discord_bot: Arc<Client>) {
28+
info!("Spawning meta cache refresh task");
29+
tokio::spawn(refresh_meta_cache(discord_bot));
30+
}
31+
32+
#[derive(Debug, Serialize, Deserialize)]
33+
struct GuildMeta {
34+
id: Id<GuildMarker>,
35+
name: String,
36+
icon: Option<ImageHash>,
37+
roles: Vec<Role>,
38+
}
39+
40+
impl From<Guild> for GuildMeta {
41+
fn from(guild: Guild) -> Self {
42+
GuildMeta {
43+
id: guild.id,
44+
name: guild.name,
45+
icon: guild.icon,
46+
roles: guild.roles,
47+
}
48+
}
49+
}
50+
51+
async fn get_guilds_meta(
52+
Extension(discord_user): Extension<Arc<Client>>,
53+
State(app_state): State<AppState>,
54+
) -> Result<Json<Value>, StatusCode> {
55+
let member_guilds = member_guilds(&discord_user, &app_state.discord_bot).await?;
56+
57+
let mut meta = vec![];
58+
for member_guild in &member_guilds {
59+
meta.push(GuildMeta::from(get_guild(member_guild.id, &app_state.discord_bot).await?))
60+
}
61+
62+
Ok(Json(json!(meta)))
63+
}
64+
65+
async fn refresh_meta_cache(discord_bot: Arc<Client>) {
66+
loop {
67+
info!("Refreshing meta cache");
68+
match get_current_user_guilds_prime_cache(&*discord_bot).await {
69+
Ok(guilds) => {
70+
info!("Refreshing meta cache: {}", guilds.len());
71+
for guild in guilds {
72+
let _ = get_guild_prime_cache(guild.id, &*discord_bot).await;
73+
}
74+
}
75+
Err(e) => {
76+
error!("refresh_meta_cache error: {:#?}", e);
77+
}
78+
}
79+
sleep(Duration::from_secs(50)).await;
80+
}
81+
}

api/src/utils.rs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use std::collections::HashSet;
21
use twilight_http::{Client};
32
use http::StatusCode;
3+
use twilight_model::guild::Permissions;
44
use twilight_model::id::Id;
55
use twilight_model::id::marker::GuildMarker;
66
use twilight_model::user::{CurrentUser, CurrentUserGuild};
@@ -11,36 +11,42 @@ pub async fn member_guilds(
1111
discord_bot: &Client
1212
) -> Result<Vec<CurrentUserGuild>, StatusCode> {
1313
let bot_guilds = get_current_user_guilds(discord_bot).await?;
14-
let mut user_guilds = get_current_user_guilds(discord_user).await?;
15-
let bot_guild_set = bot_guilds.iter().map(|g| g.id).collect::<HashSet<Id<GuildMarker>>>();
16-
let user_guild_set = user_guilds.iter().map(|g| g.id).collect::<HashSet<Id<GuildMarker>>>();
17-
let mut intersection_set = bot_guild_set.intersection(&user_guild_set);
18-
user_guilds.retain(|g| intersection_set.any(|ig| ig == &g.id));
19-
Ok(user_guilds)
14+
let user_guilds = get_current_user_guilds(discord_user).await?;
15+
let mut guilds = vec![];
16+
for u_guild in &user_guilds {
17+
for b_guild in &bot_guilds {
18+
if u_guild.id == b_guild.id {
19+
guilds.push(b_guild.clone());
20+
continue;
21+
}
22+
}
23+
}
24+
Ok(guilds)
2025
}
2126

2227
pub async fn is_client_admin_guild(guild_id: Id<GuildMarker>, current_user: &CurrentUser, discord_bot: &Client) -> Result<bool, StatusCode> {
2328
let guild = get_guild(guild_id, discord_bot).await?;
2429
let member = get_guild_member(guild_id, current_user.id, discord_bot).await?;
25-
Ok(current_user.id == guild.owner_id || guild.roles.iter().any(|r| member.roles.contains(&r.id) && r.permissions & twilight_model::guild::Permissions::ADMINISTRATOR == twilight_model::guild::Permissions::ADMINISTRATOR))
30+
Ok(current_user.id == guild.owner_id || guild.roles.iter().any(|r| member.roles.contains(&r.id) && r.permissions & Permissions::ADMINISTRATOR == Permissions::ADMINISTRATOR))
2631
}
2732

2833
pub async fn admin_guilds(
2934
discord_user: &Client,
3035
discord_bot: &Client
3136
) -> Result<Vec<CurrentUserGuild>, StatusCode> {
3237
let bot_guilds = get_current_user_guilds(discord_bot).await?;
33-
let mut user_guilds = get_current_user_guilds(discord_user).await?;
34-
let bot_guild_set = bot_guilds.iter().map(|g| g.id).collect::<HashSet<Id<GuildMarker>>>();
35-
let user_guild_set = user_guilds.iter().map(|g| g.id).collect::<HashSet<Id<GuildMarker>>>();
36-
let mut intersection_set = bot_guild_set.intersection(&user_guild_set);
37-
user_guilds.retain(|g| intersection_set.any(|ig| ig == &g.id));
38-
39-
let mut admin_guilds = vec![];
40-
for partial_guild in user_guilds {
41-
if partial_guild.owner || partial_guild.permissions & twilight_model::guild::Permissions::ADMINISTRATOR == twilight_model::guild::Permissions::ADMINISTRATOR {
42-
admin_guilds.push(partial_guild);
38+
let user_guilds = get_current_user_guilds(discord_user).await?;
39+
let mut guilds = vec![];
40+
for u_guild in &user_guilds {
41+
for b_guild in &bot_guilds {
42+
if u_guild.id == b_guild.id {
43+
if b_guild.permissions & Permissions::ADMINISTRATOR == Permissions::ADMINISTRATOR
44+
&& (u_guild.owner || u_guild.permissions & Permissions::ADMINISTRATOR == Permissions::ADMINISTRATOR) {
45+
guilds.push(u_guild.clone());
46+
}
47+
continue;
48+
}
4349
}
4450
}
45-
Ok(admin_guilds)
51+
Ok(guilds)
4652
}

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 guilds'
24+
'identify email guilds guilds.members.read'
2525
)
2626
const user = toRef(null)
2727
const modalActiveRef = ref(false)

ui/src/helpers/discord.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {DscCurrentGuild} from "../stores/discord.js";
2+
3+
export async function getCurrentUserGuildMetadata(token) {
4+
let userGuilds = await DscCurrentGuild.fetchCurrentUserGuilds(token);
5+
return userGuilds.reduce((acc, guild) => acc.set(guild.id, guild), new Map());
6+
}
7+
8+
export function toAdminCurrentUserGuilds(userGuildMetaMap) {
9+
let adminGuilds = new Map();
10+
for (let guild of userGuildMetaMap.values()) {
11+
if (isGuildAdmin(guild)) {
12+
adminGuilds.set(guild.id, guild);
13+
}
14+
}
15+
return adminGuilds;
16+
}
17+
export function isGuildAdmin(userGuild) {
18+
return userGuild.owner || (userGuild.permissions & 0x8) === 0x8;
19+
}
20+

ui/src/helpers/meta.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {GuildMeta} from "../stores/meta.js";
2+
import {isGuildAdmin} from "./discord.js";
3+
4+
5+
export async function fetchGuildMetaMap(token) {
6+
return (await GuildMeta.fetchAll(token)).reduce((acc, guildMeta) => {
7+
guildMeta.roleMeta = guildMeta.roles.reduce((acc, role) => acc.set(role.id, role), new Map());
8+
return acc.set(guildMeta.id, guildMeta);
9+
}, new Map());
10+
}
11+
12+
export function filterByAdmin(guildMetaMap, currentUserGuildMetaMap) {
13+
let adminGuilds = new Map();
14+
for (let guildMeta of guildMetaMap.values()) {
15+
if (isGuildAdmin(currentUserGuildMetaMap.get(guildMeta.id))) {
16+
adminGuilds.set(guildMeta.id, guildMeta);
17+
}
18+
}
19+
return adminGuilds
20+
}

ui/src/pages/dashboard/DashBaseView.vue

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import DiscordAuthButton from "../../components/auth/DiscordAuthButton.vue";
77
import MainWithFooter from "../../components/MainWithFooter.vue";
88
import {Guild} from "../../stores/guild.js";
99
import {INVITE_URL} from "../../helpers/redirect.js";
10+
import {getCurrentUserGuildMetadata, isGuildAdmin, toAdminCurrentUserGuilds} from "../../helpers/discord.js";
11+
import {User} from "../../stores/user.js";
12+
import {fetchGuildMetaMap, filterByAdmin} from "../../helpers/meta.js";
1013
1114
const currentPath = ref(window.location.pathname)
1215
@@ -36,9 +39,17 @@ function reviver(key, value) {
3639
return value;
3740
}
3841
42+
let user = ref(User.loadCache());
43+
let guildMetaMap = ref(new Map());
44+
3945
let guildsKb = ref(new Map());
4046
let currentGuildId = ref(null);
4147
48+
async function loadMetadata() {
49+
guildMetaMap.value = filterByAdmin(await fetchGuildMetaMap(user.value.token.accessToken),
50+
await getCurrentUserGuildMetadata(user.value.token.accessToken));
51+
}
52+
4253
function loadMemGuilds() {
4354
let memGuilds = JSON.parse(localStorage.getItem('guilds'), reviver);
4455
if (memGuilds === null) return;
@@ -49,14 +60,15 @@ function loadMemGuilds() {
4960
if (guildsKb.value.size > 0) {
5061
guildsLoaded.value = true;
5162
}
52-
currentGuildId.value = JSON.parse(localStorage.getItem('currentGuildId')) || null;
63+
currentGuildId.value = JSON.parse(localStorage.getItem('currentGuildId')|| null) ;
5364
}
5465
5566
onMounted(async () => {
67+
await loadMetadata();
5668
loadMemGuilds();
5769
// Load remaining guilds
70+
// await getAdminDscGuilds();
5871
await sync_guilds_kb();
59-
guildsLoaded.value = true;
6072
})
6173
6274
async function setCurrentGuild(gid) {
@@ -101,20 +113,22 @@ async function sync_guilds_kb() {
101113
<div tabindex="0" role="button" class="card-title btn btn-sm btn-ghost" v-if="currentGuildId">
102114
<div class="avatar">
103115
<div class="w-6 rounded-xl">
104-
<img :src="`https://cdn.discordapp.com/icons/${currentGuildId}/${guildsKb.get(currentGuildId).icon}.webp`" v-if="guildsKb.get(currentGuildId).icon"/>
116+
<img :src="`https://cdn.discordapp.com/icons/${currentGuildId}/${guildMetaMap.get(currentGuildId).icon}.webp`" v-if="guildMetaMap.get(currentGuildId) && guildMetaMap.get(currentGuildId).icon"/>
105117
</div>
106118
</div>
107-
{{ guildsKb.get(currentGuildId).name }}
119+
{{ guildMetaMap.get(currentGuildId).name }}
108120
</div>
109121
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-1 p-2 shadow-sm">
110122
<li v-if="!guildsLoaded">
111123
<a class="justify-between">
112124
<span>Loading...</span>
113125
</a>
114126
</li>
115-
<li v-for="[gid, guild] in guildsKb"><a :class="(gid === currentGuildId && 'menu-active')" @click="setCurrentGuild(gid)">
116-
<div class="w-6 rounded-xl"><img :src="`https://cdn.discordapp.com/icons/${gid}/${guildsKb.get(gid).icon}.webp`" v-if="guildsKb.has(gid) && guildsKb.get(gid).icon"/>
117-
</div> {{ guildsKb.get(gid).name }}</a></li>
127+
<li v-for="[gid, guild] in guildMetaMap">
128+
<a :class="(gid === currentGuildId && 'menu-active')" @click="setCurrentGuild(guild.id)">
129+
<div class="w-6 rounded-xl"><img :src="`https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp`" v-if="guild.icon"/>
130+
</div> {{ guild.name }}</a>
131+
</li>
118132
<li class="bg-primary text-primary-content">
119133
<a class="justify-between" :href="INVITE_URL">
120134
<span>+ Add Server</span>

ui/src/stores/discord.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import axios from "axios";
2+
3+
export class DscCurrentGuild {
4+
constructor(id, name, icon, owner, permissions) {
5+
this.id = id;
6+
this.name = name;
7+
this.icon = icon;
8+
this.owner = owner;
9+
this.permissions = permissions;
10+
}
11+
12+
static fromJson(json) {
13+
return new DscCurrentGuild(json['id'], json['name'], json['icon'], json['owner'], json['permissions']);
14+
}
15+
16+
static async fetch(guildId, token) {
17+
let r = await axios.get(`https://discord.com/api/v10/guilds/${guildId}`,
18+
{ headers: { 'Authorization': 'Bearer ' + token } });
19+
return DscCurrentGuild.fromJson(r.data)
20+
}
21+
22+
static async fetchCurrentUserGuilds(token) {
23+
let r = await axios.get(`https://discord.com/api/v10/users/@me/guilds`,
24+
{ headers: { 'Authorization': 'Bearer ' + token } });
25+
26+
return r.data.map(DscCurrentGuild.fromJson)
27+
}
28+
}

ui/src/stores/meta.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import axios from "axios";
2+
3+
const VITE_KB_API_URL = import.meta.env.VITE_KB_API_URL
4+
5+
6+
export class RoleMeta {
7+
constructor(id, name, permissions) {
8+
this.id = id;
9+
this.name = name;
10+
this.permissions = permissions;
11+
}
12+
13+
static fromJson(json) {
14+
return new RoleMeta(json['id'], json['name'], json['permissions']);
15+
}
16+
}
17+
18+
export class GuildMeta {
19+
constructor(id, name, icon, roles) {
20+
this.id = id;
21+
this.name = name;
22+
this.icon = icon;
23+
this.roles = roles;
24+
}
25+
26+
static fromJson(json) {
27+
return new GuildMeta(json['id'], json['name'], json['icon'], json['roles'].map(RoleMeta.fromJson));
28+
}
29+
30+
static async fetchAll(token) {
31+
let r = await axios.get(`${VITE_KB_API_URL}/meta/guilds`,
32+
{ headers: { 'Authorization': 'Discord ' + token } });
33+
return r.data.map(GuildMeta.fromJson);
34+
}
35+
}

0 commit comments

Comments
 (0)