Compare commits

...

11 commits

Author SHA1 Message Date
a80737ee62
ci: first config
All checks were successful
On_Push / lint_fmt (push) Successful in 57s
On_Push / lint_clippy (push) Successful in 46s
On_Push / test (push) Successful in 1m33s
On_Push / build (push) Successful in 1m33s
2024-11-23 20:03:17 +00:00
b9e35dfe09
nix: also a way to run tests from nix 2024-11-23 19:39:44 +00:00
3604ff197f
feat: not got feature flags 2024-11-23 18:53:48 +00:00
df28a6e11b
doc: turns out ye can have doctests, code in teh documentationt aht can be tested
wild
2024-11-23 18:40:35 +00:00
bcf8fba4f0
doc: added proper docs 2024-11-23 18:26:28 +00:00
92a303401b
fmt: cargo fmt and clippy 2024-11-23 02:13:29 +00:00
e2024eaa34
feat: move everything into a single file and compact it down 2024-11-23 02:00:13 +00:00
de87f5a926
feat: modified functions to rewqest 2024-11-23 01:20:04 +00:00
17af5069b3
feat: coped in code from discord bot 2024-11-23 01:05:07 +00:00
82d909693d
feat: basic mocking setup 2024-11-22 23:40:56 +00:00
95fe298ae1
git: project setup
Signed-off-by: Brendan Golden <git@brendan.ie>
2024-11-22 23:02:08 +00:00
12 changed files with 2260 additions and 1 deletions

View file

@ -0,0 +1,65 @@
name: On_Push
on:
push:
branches:
- 'main'
paths:
- flake.*
- src/**/*
- Cargo.*
- .forgejo/**/*
- rust-toolchain.toml
jobs:
# rust code must be formatted for standardisation
lint_fmt:
# build it using teh base nixos system, helps with caching
runs-on: nix
steps:
# get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4
- run: nix build .#fmt --verbose
# clippy is incredibly useful for making yer code better
lint_clippy:
# build it using teh base nixos system, helps with caching
runs-on: nix
permissions:
checks: write
steps:
# get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4
- run: nix build .#clippy --verbose
test:
# build it using teh base nixos system, helps with caching
runs-on: nix
permissions:
checks: write
steps:
# get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4
- run: nix build .#test --verbose
build:
# build it using teh base nixos system, helps with caching
runs-on: nix
needs: [ lint_fmt, lint_clippy, test ]
steps:
# get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4
- name: "Build it locally"
run: nix build --verbose
# # deploy it upstream
# deploy:
# # runs on teh default docker container
# runs-on: docker
# needs: [ build ]
# steps:
# - name: "Deploy to Skynet"
# uses: https://forgejo.skynet.ie/Skynet/actions-deploy-to-skynet@v2
# with:
# input: 'skynet_discord_bot'
# token: ${{ secrets.API_TOKEN_FORGEJO }}

37
.gitattributes vendored Normal file
View file

@ -0,0 +1,37 @@
# Git config here
* text eol=lf
#############################################
# Git lfs stuff
# Documents
*.pdf filter=lfs diff=lfs merge=lfs -text
*.doc filter=lfs diff=lfs merge=lfs -text
*.docx filter=lfs diff=lfs merge=lfs -text
# Excel
*.xls filter=lfs diff=lfs merge=lfs -text
*.xlsx filter=lfs diff=lfs merge=lfs -text
*.xlsm filter=lfs diff=lfs merge=lfs -text
# Powerpoints
*.ppt filter=lfs diff=lfs merge=lfs -text
*.pptx filter=lfs diff=lfs merge=lfs -text
*.ppsx filter=lfs diff=lfs merge=lfs -text
# Images
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
# Video
*.mkv filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.wmv filter=lfs diff=lfs merge=lfs -text
# Misc
*.zip filter=lfs diff=lfs merge=lfs -text
# ET4011
*.cbe filter=lfs diff=lfs merge=lfs -text
*.pbs filter=lfs diff=lfs merge=lfs -text
# Open/Libre office
# from https://www.libreoffice.org/discover/what-is-opendocument/
*.odt filter=lfs diff=lfs merge=lfs -text
*.ods filter=lfs diff=lfs merge=lfs -text
*.odp filter=lfs diff=lfs merge=lfs -text
*.odg filter=lfs diff=lfs merge=lfs -text
# QT
*.ui filter=lfs diff=lfs merge=lfs -text

