Browse Source

Add gitea release command

master
NGnius (Graham) 3 years ago
parent
commit
cfee9fdd0f
5 changed files with 424 additions and 18 deletions
  1. +27
    -1
      src/command_definitions.rs
  2. +230
    -12
      src/discord.rs
  3. +64
    -0
      src/gitea.rs
  4. +86
    -0
      src/gitea_command.rs
  5. +17
    -5
      src/main.rs

+ 27
- 1
src/command_definitions.rs View File

@@ -22,6 +22,32 @@ pub fn hello_world(interaction: &Interaction) -> InteractionResponse {
tts: false,
content: format!("Hello {}!", cmd.member.nick.unwrap_or(cmd.member.user.unwrap().username)),
allowed_mentions: None,
embeds: None,
}),
}
}
}

// gitea-release command definition
pub fn def_gitea_release() -> (discord::ApplicationCommand, Option<String>) {
(discord::ApplicationCommand {
id: None,
application_id: None,
name: "gitea-release".to_string(),
description: "".to_string(),
options: Some(vec![
discord::ApplicationCommandOption::String {
name: "username".to_string(),
description: "Gitea username".to_string(),
required: true,
choices: None
},
discord::ApplicationCommandOption::String {
name: "repo".to_string(),
description: "Gitea repository".to_string(),
required: true,
choices: None
},
]),
}, Some("616329232389505055".to_string()))
}


+ 230
- 12
src/discord.rs View File

