Skip to content

Commit eca81b3

Browse files
committed
pagination, regex tweaks & other minor changes
1 parent afa6d48 commit eca81b3

5 files changed

Lines changed: 192 additions & 44 deletions

File tree

src/commands/mod.rs

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ pub(crate) const ACCENT_COLOUR: Colour = Colour(0x8957e5);
55
pub(crate) const OK_COLOUR: Colour = Colour(0x2ecc71);
66
pub(crate) const ERROR_COLOUR: Colour = Colour(0xe74c3c);
77

8-
use serenity::model::Colour;
9-
108
use crate::{Context, Error};
119

12-
use poise::serenity_prelude::{self as serenity, CreateEmbed};
10+
use poise::serenity_prelude::{
11+
self as serenity, Colour, ComponentInteractionCollector, CreateActionRow, CreateButton,
12+
CreateEmbed, CreateEmbedFooter, CreateInteractionResponse, CreateInteractionResponseMessage,
13+
};
14+
use poise::CreateReply;
1315

1416
#[poise::command(prefix_command, hide_in_help)]
1517
pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
@@ -46,3 +48,105 @@ pub async fn respond_err(ctx: &Context<'_>, title: &str, content: &str) {
4648

4749
respond_embed(ctx, embed, false).await;
4850
}
51+
52+
pub async fn paginate_lists<U, E>(
53+
ctx: poise::Context<'_, U, E>,
54+
pages: &[Vec<(String, String, bool)>],
55+
embed_title: &str,
56+
) -> Result<(), Error> {
57+
let ctx_id = ctx.id();
58+
let prev_button_id = format!("{}prev", ctx_id);
59+
let next_button_id = format!("{}next", ctx_id);
60+
61+
let colour = Colour::TEAL;
62+
63+
let components = CreateActionRow::Buttons(vec![
64+
CreateButton::new(&prev_button_id).emoji('◀'),
65+
CreateButton::new(&next_button_id).emoji('▶'),
66+
]);
67+
let mut current_page = 0;
68+
69+
// Don't paginate if its one page.
70+
let reply = if pages.len() > 1 {
71+
CreateReply::default()
72+
.embed(
73+
CreateEmbed::default()
74+
.title(embed_title)
75+
.fields(pages[current_page].clone())
76+
.colour(colour)
77+
.footer(CreateEmbedFooter::new(format!(
78+
"Page: {}/{}",
79+
current_page + 1,
80+
pages.len()
81+
))),
82+
)
83+
.components(vec![components])
84+
} else {
85+
CreateReply::default().embed(
86+
CreateEmbed::default()
87+
.title(embed_title)
88+
.colour(colour)
89+
.fields(pages[current_page].clone()),
90+
)
91+
};
92+
93+
let msg = ctx.send(reply).await?;
94+
95+
if pages.len() > 1 {
96+
while let Some(press) = ComponentInteractionCollector::new(ctx)
97+
.filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string()))
98+
.timeout(std::time::Duration::from_secs(180))
99+
.await
100+
{
101+
if press.data.custom_id == next_button_id {
102+
current_page += 1;
103+
if current_page >= pages.len() {
104+
current_page = 0;
105+
}
106+
} else if press.data.custom_id == prev_button_id {
107+
current_page = current_page.checked_sub(1).unwrap_or(pages.len() - 1);
108+
} else {
109+
continue;
110+
}
111+
112+
press
113+
.create_response(
114+
ctx.serenity_context(),
115+
CreateInteractionResponse::UpdateMessage(
116+
CreateInteractionResponseMessage::new().embed(
117+
serenity::CreateEmbed::new()
118+
.title(embed_title)
119+
.colour(colour)
120+
.fields(pages[current_page].clone())
121+
.footer(CreateEmbedFooter::new(format!(
122+
"Page: {}/{}",
123+
current_page + 1,
124+
pages.len()
125+
))),
126+
),
127+
),
128+
)
129+
.await?;
130+
}
131+
// Remove components after timeout.
132+
msg.edit(
133+
ctx,
134+
poise::CreateReply::default()
135+
.embed(
136+
serenity::CreateEmbed::default()
137+
.title(embed_title)
138+
.colour(colour)
139+
.fields(pages[current_page].clone())
140+
.footer(CreateEmbedFooter::new(format!(
141+
"Page: {}/{}",
142+
current_page + 1,
143+
pages.len()
144+
))),
145+
)
146+
.components(vec![]),
147+
)
148+
.await?;
149+
}
150+
151+
Ok(())
152+
}