15
.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
/target
/.idea
.env
*.env
result
/result
*.db
*.db.*
tmp/
tmp.*
*.csv

9
.rustfmt.toml Normal file
View file

@ -0,0 +1,9 @@
max_width = 150
single_line_if_else_max_width = 100
chain_width = 100
fn_params_layout = "Compressed"
#control_brace_style = "ClosingNextLine"
#brace_style = "PreferSameLine"
struct_lit_width = 0
tab_spaces = 2
use_small_heuristics = "Max"

1504
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "wolves-oxidised"
version = "0.1.0"
edition = "2021"
[features]
# this is for anythign in dev and not finalised yet
unstable = []
[dependencies]
# for making teh requests
reqwest = { version = "0.12", features = ["json"] }
# for testing async stuff
tokio-test = "0.4"
# parsing teh results
serde_json = { version = "1.0", features = ["raw_value"] }
serde = { version = "1.0.215", features = ["derive"] }

View file

@ -1,3 +1,11 @@
# wolves-oxidised
Rust library to interact with the UL Wolves API
Rust library to interact with the UL Wolves API
## Mockoon
Mockoon is a tool that is able to mock an api.
The responses from Woles are mocked using this tool and are stored in ``mocking``.
``nix-shell -p mockoon``
Then load up teh config in ``mocking``

93
flake.lock Normal file
View file

@ -0,0 +1,93 @@
{
"nodes": {
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1721727458,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
"owner": "nix-community",
"repo": "naersk",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1731890469,
"narHash": "sha256-D1FNZ70NmQEwNxpSSdTXCSklBH1z2isPR84J6DQrJGs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5083ec887760adfe12af64830a66807423a859a7",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1732014248,
"narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "23e89b7da85c3640bbc2173fe04f4bd114342367",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

66
flake.nix Normal file
View file

@ -0,0 +1,66 @@
{
description = "UL Wolves API Library";
inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable";
naersk.url = "github:nix-community/naersk";
utils.url = "github:numtide/flake-utils";
};
nixConfig = {
extra-substituters = "https://nix-cache.skynet.ie/skynet-cache";
extra-trusted-public-keys = "skynet-cache:zMFLzcRZPhUpjXUy8SF8Cf7KGAZwo98SKrzeXvdWABo=";
};
outputs = {
self,
nixpkgs,
utils,
naersk,
}:
utils.lib.eachDefaultSystem (
system: let
pkgs = (import nixpkgs) {inherit system;};
naersk' = pkgs.callPackage naersk {};
package_name = "wolves_api";
buildInputs = with pkgs; [
openssl
pkg-config
rustfmt
];
in rec {
packages = {
# For `nix build` & `nix run`:
default = naersk'.buildPackage {
pname = "${package_name}";
src = ./.;
buildInputs = buildInputs;
};
# Run `nix build .#fmt` to run tests
fmt = naersk'.buildPackage {
src = ./.;
mode = "fmt";
buildInputs = buildInputs;
};
# Run `nix build .#clippy` to lint code
clippy = naersk'.buildPackage {
src = ./.;
mode = "clippy";
buildInputs = buildInputs;
};
test = naersk'.buildPackage {
src = ./.;
mode = "test";
buildInputs = buildInputs;
};
};
# `nix develop`
devShell = pkgs.mkShell {
nativeBuildInputs = (
with pkgs; [rustc cargo]
) ++ buildInputs;
};
}
);
}

187
mocking/wolves_api.json Normal file
View file