@@ -133,7 +133,14 @@ pub struct User {
pub struct ApplicationCommandInteractionData {
pub id: String,
pub name: String,
pub options: Option<Vec<ApplicationCommandInteractionData>>,
pub options: Option<Vec<ApplicationCommandInteractionDataOption>>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct ApplicationCommandInteractionDataOption {
pub name: String,
pub value: Option<String>, // FIXME this could be bool, integer, or sub-command as well
pub options: Option<Vec<ApplicationCommandInteractionDataOption>>,
}

pub enum InteractionResponse {
@@ -176,12 +183,91 @@ pub struct InteractionResponseRaw {
pub struct InteractionApplicationCommandCallbackData {
pub tts: bool,
pub content: String,
//pub embeds: Option<Vec<Embed>>,
pub embeds: Option<Vec<Embed>>,
pub allowed_mentions: Option<String>,
}

// slash command management structures
// embed and sub-objects
#[derive(Serialize, Deserialize, Clone)]
pub struct Embed {
pub title: Option<String>,
#[serde(rename = "type")]
pub type_: Option<String>,
pub description: Option<String>,
pub url: Option<String>,
pub timestamp: Option<String>,
pub color: Option<usize>,
pub footer: Option<EmbedFooter>,
pub image: Option<EmbedImage>,
pub thumbnail: Option<EmbedThumbnail>,
pub video: Option<EmbedVideo>,
pub provider: Option<EmbedProvider>,
pub author: Option<EmbedAuthor>,
pub fields: Option<Vec<EmbedField>>,
}

pub const EMBED_TYPE_RICH: &str = "rich";
pub const EMBED_TYPE_IMAGE: &str = "image";
pub const EMBED_TYPE_VIDEO: &str = "video";
pub const EMBED_TYPE_GIFV: &str = "gifv";
pub const EMBED_TYPE_ARTICLE: &str = "article";
pub const EMBED_TYPE_LINK: &str = "link";


#[derive(Serialize, Deserialize, Clone)]
pub struct EmbedFooter {
pub text: String,
pub icon_url: Option<String>,
pub proxy_icon_url: Option<String>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct EmbedImage {
pub url: Option<String>,
pub proxy_url: Option<String>,
pub height: Option<usize>,
pub width: Option<usize>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct EmbedThumbnail {
pub url: Option<String>,
pub proxy_url: Option<String>,
pub height: Option<usize>,
pub width: Option<usize>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct EmbedVideo {
pub url: Option<String>,
pub proxy_url: Option<String>,
pub height: Option<usize>,
pub width: Option<usize>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct EmbedProvider {
pub name: Option<String>,
pub url: Option<String>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct EmbedAuthor {
pub name: Option<String>,
pub url: Option<String>,
pub icon_url: Option<String>,
pub proxy_icon_url: Option<String>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct EmbedField {
pub name: String,
pub value: String,
pub inline: Option<bool>,
}

// slash command management structures
#[derive(Clone)]
pub struct ApplicationCommand {
pub id: Option<String>,
pub application_id: Option<String>,
@@ -190,56 +276,86 @@ pub struct ApplicationCommand {
pub options: Option<Vec<ApplicationCommandOption>>,
}

impl ApplicationCommand {
pub fn raw(&self) -> ApplicationCommandRaw {
let mut options = None;
if let Some(opts) = &self.options {
let mut options_raw = Vec::with_capacity(opts.len());
for i in opts {
options_raw.push(i.raw());
}
if !options_raw.is_empty() {
options = Some(options_raw);
}
}
ApplicationCommandRaw {
id: self.id.clone(),
application_id: self.application_id.clone(),
name: self.name.to_string(),
description: self.description.to_string(),
options,
}
}
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub struct ApplicationCommandRaw {
pub id: Option<String>,
pub application_id: Option<String>,
pub name: String,
pub description: String,
pub options: Option<Vec<ApplicationCommandOptionRaw>>,
}

#[derive(Clone)]
pub enum ApplicationCommandOption {
#[serde(rename = "1")]
//#[serde(rename = "1")]
SubCommand {
name: String,
description: String,
required: bool,
options: Option<Vec<ApplicationCommandOption>>,
},
#[serde(rename = "2")]
//#[serde(rename = "2")]
SubCommandGroup {
name: String,
description: String,
required: bool,
options: Option<Vec<ApplicationCommandOption>>,
},
#[serde(rename = "3")]
//#[serde(rename = "3")]
String {
name: String,
description: String,
required: bool,
choices: Option<Vec<ApplicationCommandOptionChoice>>,
},
#[serde(rename = "4")]
//#[serde(rename = "4")]
Integer {
name: String,
description: String,
required: bool,
choices: Option<Vec<ApplicationCommandOptionChoice>>,
},
#[serde(rename = "5")]
//#[serde(rename = "5")]
Boolean {
name: String,
description: String,
required: bool,
},
#[serde(rename = "6")]
//#[serde(rename = "6")]
User {
name: String,
description: String,
required: bool,
},
#[serde(rename = "7")]
//#[serde(rename = "7")]
Channel {
name: String,
description: String,
required: bool,
},
#[serde(rename = "8")]
//#[serde(rename = "8")]
Role {
name: String,
description: String,
@@ -247,6 +363,108 @@ pub enum ApplicationCommandOption {
},
}

impl ApplicationCommandOption {
pub fn raw(&self) -> ApplicationCommandOptionRaw {
match self {
ApplicationCommandOption::SubCommand { name, description, required, options} => {
let mut options_opt = None;
if let Some(opts) = options {
let mut options_raw = Vec::with_capacity(opts.len());
for i in opts {
options_raw.push(i.raw());
}
options_opt = Some(options_raw);
}
ApplicationCommandOptionRaw {
type_: 1,
name: name.to_string(),
description: description.to_string(),
required: *required,
options: options_opt,
choices: None,
}
},
ApplicationCommandOption::SubCommandGroup { name, description, required, options} => {
let mut options_opt = None;
if let Some(opts) = options {
let mut options_raw = Vec::with_capacity(opts.len());
for i in opts {
options_raw.push(i.raw());
}
options_opt = Some(options_raw);
}
ApplicationCommandOptionRaw {
type_: 2,
name: name.to_string(),
description: description.to_string(),
required: *required,
options: options_opt,
choices: None,
}
},
ApplicationCommandOption::String { name, description, required, choices} => ApplicationCommandOptionRaw {
type_: 3,
name: name.to_string(),
description: description.to_string(),
required: *required,
options: None,
choices: Some(choices.clone().unwrap()),
},
ApplicationCommandOption::Integer { name, description, required, choices} => ApplicationCommandOptionRaw {
type_: 4,
name: name.to_string(),
description: description.to_string(),
required: *required,
options: None,
choices: Some(choices.clone().unwrap()),
},
ApplicationCommandOption::Boolean { name, description, required} => ApplicationCommandOptionRaw {
type_: 5,
name: name.to_string(),
description: description.to_string(),
required: *required,
options: None,
choices: None
},
ApplicationCommandOption::User { name, description, required} => ApplicationCommandOptionRaw {
type_: 6,
name: name.to_string(),
description: description.to_string(),
required: *required,
options: None,
choices: None
},
ApplicationCommandOption::Channel { name, description, required} => ApplicationCommandOptionRaw {
type_: 7,
name: name.to_string(),
description: description.to_string(),
required: *required,
options: None,
choices: None
},
ApplicationCommandOption::Role { name, description, required} => ApplicationCommandOptionRaw {
type_: 8,
name: name.to_string(),
description: description.to_string(),
required: *required,
options: None,
choices: None
},
}
}
}

#[derive(Serialize, Deserialize, Clone)]
pub struct ApplicationCommandOptionRaw {
#[serde(rename = "type")]
pub type_: usize,
pub name: String,
pub description: String,
pub required: bool,
pub options: Option<Vec<ApplicationCommandOptionRaw>>,
pub choices: Option<Vec<ApplicationCommandOptionChoice>>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct ApplicationCommandOptionChoice {
pub name: String,


+ 64
- 0
src/gitea.rs View File

@@ -0,0 +1,64 @@
use serde::{Deserialize, Serialize};
use reqwest::blocking;

pub const GITEA_API_URL: &str = "https://git.exmods.org/api/v1";

// API endpoints
pub fn get_releases(owner: &str, repo: &str) -> Result<Vec<Release>, String> {
let client = blocking::Client::new();
let url = format!("{}/{}/{}/releases", GITEA_API_URL, owner, repo);
let result = client.get(&url).send();
if let Ok(resp) = result {
if let Ok(data) = resp.json::<Vec<Release>>() {
return Ok(data);
} else {
return Err("Invalid JSON payload".to_string());
}
}
Err("Invalid Result".to_string())
}

// API structures

#[derive(Serialize, Deserialize, Clone)]
pub struct Release {
pub id: usize,
pub tag_name: String,
pub target_commitish: String,
pub name: String,
pub body: String,
pub url: String,
pub tarball_url: String,
pub zipball_url: String,
pub draft: bool,
pub prerelease: bool,
pub created_at: String,
pub published_at: String,
pub author: Author,
pub assets: Option<Vec<Asset>>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Author {
pub id: usize,
pub login: String,
pub full_name: String,
pub email: String,
pub avatar_url: String,
pub language: String,
pub is_admin: bool,
pub last_login: String,
pub created: String,
pub username: String,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Asset {
pub id: usize,
pub name: String,
pub size: usize,
pub download_count: usize,
pub created_at: String,
pub uuid: String,
pub browser_download_url: String,
}

+ 86
- 0
src/gitea_command.rs View File

@@ -0,0 +1,86 @@
use crate::gitea::get_releases;
use crate::discord::{Interaction, InteractionResponse, InteractionApplicationCommandCallbackData, Embed, EmbedFooter, EmbedAuthor, EmbedField};

pub fn gitea_release(interaction: &Interaction) -> InteractionResponse {
let cmd = interaction.cmd().unwrap();
let mut username = String::new();
let mut repo_name = String::new();
for opt in &cmd.data.options.unwrap() {
match &opt.name as &str {
"username" => username = opt.value.clone().unwrap(),
"repo" => repo_name = opt.value.clone().unwrap(),
_ => {}
}
}
let res = get_releases(&username, &repo_name);
if let Ok(resp) = res {
if resp.is_empty() {
// no release in this repo
return InteractionResponse::ChannelMessage {
data: Some(InteractionApplicationCommandCallbackData {
tts: false,
content: format!("No releases found for {}'s {} repository", &username, &repo_name),
embeds: None,
allowed_mentions: None
})
}
}
// releases found, use most recent (0th index)
let release = &resp[0];
// build downloads field
let mut asset_str = String::new();
if let Some(assets) = &release.assets {
for f in assets {
asset_str += &format!("[{}]({})\n", f.name, f.browser_download_url);
}
}
// assemble embed
let embed = Embed {
title: Some(format!("{} {}", &repo_name, &release.tag_name)),
type_: None,
description: Some(release.body.clone()),
url: None,
timestamp: None,
color: Some(0x00C800), // Colour::from_rgb(0, 200, 0)
footer: Some(EmbedFooter {
text: release.author.login.clone(),
icon_url: Some(release.author.avatar_url.clone()),
proxy_icon_url: None
}),
image: None,
thumbnail: None,
video: None,
provider: None,
author: Some(EmbedAuthor {
name: Some(release.name.clone()),
url: Some(format!("https://git.exmods.org/{}/{}", &username, &repo_name)),
icon_url: None,
proxy_icon_url: None
}),
fields: Some(vec![
EmbedField {
name: "Download".to_string(),
value: asset_str,
inline: Some(true)
}
])
};
return InteractionResponse::ChannelMessage {
data: Some(InteractionApplicationCommandCallbackData {
tts: false,
content: "".to_string(),
embeds: Some(vec![embed]),
allowed_mentions: None
})
}
} else {
return InteractionResponse::ChannelMessageWithSource {
data: Some(InteractionApplicationCommandCallbackData {
tts: false,
content: format!("Gitea API error: `{}`", res.err().unwrap()),
embeds: None,
allowed_mentions: None
})
}
}
}

+ 17
- 5
src/main.rs View File

@@ -3,6 +3,8 @@
mod auth_tools;
mod discord;
mod command_definitions;
mod gitea;
mod gitea_command;

#[macro_use] extern crate rocket;
use lazy_static::lazy_static;
@@ -10,8 +12,9 @@ use lazy_static::lazy_static;
use rocket_contrib::json::Json;
use std::sync::RwLock;
use crate::auth_tools::{AuthenticatedInteraction};
use crate::discord::{Interaction, InteractionResponse, /*ApplicationCommand, */InteractionResponseRaw};
use crate::command_definitions::{hello_world, def_hello_world};
use crate::discord::{Interaction, InteractionResponse, InteractionResponseRaw, InteractionApplicationCommandCallbackData};
use crate::command_definitions::{hello_world, def_hello_world, def_gitea_release};
use crate::gitea_command::gitea_release;

static GLOBAL_COMMAND_KEY: &str = "GLOBAL command KEY";

@@ -29,7 +32,15 @@ fn root_post(interaction: AuthenticatedInteraction) -> Json<InteractionResponseR
Interaction::Command {data, ..} => {
match &data.name as &str {
"hello-world" => hello_world(&interaction.interaction),
_ => InteractionResponse::AcknowledgeWithSource {},
"gitea-release" => gitea_release(&interaction.interaction),
_ => InteractionResponse::ChannelMessageWithSource {
data: Some(InteractionApplicationCommandCallbackData {
tts: false,
content: "Oops, that's not implemented yet!".to_string(),
embeds: None,
allowed_mentions: None
})
},
}
},
_ => InteractionResponse::AcknowledgeWithSource {},
@@ -65,6 +76,7 @@ fn main() {
let req_client = reqwest::blocking::Client::new();
// TODO add more commands
register_command(&def_hello_world, &req_client);
register_command(&def_gitea_release, &req_client);
// start web server
rocket::ignite().mount("/", routes![root_post, hello_get]).launch();
}
@@ -77,7 +89,7 @@ fn register_command(f: &dyn Fn() -> (discord::ApplicationCommand, Option<String>
let url = format!("https://discord.com/api/v8/applications/{}/guilds/{}/commands", APPLICATION_ID.read().unwrap().as_ref().unwrap().clone(), &guild_id);
let res = client.post(&url)
.header("Authorization", format!("Bot {}", BOT_TOKEN.read().unwrap().as_ref().unwrap().clone()))
.json(&payload)
.json(&payload.raw())
.send();
if let Ok(d) = res {
println!("Registered/updated `{}` ({}) GUILD:{}", &payload.name, &d.status().as_str(), &guild_id);
@@ -88,7 +100,7 @@ fn register_command(f: &dyn Fn() -> (discord::ApplicationCommand, Option<String>
let url = format!("https://discord.com/api/v8/applications/{}/commands", APPLICATION_ID.read().unwrap().as_ref().unwrap().clone());
let res = client.post(&url)
.header("Authorization", format!("Bot {}", BOT_TOKEN.read().unwrap().as_ref().unwrap().clone()))
.json(&payload)
.json(&payload.raw())
.send();
if let Ok(d) = res {
println!("Registered/updated `{}` ({})", &payload.name, &d.status().as_str());


Loading…
Cancel
Save