+214
src/args.rs
+214
src/args.rs
···
1
+
fn help() {
2
+
println!(
3
+
"Help: tangled-on-commit
4
+
Listen for commits on a specified repository and execute a shell command.
5
+
6
+
CLI Arguments:
7
+
`tangled-on-commit (-h | --help)`
8
+
Displays this message
9
+
10
+
`tangled-on-commit`
11
+
No specified handle, repo, or command. Falls back to config/env
12
+
13
+
`tangled-on-commit SHELL`
14
+
Uses config/env for handle and repo
15
+
16
+
`tangled-on-commit @HANDLE SHELL`
17
+
Uses config/env for repo
18
+
19
+
`tangled-on-commit REPO SHELL`
20
+
Uses config/env for handle
21
+
22
+
`tangled-on-commit @HANDLE/REPO SHELL`
23
+
`tangled-on-commit HANDLE REPO SHELL`
24
+
No config/env
25
+
26
+
JSON:
27
+
Loads the file `tangled-on-commit.json` from cwd if it exists.
28
+
Reads keys \"handle\", \"repo_name\", and \"shell\".
29
+
Unknown keys are ignored and any key can be ommitted
30
+
JSON is used if the arguments aren't passed to the CLI
31
+
32
+
Env:
33
+
Loads the environment variables `TANGLED_ON_COMMIT_HANDLE` and `TANGLED_ON_COMMIT_REPO_NAME`
34
+
Shell cannot be set by environment variables.
35
+
Env variables are used if relevant keys are ommitted an arguments aren't passed to the CLI.
36
+
"
37
+
)
38
+
}
39
+
40
+
#[derive(Debug)]
41
+
pub struct Config {
42
+
pub handle: String,
43
+
pub repo_name: String,
44
+
pub shell: String,
45
+
}
46
+
47
+
pub fn load_config() -> Result<Config, ()> {
48
+
// load config from cli args if present
49
+
// if omitted, fallback to a local `tangled-on-commit.json` file
50
+
// if key omitted or file not found, fall back to env
51
+
// if env omitted, error out and quit
52
+
// note: shell is not loaded from env (to avoid the user unknowingly executing scripts)
53
+
54
+
// if any args are `-h` || `--help` display help and quit
55
+
for arg in std::env::args() {
56
+
if arg == "-h" || arg == "--help" {
57
+
help();
58
+
return Err(());
59
+
}
60
+
}
61
+
62
+
// if 0 args are passed, skip
63
+
// if 1 arg is passed, its the shell command
64
+
// if 2 args are passed:
65
+
// if arg 1 starts in @ its a handle
66
+
// if it contains a / the contents after the slash is the reponame
67
+
// else its the reponame
68
+
// arg 2 is always the shell command
69
+
// if 3 args are passed its handle repo shell
70
+
// if more args are passed its an error
71
+
72
+
let mut handle: Option<String> = None;
73
+
let mut repo_name: Option<String> = None;
74
+
let mut shell: Option<String> = None;
75
+
match std::env::args().collect::<Vec<_>>().len() {
76
+
// 0 args (std env args includes this script)
77
+
1 => {}
78
+
2 => {
79
+
shell = Some(
80
+
std::env::args()
81
+
.last()
82
+
.expect("Invalid state: 2 `Some` std::env::args() but found no Some values"),
83
+
)
84
+
}
85
+
3 => {
86
+
// load args and consume first
87
+
let mut args = std::env::args();
88
+
args.next();
89
+
90
+
if let Some(val) = args.next() {
91
+
if val.starts_with("@") {
92
+
if val.contains("/") {
93
+
let entries: Vec<_> = val.split("/").collect();
94
+
if entries.len() != 2 {
95
+
return Err(());
96
+
}
97
+
handle = Some(entries[0][1..].to_string());
98
+
repo_name = Some(entries[1].to_string());
99
+
} else {
100
+
handle = Some(val[1..].to_string());
101
+
}
102
+
} else {
103
+
repo_name = Some(val)
104
+
};
105
+
}
106
+
shell = Some(
107
+
args.next()
108
+
.expect("Invalid state: 3 `Some` std::env::args() but only found 2"),
109
+
);
110
+
}
111
+
4 => {
112
+
// load args and consume first
113
+
let mut args = std::env::args();
114
+
args.next();
115
+
116
+
handle = Some(
117
+
args.next()
118
+
.expect("Invalid state: 4 `Some` std::env::args() but only found 1"),
119
+
);
120
+
repo_name = Some(
121
+
args.next()
122
+
.expect("Invalid state: 4 `Some` std::env::args() but only found 2"),
123
+
);
124
+
shell = Some(
125
+
args.next()
126
+
.expect("Invalid state: 4 `Some` std::env::args() but only found 3"),
127
+
);
128
+
}
129
+
_ => {
130
+
// err
131
+
}
132
+
}
133
+
134
+
if let Some(ref handle) = handle
135
+
&& let Some(ref repo_name) = repo_name
136
+
&& let Some(ref shell) = shell
137
+
{
138
+
return Ok(Config {
139
+
handle: handle.to_string(),
140
+
repo_name: repo_name.to_string(),
141
+
shell: shell.to_string(),
142
+
});
143
+
}
144
+
145
+
// now load config
146
+
if let Ok(file) = std::fs::read_to_string("./tangled-on-commit.json") {
147
+
if let Ok(parsed) = json::parse(&file) {
148
+
if handle.is_none()
149
+
&& let Some(json_handle) = parsed["handle"].as_str()
150
+
{
151
+
handle = Some(String::from(json_handle))
152
+
}
153
+
if repo_name.is_none()
154
+
&& let Some(json_repo_name) = parsed["repo_name"].as_str()
155
+
{
156
+
repo_name = Some(String::from(json_repo_name))
157
+
}
158
+
if shell.is_none()
159
+
&& let Some(json_shell) = parsed["shell"].as_str()
160
+
{
161
+
shell = Some(String::from(json_shell))
162
+
}
163
+
}
164
+
}
165
+
166
+
if let Some(ref handle) = handle
167
+
&& let Some(ref repo_name) = repo_name
168
+
&& let Some(ref shell) = shell
169
+
{
170
+
return Ok(Config {
171
+
handle: handle.to_string(),
172
+
repo_name: repo_name.to_string(),
173
+
shell: shell.to_string(),
174
+
});
175
+
}
176
+
177
+
// now load from env
178
+
if handle.is_none()
179
+
&& let Ok(env_handle) = std::env::var("TANGLED_ON_COMMIT_HANDLE")
180
+
{
181
+
handle = Some(String::from(env_handle))
182
+
}
183
+
if repo_name.is_none()
184
+
&& let Ok(env_repo_name) = std::env::var("TANGLED_ON_COMMIT_REPO_NAME")
185
+
{
186
+
repo_name = Some(String::from(env_repo_name))
187
+
}
188
+
189
+
if let Some(ref handle) = handle
190
+
&& let Some(ref repo_name) = repo_name
191
+
&& let Some(ref shell) = shell
192
+
{
193
+
return Ok(Config {
194
+
handle: handle.to_string(),
195
+
repo_name: repo_name.to_string(),
196
+
shell: shell.to_string(),
197
+
});
198
+
}
199
+
200
+
// couldnt resolve every value
201
+
// print an error and quit
202
+
println!("Unable to find a value for every setting. Missing values for:");
203
+
if handle.is_none() {
204
+
println!("- handle");
205
+
}
206
+
if repo_name.is_none() {
207
+
println!("- repo_name");
208
+
}
209
+
if shell.is_none() {
210
+
println!("- shell");
211
+
}
212
+
println!("\nRun `tangled-on-commit --help` to see the help message");
213
+
return Err(());
214
+
}
+56
src/did.rs
+56
src/did.rs
···
1
+
use trust_dns_resolver::Resolver;
2
+
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
3
+
4
+
#[derive(Debug)]
5
+
pub struct DidDoc {
6
+
pub did: String,
7
+
}
8
+
9
+
fn get_txt_did(handle: &String) -> Result<String, ()> {
10
+
// create a txt resolver
11
+
let resolver = match Resolver::new(ResolverConfig::default(), ResolverOpts::default()) {
12
+
Ok(val) => val,
13
+
Err(_) => return Err(()),
14
+
};
15
+
16
+
// resolve _atproto.handle to a TXT record
17
+
let txt_res = match resolver.txt_lookup("_atproto.".to_owned() + &handle) {
18
+
Ok(val) => val,
19
+
Err(_) => return Err(()), // collect all entries and convert to strings
20
+
}
21
+
.into_iter()
22
+
.map(|x| x.to_string())
23
+
.collect::<Vec<_>>();
24
+
25
+
// filter entries which do not start with `did=`
26
+
let did_res = txt_res
27
+
.clone()
28
+
.extract_if(.., |x| x.starts_with("did="))
29
+
.collect::<Vec<_>>();
30
+
// only 1 did= can exist
31
+
// https://atproto.com/specs/handle#:~:text=If%20multiple%20valid%20records%20with%20different%20DIDs%20are%20present,%20resolution%20should%20fail.
32
+
if did_res.len() != 1 {
33
+
return Err(());
34
+
}
35
+
let did = did_res[0].clone();
36
+
37
+
return Ok(did.clone());
38
+
}
39
+
40
+
fn get_http_did(handle: &String) -> Result<String, ()> {
41
+
return Ok(String::new());
42
+
}
43
+
44
+
pub fn get_did(handle: String) -> Result<DidDoc, ()> {
45
+
let did = if let Ok(did) = get_txt_did(&handle) {
46
+
did
47
+
} else {
48
+
if let Ok(did) = get_http_did(&handle) {
49
+
did
50
+
} else {
51
+
return Err(());
52
+
}
53
+
};
54
+
55
+
return Ok(DidDoc { did });
56
+
}
+4
-255
src/main.rs
+4
-255
src/main.rs
···
1
-
use trust_dns_resolver::Resolver;
2
-
use trust_dns_resolver::config::{ResolverConfig, ResolverOpts};
3
-
4
-
fn help() {
5
-
println!(
6
-
"Help: tangled-on-commit
7
-
Listen for commits on a specified repository and execute a shell command.
8
-
9
-
CLI Arguments:
10
-
`tangled-on-commit (-h | --help)`
11
-
Displays this message
12
-
13
-
`tangled-on-commit`
14
-
No specified handle, repo, or command. Falls back to config/env
15
-
16
-
`tangled-on-commit SHELL`
17
-
Uses config/env for handle and repo
18
-
19
-
`tangled-on-commit @HANDLE SHELL`
20
-
Uses config/env for repo
21
-
22
-
`tangled-on-commit REPO SHELL`
23
-
Uses config/env for handle
24
-
25
-
`tangled-on-commit @HANDLE/REPO SHELL`
26
-
`tangled-on-commit HANDLE REPO SHELL`
27
-
No config/env
28
-
29
-
JSON:
30
-
Loads the file `tangled-on-commit.json` from cwd if it exists.
31
-
Reads keys \"handle\", \"repo_name\", and \"shell\".
32
-
Unknown keys are ignored and any key can be ommitted
33
-
JSON is used if the arguments aren't passed to the CLI
34
-
35
-
Env:
36
-
Loads the environment variables `TANGLED_ON_COMMIT_HANDLE` and `TANGLED_ON_COMMIT_REPO_NAME`
37
-
Shell cannot be set by environment variables.
38
-
Env variables are used if relevant keys are ommitted an arguments aren't passed to the CLI.
39
-
"
40
-
)
41
-
}
42
-
43
-
#[derive(Debug)]
44
-
struct Config {
45
-
handle: String,
46
-
repo_name: String,
47
-
shell: String,
48
-
}
49
-
50
-
fn load_config() -> Result<Config, ()> {
51
-
// load config from cli args if present
52
-
// if omitted, fallback to a local `tangled-on-commit.json` file
53
-
// if key omitted or file not found, fall back to env
54
-
// if env omitted, error out and quit
55
-
// note: shell is not loaded from env (to avoid the user unknowingly executing scripts)
56
-
57
-
// if any args are `-h` || `--help` display help and quit
58
-
for arg in std::env::args() {
59
-
if arg == "-h" || arg == "--help" {
60
-
help();
61
-
return Err(());
62
-
}
63
-
}
64
-
65
-
// if 0 args are passed, skip
66
-
// if 1 arg is passed, its the shell command
67
-
// if 2 args are passed:
68
-
// if arg 1 starts in @ its a handle
69
-
// if it contains a / the contents after the slash is the reponame
70
-
// else its the reponame
71
-
// arg 2 is always the shell command
72
-
// if 3 args are passed its handle repo shell
73
-
// if more args are passed its an error
74
-
75
-
let mut handle: Option<String> = None;
76
-
let mut repo_name: Option<String> = None;
77
-
let mut shell: Option<String> = None;
78
-
match std::env::args().collect::<Vec<_>>().len() {
79
-
// 0 args (std env args includes this script)
80
-
1 => {}
81
-
2 => {
82
-
shell = Some(
83
-
std::env::args()
84
-
.last()
85
-
.expect("Invalid state: 2 `Some` std::env::args() but found no Some values"),
86
-
)
87
-
}
88
-
3 => {
89
-
// load args and consume first
90
-
let mut args = std::env::args();
91
-
args.next();
92
-
93
-
if let Some(val) = args.next() {
94
-
if val.starts_with("@") {
95
-
if val.contains("/") {
96
-
let entries: Vec<_> = val.split("/").collect();
97
-
if entries.len() != 2 {
98
-
return Err(());
99
-
}
100
-
handle = Some(entries[0][1..].to_string());
101
-
repo_name = Some(entries[1].to_string());
102
-
} else {
103
-
handle = Some(val[1..].to_string());
104
-
}
105
-
} else {
106
-
repo_name = Some(val)
107
-
};
108
-
}
109
-
shell = Some(
110
-
args.next()
111
-
.expect("Invalid state: 3 `Some` std::env::args() but only found 2"),
112
-
);
113
-
}
114
-
4 => {
115
-
// load args and consume first
116
-
let mut args = std::env::args();
117
-
args.next();
118
-
119
-
handle = Some(
120
-
args.next()
121
-
.expect("Invalid state: 4 `Some` std::env::args() but only found 1"),
122
-
);
123
-
repo_name = Some(
124
-
args.next()
125
-
.expect("Invalid state: 4 `Some` std::env::args() but only found 2"),
126
-
);
127
-
shell = Some(
128
-
args.next()
129
-
.expect("Invalid state: 4 `Some` std::env::args() but only found 3"),
130
-
);
131
-
}
132
-
_ => {
133
-
// err
134
-
}
135
-
}
136
-
137
-
if let Some(ref handle) = handle
138
-
&& let Some(ref repo_name) = repo_name
139
-
&& let Some(ref shell) = shell
140
-
{
141
-
return Ok(Config {
142
-
handle: handle.to_string(),
143
-
repo_name: repo_name.to_string(),
144
-
shell: shell.to_string(),
145
-
});
146
-
}
147
-
148
-
// now load config
149
-
if let Ok(file) = std::fs::read_to_string("./tangled-on-commit.json") {
150
-
if let Ok(parsed) = json::parse(&file) {
151
-
if handle.is_none()
152
-
&& let Some(json_handle) = parsed["handle"].as_str()
153
-
{
154
-
handle = Some(String::from(json_handle))
155
-
}
156
-
if repo_name.is_none()
157
-
&& let Some(json_repo_name) = parsed["repo_name"].as_str()
158
-
{
159
-
repo_name = Some(String::from(json_repo_name))
160
-
}
161
-
if shell.is_none()
162
-
&& let Some(json_shell) = parsed["shell"].as_str()
163
-
{
164
-
shell = Some(String::from(json_shell))
165
-
}
166
-
}
167
-
}
168
-
169
-
if let Some(ref handle) = handle
170
-
&& let Some(ref repo_name) = repo_name
171
-
&& let Some(ref shell) = shell
172
-
{
173
-
return Ok(Config {
174
-
handle: handle.to_string(),
175
-
repo_name: repo_name.to_string(),
176
-
shell: shell.to_string(),
177
-
});
178
-
}
179
-
180
-
// now load from env
181
-
if handle.is_none()
182
-
&& let Ok(env_handle) = std::env::var("TANGLED_ON_COMMIT_HANDLE")
183
-
{
184
-
handle = Some(String::from(env_handle))
185
-
}
186
-
if repo_name.is_none()
187
-
&& let Ok(env_repo_name) = std::env::var("TANGLED_ON_COMMIT_REPO_NAME")
188
-
{
189
-
repo_name = Some(String::from(env_repo_name))
190
-
}
191
-
192
-
if let Some(ref handle) = handle
193
-
&& let Some(ref repo_name) = repo_name
194
-
&& let Some(ref shell) = shell
195
-
{
196
-
return Ok(Config {
197
-
handle: handle.to_string(),
198
-
repo_name: repo_name.to_string(),
199
-
shell: shell.to_string(),
200
-
});
201
-
}
202
-
203
-
// couldnt resolve every value
204
-
// print an error and quit
205
-
println!("Unable to find a value for every setting. Missing values for:");
206
-
if handle.is_none() {
207
-
println!("- handle");
208
-
}
209
-
if repo_name.is_none() {
210
-
println!("- repo_name");
211
-
}
212
-
if shell.is_none() {
213
-
println!("- shell");
214
-
}
215
-
println!("\nRun `tangled-on-commit --help` to see the help message");
216
-
return Err(());
217
-
}
218
-
219
-
#[derive(Debug)]
220
-
struct DidDoc {
221
-
did: String,
222
-
}
223
-
224
-
fn get_did(handle: String) -> Result<DidDoc, ()> {
225
-
// create a txt resolver
226
-
let resolver = match Resolver::new(ResolverConfig::default(), ResolverOpts::default()) {
227
-
Ok(val) => val,
228
-
Err(_) => return Err(()),
229
-
};
230
-
231
-
// resolve _atproto.handle to a TXT record
232
-
let txt_res = match resolver.txt_lookup("_atproto.".to_owned() + &handle) {
233
-
Ok(val) => val,
234
-
Err(_) => return Err(()), // collect all entries and convert to strings
235
-
}
236
-
.into_iter()
237
-
.map(|x| x.to_string())
238
-
.collect::<Vec<_>>();
239
-
240
-
// filter entries which do not start with `did=`
241
-
let did_res = txt_res
242
-
.clone()
243
-
.extract_if(.., |x| x.starts_with("did="))
244
-
.collect::<Vec<_>>();
245
-
// only 1 did= can exist
246
-
// https://atproto.com/specs/handle#:~:text=If%20multiple%20valid%20records%20with%20different%20DIDs%20are%20present,%20resolution%20should%20fail.
247
-
if did_res.len() != 1 {
248
-
return Err(());
249
-
}
250
-
let did = did_res[0].clone();
251
-
252
-
return Ok(DidDoc { did: did.clone() });
253
-
}
1
+
mod args;
2
+
mod did;
254
3
255
4
fn main() -> Result<(), ()> {
256
5
// load configuration
257
-
let config = match load_config() {
6
+
let config = match args::load_config() {
258
7
Ok(res) => res,
259
8
Err(_) => {
260
9
// q
···
264
13
println!("{:#?}", config);
265
14
266
15
// resolve handle to did
267
-
let did_doc = match get_did(config.handle) {
16
+
let did_doc = match did::get_did(config.handle) {
268
17
Ok(res) => res,
269
18
Err(_) => {
270
19
// q