@ -0,0 +1,187 @@
{
"uuid": "5b065de8-1360-4d55-aba4-c0f919f3669e",
"lastMigration": 32,
"name": "Wolves API",
"endpointPrefix": "",
"latency": 0,
"port": 3001,
"hostname": "",
"folders": [],
"routes": [
{
"uuid": "b8a7a9df-6d18-46ca-9f55-87385b1654cd",
"type": "http",
"documentation": "",
"method": "post",
"endpoint": "get_cns",
"responses": [
{
"uuid": "55d1e82e-ea35-4929-b8ce-2b1fe6e2f962",
"body": "{\n \"success\": 1,\n \"result\": {{data 'WolvesCommittees'}}\n}",
"latency": 0,
"statusCode": 200,
"label": "",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
"databucketID": "pwev",
"sendFileAsBody": false,
"rules": [],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false,
"default": true,
"crudKey": "id",
"callbacks": []
}
],
"responseMode": null
},
{
"uuid": "3d598a24-8088-43b4-a221-2296a7d7dc0b",
"type": "http",
"documentation": "",
"method": "post",
"endpoint": "get_members",
"responses": [
{
"uuid": "808c8312-66f6-4498-ac60-9fcf15211a6a",
"body": "{\n \"success\": 1,\n \"result\": {{data 'WolvesMembers'}}\n}",
"latency": 0,
"statusCode": 200,
"label": "",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
"databucketID": "",
"sendFileAsBody": false,
"rules": [],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false,
"default": true,
"crudKey": "id",
"callbacks": []
}
],
"responseMode": null
},
{
"uuid": "18a929f9-e616-4cf6-a67e-0950e1248ce8",
"type": "http",
"documentation": "",
"method": "post",
"endpoint": "get_member",
"responses": [
{
"uuid": "f4b77b66-4e4b-4429-b5ca-7bd3cd06cd04",
"body": "{\n \"success\": 1,\n \"result\": {{data 'WolvesMember'}}\n}",
"latency": 0,
"statusCode": 200,
"label": "",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
"databucketID": "",
"sendFileAsBody": false,
"rules": [],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false,
"default": true,
"crudKey": "id",
"callbacks": []
},
{
"uuid": "84af1b80-9b7c-48e9-a555-ed22f4652e75",
"body": "{\n \"success\": 1,\n \"result\": null\n}",
"latency": 0,
"statusCode": 200,
"label": "",
"headers": [],
"bodyType": "INLINE",
"filePath": "",
"databucketID": "",
"sendFileAsBody": false,
"rules": [],
"rulesOperator": "OR",
"disableTemplating": false,
"fallbackTo404": false,
"default": false,
"crudKey": "id",
"callbacks": []
}
],
"responseMode": "RANDOM"
}
],
"rootChildren": [
{
"type": "route",
"uuid": "b8a7a9df-6d18-46ca-9f55-87385b1654cd"
},
{
"type": "route",
"uuid": "3d598a24-8088-43b4-a221-2296a7d7dc0b"
},
{
"type": "route",
"uuid": "18a929f9-e616-4cf6-a67e-0950e1248ce8"
}
],
"proxyMode": false,
"proxyHost": "",
"proxyRemovePrefix": false,
"tlsOptions": {
"enabled": false,
"type": "CERT",
"pfxPath": "",
"certPath": "",
"keyPath": "",
"caPath": "",
"passphrase": ""
},
"cors": true,
"headers": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"proxyReqHeaders": [
{
"key": "",
"value": ""
}
],
"proxyResHeaders": [
{
"key": "",
"value": ""
}
],
"data": [
{
"uuid": "44f1e367-1b0c-4008-bb76-2f2547adeea2",
"id": "pwev",
"name": "WolvesCommittees",
"documentation": "",
"value": "[\n {{#repeat 1 20}}\n {{setVar 'name' (faker 'internet.userName')}}\n {\n \"id\": \"{{int 100 500}}\",\n \"name\": \"{{@name}}\",\n \"link\": \"https://ulwolves.ie/society/{{@name}}\",\n \"committee\": [\n {{#repeat 1 10}}\n \"{{int 1000 5000}}\",\n {{/repeat}}\n ]\n }\n {{/repeat}}\n]"
},
{
"uuid": "58843751-e03b-49e0-96d5-c9449deae514",
"id": "nejb",
"name": "WolvesMembers",
"documentation": "",
"value": "\n[\n \n {{#repeat 1 20}} \n {\n {{! Name of the Club/Soc}}\n \"committee\": \"Computer\",\n \"member_id\": \"{{int 100 500}}\",\n \"first_name\": \"{{firstName}}\",\n \"last_name\": \"{{lastName}}\",\n \"contact_email\": \"{{email}}\",\n \"opt_in_email\": \"{{oneOf (array '0' '1')}}\",\n \"student_id\": {{{oneOf (array 'null' '\"24123456\"' )}}},\n \"note\": {{{oneOf (array 'null' '\"note\"')}}},\n \"expiry\": \"{{date '2020-11-20' '2026-11-25' 'yyyy-MM-dd HH:mm:ss'}}\",\n \"requested\": \"{{date '2020-11-20' '2026-11-25' 'yyyy-MM-dd HH:mm:ss'}}\",\n \"approved\": \"{{date '2020-11-20' '2026-11-25' 'yyyy-MM-dd HH:mm:ss'}}\",\n \"sitename\":\"UL Wolves\",\n \"domain\":\"ulwolves.ie\"\n }\n {{/repeat}}\n]\n"
},
{
"uuid": "208bfc7f-ef60-4fdd-be8d-44bcfe48d264",
"id": "ef36",
"name": "WolvesMember",
"documentation": "",
"value": "{\n \"member_id\": \"{{int 100 500}}\",\n \"contact_email\": \"{{email}}\"\n}\n"
}
],
"callbacks": []
}

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "1.82"