src/commands/snippets.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
Context, Error,
55
};
66
use ::serenity::futures::{Stream, StreamExt};
7-
use poise::serenity_prelude::{futures, Colour, CreateAttachment, CreateEmbed};
7+
use poise::serenity_prelude::{futures, CreateAttachment, CreateEmbed};
88

99
async fn autocomplete_snippet<'a>(
1010
ctx: Context<'a>,
@@ -142,11 +142,11 @@ pub async fn edit_snippet(
142142
Ok(())
143143
}
144144

145-
/// Delete snippet
145+
/// Removes a snippet
146146
///
147147
/// Must use the full formatted snippet name (id: title)
148-
#[poise::command(rename = "delete-snippet", slash_command, guild_only)]
149-
pub async fn delete_snippet(
148+
#[poise::command(rename = "remove-snippet", slash_command, guild_only)]
149+
pub async fn remove_snippet(
150150
ctx: Context<'_>,
151151
#[autocomplete = "autocomplete_snippet"]
152152
#[description = "The snippet's id"]
@@ -172,6 +172,7 @@ pub async fn delete_snippet(
172172
/// Lists all snippets
173173
#[poise::command(
174174
rename = "list-snippets",
175+
aliases("list-snippet", "snippets"),
175176
slash_command,
176177
prefix_command,
177178
guild_only,
@@ -180,15 +181,25 @@ pub async fn delete_snippet(
180181
pub async fn list_snippets(ctx: Context<'_>) -> Result<(), Error> {
181182
let snippets = { ctx.data().state.read().unwrap().snippets.clone() };
182183

183-
let mut embed = CreateEmbed::default().title("Snippets").color(Colour::TEAL);
184-
185-
// fields are limited to 25 max, we can't display more than 25 snippets in the snippets command
186-
// due to a discord limitation.
187-
for snippet in snippets.iter().take(25) {
188-
embed = embed.field(format!("`{}`", snippet.id), &snippet.title, false);
184+
if snippets.is_empty() {
185+
respond_err(
186+
&ctx,
187+
"Cannot send list of snippets",
188+
"There are no snippets to list!",
189+
)
190+
.await;
191+
return Ok(());
189192
}
190193

191-
ctx.send(poise::CreateReply::default().embed(embed)).await?;
194+
let pages: Vec<Vec<(String, String, bool)>> = snippets
195+
.iter()
196+
.map(|snippet| (snippet.id.clone(), snippet.title.clone(), true))
197+
.collect::<Vec<(String, String, bool)>>()
198+
.chunks(25)
199+
.map(|chunk| chunk.to_vec())
200+
.collect();
201+
202+
super::paginate_lists(ctx, &pages, "Snippets").await?;
192203

193204
Ok(())
194205
}

src/commands/utils.rs

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -255,30 +255,30 @@ pub async fn edit_embed(
255255
Ok(())
256256
}
257257

258-
/// Adds an issue token
259-
#[poise::command(rename = "add-issue-token", slash_command, guild_only)]
260-
pub async fn add_issue_token(
258+
/// Adds a repository token
259+
#[poise::command(rename = "add-repository", slash_command, guild_only)]
260+
pub async fn add_repo(
261261
ctx: Context<'_>,
262-
#[description = "The key to the issue token in a lowercase alphabetic string"] key: String,
262+
#[description = "The key to the repository in a lowercase alphabetic string"] key: String,
263263
#[description = "The owner of the repository."] owner: String,
264264
#[description = "The respository name."] repository: String,
265265
) -> Result<(), Error> {
266266
let key_regex = Regex::new(r"[a-z+]+$").unwrap();
267-
let repo_details_regex = Regex::new(r"^[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?$").unwrap();
267+
let repo_details_regex = Regex::new(r"^([a-zA-Z0-9-_]+)*$").unwrap();
268268
if !key_regex.is_match(&key) {
269269
respond_err(
270270
&ctx,
271-
"Issue token parsing error",
272-
"The key is limited to lowercase letters only.",
271+
"Key parsing error",
272+
"The key can only lowercase ASCII letters, digits, and the characters ., -, and _.",
273273
)
274274
.await;
275275
return Ok(());
276276
}
277-
if !repo_details_regex.is_match(&key) || !repo_details_regex.is_match(&repository) {
277+
if !repo_details_regex.is_match(&owner) || !repo_details_regex.is_match(&repository) {
278278
respond_err(
279279
&ctx,
280-
"Issue token parsing error",
281-
"Your inputs for owner and repository name must be valid.",
280+
"Repository details parsing error",
281+
"Your inputs for owner and repository name must be valid repository names.",
282282
)
283283
.await;
284284
return Ok(());
@@ -291,10 +291,14 @@ pub async fn add_issue_token(
291291
name: repository.clone(),
292292
};
293293

294-
rwlock_guard.issue_prefixes.insert(key.clone(), details);
294+
rwlock_guard
295+
.issue_prefixes
296+
.insert(key.clone().to_lowercase(), details);
295297
println!(
296-
"Successfully added issue token {} for **{}/{}**",
297-
key, owner, repository
298+
"Successfully added repository {} for **{}/{}**",
299+
key.to_lowercase(),
300+
owner,
301+
repository
298302
);
299303
rwlock_guard.write();
300304
};
@@ -309,28 +313,28 @@ pub async fn add_issue_token(
309313
Ok(())
310314
}
311315

312-
/// Removes an issue token.
313-
#[poise::command(rename = "remove-issue-token", slash_command, guild_only)]
314-
pub async fn remove_issue_token(
316+
/// Removes a repository
317+
#[poise::command(rename = "remove-repository", slash_command, guild_only)]
318+
pub async fn remove_repo(
315319
ctx: Context<'_>,
316320
#[autocomplete = "autocomplete_key"]
317-
#[description = "The issue token key."]
321+
#[description = "The repository key."]
318322
key: String,
319323
) -> Result<(), Error> {
320324
// I know we could just do rm_repo, but that doesn't return a result.
321325
// I may change this in the future, but before I do that I'll probably
322326
// impl a solution directly into the types?
323327

324328
// not sure why I have to do this, it won't settle otherwise.
325-
let key_str = format!("The issue token with the key '{}' has been removed", key);
329+
let key_str = format!("The repository with the key '{}' has been removed", key);
326330
match get_repo_details(&ctx, &key).await {
327331
Some(_) => {
328332
rm_repo(&ctx, &key).await;
329333

330-
respond_ok(&ctx, "Successfully removed token!", &key_str).await;
334+
respond_ok(&ctx, "Successfully removed repository!", &key_str).await;
331335
}
332336
None => {
333-
let title = "Failure to find issue token";
337+
let title = "Failure to find repository";
334338
let content = format!("The key '{}' does not exist.", key);
335339
respond_err(&ctx, title, &content).await;
336340
}
@@ -339,24 +343,51 @@ pub async fn remove_issue_token(
339343
Ok(())
340344
}
341345

342-
/// Lists all snippets
346+
/// Lists all repositories
343347
#[poise::command(
344-
rename = "list-tokens",
348+
rename = "list-repositories",
349+
aliases("repos-list", "list-repos", "repos"),
345350
slash_command,
346351
prefix_command,
347352
guild_only,
348353
track_edits
349354
)]
350-
pub async fn list_tokens(ctx: Context<'_>) -> Result<(), Error> {
355+
pub async fn list_repos(ctx: Context<'_>) -> Result<(), Error> {
351356
let tokens = { ctx.data().state.read().unwrap().issue_prefixes.clone() };
352357

358+
if tokens.is_empty() {
359+
respond_err(
360+
&ctx,
361+
"Cannot send list of repositories",
362+
"There are no repositories to list!",
363+
)
364+
.await;
365+
return Ok(());
366+
}
367+
368+
let pages: Vec<Vec<(String, String, bool)>> = tokens
369+
.iter()
370+
.map(|token| {
371+
(
372+
token.0.clone(),
373+
format!("{}/{}", token.1.name, token.1.owner),
374+
true,
375+
)
376+
})
377+
.collect::<Vec<(String, String, bool)>>()
378+
.chunks(25)
379+
.map(|chunk| chunk.to_vec())
380+
.collect();
381+
382+
super::paginate_lists(ctx, &pages, "Repositories").await?;
383+
353384
let mut embed = CreateEmbed::default()
354385
.title("Issue tokens")
355386
.color(Colour::TEAL);
356387

357388
// fields are limited to 25 max, we can't display more than 25 snippets in the snippets command
358389
// due to a discord limitation.
359-
for token in tokens.iter().take(25) {
390+
for token in tokens {
360391
embed = embed.field(
361392
format!("**{}**", token.0),
362393
format!("{}/{}", token.1.owner, token.1.name),

src/events/issue.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub async fn message(data: &Data, ctx: &Context, message: &Message) {
4545
.await
4646
{
4747
// Safe to unwap member because this only runs in guilds.
48+
// The only way this could go wrong if cache isn't ready? (fresh bot restart)
4849
let has_perms = press.member.as_ref().map_or(false, |member| {
4950
member.permissions.map_or(false, |member_perms| {
5051
member_perms.contains(Permissions::MANAGE_MESSAGES)
@@ -106,7 +107,8 @@ async fn issue_embeds(data: &Data, message: &Message) -> Option<Vec<CreateEmbed>
106107
let client = octocrab::instance();
107108
let ratelimit = client.ratelimit();
108109

109-
let regex = Regex::new(r#" ?([a-z]+)?#([0-9]+[0-9]) ?"#).expect("Expected numbers regex");
110+
let regex =
111+
Regex::new(r#" ?([a-zA-Z0-9-_.]+)?#([0-9]+[0-9]) ?"#).expect("Expected numbers regex");
110112

111113
let custom_repos = { data.state.read().unwrap().issue_prefixes.clone() };
112114

@@ -118,7 +120,7 @@ async fn issue_embeds(data: &Data, message: &Message) -> Option<Vec<CreateEmbed>
118120
let issue_num = m.as_str().parse::<u64>().expect("Match is not a number");
119121

120122
if let Some(repo) = capture.get(1) {
121-
let repository = custom_repos.get(repo.as_str());
123+
let repository = custom_repos.get(&repo.as_str().to_lowercase());
122124
if let Some(repository) = repository {
123125
let (owner, repo) = repository.get();
124126

src/main.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ async fn main() {
4848
commands::register(),
4949
commands::snippets::snippet(),
5050
commands::snippets::create_snippet(),
51-
commands::snippets::delete_snippet(),
51+
commands::snippets::remove_snippet(),
5252
commands::snippets::export_snippet(),
5353
commands::snippets::list_snippets(),
5454
commands::snippets::edit_snippet(),
5555
commands::utils::embed(),
5656
commands::utils::edit_embed(),
57-
commands::utils::add_issue_token(),
58-
commands::utils::remove_issue_token(),
59-
commands::utils::list_tokens(),
57+
commands::utils::add_repo(),
58+
commands::utils::remove_repo(),
59+
commands::utils::list_repos(),
6060
],
6161
prefix_options: poise::PrefixFrameworkOptions {
6262
prefix: Some("!".into()),

0 commit comments

Comments
 (0)