253
src/lib.rs Normal file
View file

@ -0,0 +1,253 @@
use serde::{Deserialize, Serialize};
// This is what Wolves returns to us
// success will always be 1?
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResult<T> {
success: i8,
result: Vec<T>,
}
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultSingle<T> {
success: i8,
result: T,
}
/// Data returned for a member of a club/soc
#[derive(Deserialize, Serialize, Debug)]
pub struct WolvesUser {
// TODO: Might be worth trying to get this replaced with the club/soc ID?
/// Club/Soc the user is a member of
pub committee: String,
/// ID which uniquely identifies them on teh site
pub member_id: String,
/// First Name
pub first_name: String,
/// Last Name
pub last_name: String,
/// Email that they have set in their profile: <https://ulwolves.ie/memberships/profile>
///
/// Note: This does not indicate if they wish to be contacted via this address.
pub contact_email: String,
/// Denotes if a user has opted into being emailed: ``0`` or ``1``
pub opt_in_email: String,
/// if the member is/was a student this is their ID
pub student_id: Option<String>,
/// Optional note set by Committee
pub note: Option<String>,
/// When their membership will expire: ``yyyy-MM-dd HH:mm:ss``
pub expiry: String,
/// When member requested membership: ``yyyy-MM-dd HH:mm:ss``
pub requested: String,
/// When the member was approved: ``yyyy-MM-dd HH:mm:ss``
pub approved: String,
/// Name of the site the user is a member of:``UL Wolves``
pub sitename: String,
/// Domain of the site they are a member of: ``ulwolves.ie``
pub domain: String,
}
#[cfg(feature = "unstable")]
/// Information on an individual club/soc
#[derive(Deserialize, Serialize, Debug)]
pub struct WolvesCNS {
/// ID of teh club/Society
pub id: String,
/// Name of the Club/Society
pub name: String,
/// Link to their page such as <https://ulwolves.ie/society/computer>
pub link: String,
/// Array of Committee members ``member_id````'s
pub committee: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
enum WolvesUserExists {
B(bool),
S(String),
}
pub struct Client {
base_wolves: String,
base_key: Option<String>,
}
impl Client {
/// Create a new client for teh Wolves API
///
/// # Arguments
/// * `base_wolves` - Base URL for the requests, for example: ``https://cp.ulwolves.ie/api``
/// * `base_key` - An instance API Key for running higher level privilege tasks
///
/// # Examples
///
/// ```rust
/// use wolves_oxidised::Client;
/// let client = Client::new("https://cp.ulwolves.ie/api", None);
/// ```
/// ```rust
/// use wolves_oxidised::Client;
/// let client = Client::new("https://cp.ulwolves.ie/api", Some("api_key"));
/// ```
pub fn new(base_wolves: &str, base_key: Option<&str>) -> Self {
Self {
base_wolves: base_wolves.to_string(),
base_key: base_key.map(|x| x.to_string()),
}
}
/// General method to get endpoints which return an array
async fn get_bulk<T: serde::de::DeserializeOwned>(&self, wolves_endpoint: &str, api_key: &str) -> Vec<T> {
if self.base_wolves.is_empty() {
return vec![];
}
let url = format!("{}/{}", &self.base_wolves, wolves_endpoint);
// get wolves data
match reqwest::Client::new().post(&url).header("X-AM-Identity", api_key).send().await {
Ok(x) => {
if let Ok(WolvesResult {
success,
result,
}) = x.json::<WolvesResult<T>>().await
{
if success != 1 {
return vec![];
}
return result;
}
}
Err(e) => {
dbg!(e);
}
}
vec![]
}
/// Get the members of a Club?Society based on teh API key inputted
///
/// # Arguments
/// * `api_key` - API key scoped to teh specific club/soc
///
/// # Examples
///
/// ```rust
/// # tokio_test::block_on(async {
/// use wolves_oxidised::{Client, WolvesUser};
/// let client = Client::new("https://cp.ulwolves.ie/api", None);
/// let result: Vec<WolvesUser> = client.get_members("api_key_club_1").await;
/// # })
/// ```
pub async fn get_members(&self, api_key: &str) -> Vec<WolvesUser> {
self.get_bulk::<WolvesUser>("get_members", api_key).await
}
#[cfg(feature = "unstable")]
/// Get information about teh Club/Soc, including committee members
///
/// # Examples
/// No instance API key set
/// ```rust
/// # tokio_test::block_on(async {
/// use wolves_oxidised::{Client, WolvesCNS};
/// let client = Client::new("https://cp.ulwolves.ie/api", None);
/// let result: Vec<WolvesCNS> = client.get_committees().await;
/// assert_eq!(result.len(), 0);
/// # })
/// ```
///
/// Instance API key set, will return details if there are no other errors
/// ```rust
/// # tokio_test::block_on(async {
/// use wolves_oxidised::{Client, WolvesCNS};
/// let client = Client::new("https://cp.ulwolves.ie/api", Some("api_key_instance"));
/// let result: Vec<WolvesCNS> = client.get_committees().await;
/// # })
/// ```
pub async fn get_committees(&self) -> Vec<WolvesCNS> {
if let Some(api_key) = &self.base_key {
self.get_bulk::<WolvesCNS>("get_cns", api_key).await
} else {
vec![]
}
}
/// Get the ``member_id`` for a specific email if it exists.
///
/// # Arguments
/// * `api_key` - API key scoped to teh specific club/soc
///
/// # Examples
/// No instance API key set
/// ```rust
/// # tokio_test::block_on(async {
/// use wolves_oxidised::Client;
/// let client = Client::new("https://cp.ulwolves.ie/api", None);
/// let result: Option<i64> = client.get_member("example@example.ie").await;
/// assert!(result.is_none());
/// # })
/// ```
///
/// Instance API key set, will return details if there are no other errors
/// ```rust
/// # tokio_test::block_on(async {
/// use wolves_oxidised::Client;
/// let client = Client::new("https://cp.ulwolves.ie/api", Some("api_key_instance"));
/// let result: Option<i64> = client.get_member("example@example.ie").await;
/// # })
/// ```
pub async fn get_member(self, email: &str) -> Option<i64> {
// if the key isnt set then we cant do anything.
let api_key = match &self.base_key {
None => {
return None;
}
Some(key) => key,
};
let url = format!("{}/get_id_from_email", &self.base_wolves);
match reqwest::Client::new()
.post(&url)
.form(&[("email", email)])
.header("X-AM-Identity", api_key)
.send()
.await
{
Ok(x) => {
if let Ok(y) = x.json::<WolvesResultSingle<WolvesUserExists>>().await {
// this is the only time we will get a positive response, the None at the end catches everything else
if let WolvesUserExists::S(z) = y.result {
if let Ok(id) = z.parse::<i64>() {
return Some(id);
}
}
}
}
Err(e) => {
dbg!(e);
}
}
None
}
}
// pub fn add(left: u64, right: u64) -> u64 {
// left + right
// }
//
// #[cfg(test)]
// mod tests {
// use super::*;
//
// #[test]
// fn it_works() {
// let result = add(2, 2);
// assert_eq!(result, 4);
// }
// }