Skip to content
GitLab
Explore
Sign in
Commits on Source (2)
Colorize and purify chatter
· f9bda092
fooker
authored
Feb 13, 2019
f9bda092
Fixed channel addressing
· 0cfeb8da
fooker
authored
Feb 14, 2019
0cfeb8da
Show whitespace changes
Inline
Side-by-side
src/board/ballot.rs
View file @
0cfeb8da
use
chrono
::{
Local
,
NaiveDate
};
use
crate
::
opinion
::
*
;
use
std
::
collections
::{
hash_map
::{
DefaultHasher
,
Entry
::{
Occupied
,
Vacant
},
OccupiedEntry
},
HashMap
};
use
std
::
hash
::{
Hash
,
Hasher
};
use
std
::
ops
::
Deref
;
use
std
::
ops
::
DerefMut
;
#[derive(Serialize,
Deserialize,
Clone,
Copy,
Eq,
PartialEq,
Hash)]
use
chrono
::{
Local
,
NaiveDate
};
use
crate
::
opinion
::
*
;
#[derive(Clone,
Copy,
Debug,
Serialize,
Deserialize,
Eq,
PartialEq,
Hash)]
pub
struct
BallotId
(
u32
);
impl
std
::
fmt
::
Display
for
BallotId
{
...
...
@@ -24,7 +26,7 @@ impl std::convert::Into<u32> for BallotId {
fn
into
(
self
)
->
u32
{
self
.0
}
}
#[derive(Serialize,
Deserialize)]
#[derive(
Clone,
Debug,
Serialize,
Deserialize)]
pub
struct
Ballot
{
creation_date
:
NaiveDate
,
creator
:
String
,
...
...
@@ -87,7 +89,7 @@ impl<'a> DerefMut for Entry<'a> {
}
}
#[derive(Serialize,
Deserialize)]
#[derive(
Debug,
Serialize,
Deserialize)]
pub
struct
State
{
ballots
:
HashMap
<
BallotId
,
Ballot
>
,
}
...
...
@@ -106,7 +108,7 @@ impl State {
}
}
pub
fn
iter
<
'a
>
(
&
'a
self
)
->
impl
Iterator
<
Item
=
(
&
BallotId
,
&
Ballot
)
>
+
'a
{
pub
fn
iter
(
&
self
)
->
impl
Iterator
<
Item
=
(
&
BallotId
,
&
Ballot
)
>
{
self
.ballots
.iter
()
}
...
...
src/board/main.rs
View file @
0cfeb8da
...
...
@@ -8,15 +8,18 @@ extern crate serde_json;
extern
crate
spacebot
;
extern
crate
time
;
use
std
::
collections
::
HashSet
;
use
std
::
fmt
;
use
std
::
iter
::
FromIterator
;
use
clap
::{
Arg
,
SubCommand
};
use
itertools
::
Itertools
;
use
spacebot
::
prelude
::
*
;
use
crate
::
ballot
::
*
;
use
crate
::
opinion
::
*
;
use
crate
::
repo
::
*
;
use
itertools
::
Itertools
;
use
spacebot
::
prelude
::
*
;
use
std
::
collections
::
HashSet
;
use
std
::
iter
::
FromIterator
;
use
failure
::
Error
;
mod
opinion
;
mod
ballot
;
...
...
@@ -51,6 +54,65 @@ impl ModuleConfig for Config {
}
}
#[derive(Clone,
Debug)]
struct
BallotFormat
<
'a
>
{
ballot
:
&
'a
Ballot
,
verbose
:
bool
,
}
impl
<
'a
>
BallotFormat
<
'a
>
{
pub
fn
quiet
(
ballot
:
&
'a
Ballot
)
->
Self
{
return
Self
{
ballot
,
verbose
:
false
,
};
}
pub
fn
verbose
(
ballot
:
&
'a
Ballot
)
->
Self
{
return
Self
{
ballot
,
verbose
:
true
,
};
}
}
impl
<
'a
>
fmt
::
Display
for
BallotFormat
<
'a
>
{
fn
fmt
(
&
self
,
f
:
&
mut
fmt
::
Formatter
)
->
Result
<
(),
fmt
::
Error
>
{
write!
(
f
,
"
\x03
14<
\x0F\x03
12{}
\x0F\x03
14>
\x0F
{}"
,
self
.ballot
.id
(),
self
.ballot
.text
())
?
;
if
self
.ballot
.opinions
()
.count
()
>
0
{
write!
(
f
,
"
\x03
14[
\x0F
{}
\x03
14]
\x0F
"
,
self
.ballot
.opinions
()
.iter
()
.format_with
(
" "
,
|(
nick
,
opinion
),
format
|
match
opinion
{
Opinion
::
Yes
=>
format
(
&
format_args!
(
"
\x03
03
\x02
+
\x0F\x03
03{}
\x0F
"
,
nick
)),
Opinion
::
No
=>
format
(
&
format_args!
(
"
\x03
04
\x02
-
\x0F\x03
04{}
\x0F
"
,
nick
)),
}))
?
;
}
if
self
.verbose
{
write!
(
f
,
"
\x03
15(created by {} at {})
\x0F
"
,
self
.ballot
.creator
(),
self
.ballot
.creation_date
())
?
;
}
return
Ok
(());
}
}
#[derive(Clone,
Debug,
Eq,
PartialEq)]
enum
FinishReason
<
'a
>
{
Completed
,
Committed
{
committer
:
&
'a
str
,
},
}
impl
<
'a
>
FinishReason
<
'a
>
{
pub
fn
completed
()
->
Self
{
return
FinishReason
::
Completed
;
}
pub
fn
committed
(
committer
:
&
'a
str
)
->
Self
{
return
FinishReason
::
Committed
{
committer
};
}
}
struct
BoardBot
{
board_members
:
HashSet
<
String
>
,
log_channels
:
HashSet
<
String
>
,
...
...
@@ -68,9 +130,15 @@ impl BoardBot {
let
mut
state
=
self
.state
.write
();
let
ballot
=
state
.new
(
nick
,
text
);
// Inform all members
self
.announce
(
response
,
ballot
,
&
format!
(
"New poll: {} (created by {})"
,
ballot
.text
(),
ballot
.creator
()))
?
;
// Respond to creator
response
.respond_message
(
&
format!
(
"Your proposal has been recorded: {}"
,
BallotFormat
::
quiet
(
ballot
)))
?
;
// Inform the board
for
member
in
self
.board_members
.iter
()
{
response
.message
(
member
,
&
format!
(
"New ballot: {}"
,
BallotFormat
::
verbose
(
ballot
)))
?
;
}
return
Ok
(());
}
...
...
@@ -79,13 +147,8 @@ impl BoardBot {
let
state
=
self
.state
.read
();
if
!
state
.is_empty
()
{
response
.respond_message
(
&
format!
(
"The following ballots are currently open:"
))
?
;
for
(
id
,
poll
)
in
state
.iter
()
{
response
.respond_message
(
&
format!
(
" <{}> {} [{}] (created by {} at {})"
,
id
,
poll
.text
(),
poll
.opinions
(),
poll
.creator
(),
poll
.creation_date
()))
?
;
for
(
_
,
ballot
)
in
state
.iter
()
{
response
.respond_message
(
&
format!
(
" {}"
,
BallotFormat
::
verbose
(
&
ballot
)))
?
;
}
response
.respond_message
(
"--"
)
?
;
}
else
{
...
...
@@ -101,17 +164,19 @@ impl BoardBot {
if
let
Some
(
mut
ballot
)
=
state
.get
(
id
)
{
// Save the opinion and inform the channel
if
ballot
.vote
(
nick
,
opinion
)
{
self
.announce
(
response
,
&
ballot
,
&
match
opinion
{
Opinion
::
Yes
=>
format!
(
"{} voted for
\"
{}
\"
"
,
nick
,
ballot
.text
()),
Opinion
::
No
=>
format!
(
"{} voted against
\"
{}
\"
"
,
nick
,
ballot
.text
()),
for
member
in
self
.board_members
.iter
()
{
response
.message
(
member
,
&
match
opinion
{
Opinion
::
Yes
=>
format!
(
"{} voted for {}"
,
nick
,
BallotFormat
::
quiet
(
&
ballot
)),
Opinion
::
No
=>
format!
(
"{} voted against {}"
,
nick
,
BallotFormat
::
quiet
(
&
ballot
)),
})
?
;
}
}
response
.respond_message
(
&
format!
(
"Your opinion for ballot <{}> has been recorded"
,
id
))
?
;
// Auto-commit if everybody has voted
if
ballot
.opinions
()
.count
()
==
self
.board_members
.len
()
{
self
.finish
(
response
,
ballot
.delete
())
?
;
self
.finish
(
response
,
ballot
.delete
()
,
FinishReason
::
completed
()
)
?
;
}
}
else
{
response
.respond_message
(
&
format!
(
"No such ballot: <{}>"
,
id
))
?
;
...
...
@@ -125,9 +190,9 @@ impl BoardBot {
if
let
Some
(
ballot
)
=
state
.get
(
id
)
{
if
ballot
.opinion_count
()
>
self
.board_members
.len
()
/
2
{
self
.finish
(
response
,
ballot
.delete
())
?
;
self
.finish
(
response
,
ballot
.delete
()
,
FinishReason
::
committed
(
nick
)
)
?
;
}
else
{
response
.respond_message
(
&
format!
(
"Not enough votes to commit:
<
{}
> ({})"
,
id
,
ballot
.opinions
(
)))
?
;
response
.respond_message
(
&
format!
(
"Not enough votes to commit: {}
"
,
BallotFormat
::
quiet
(
&
ballot
)))
?
;
}
}
else
{
response
.respond_message
(
&
format!
(
"No such ballot: <{}>"
,
id
))
?
;
...
...
@@ -141,9 +206,15 @@ impl BoardBot {
if
let
Some
(
ballot
)
=
state
.get
(
id
)
{
let
ballot
=
ballot
.delete
();
self
.announce
(
response
,
&
ballot
,
&
format!
(
"{} has canceled this ballot: {}: [{}] (created by {} at {})"
,
nick
,
ballot
.text
(),
ballot
.opinions
(),
ballot
.creator
(),
ballot
.creation_date
()))
?
;
for
member
in
self
.board_members
.iter
()
{
response
.message
(
member
,
&
format!
(
"{} canceled {}"
,
nick
,
BallotFormat
::
verbose
(
&
ballot
)))
?
;
}
// Inform the creator
response
.message
(
ballot
.creator
(),
&
format!
(
"Your proposal has been canceled: {}"
,
BallotFormat
::
quiet
(
&
ballot
)))
?
;
}
else
{
response
.respond_message
(
&
format!
(
"Unknown ballot: <{}>"
,
id
))
?
;
}
...
...
@@ -160,9 +231,11 @@ impl BoardBot {
match
pattern
{
Ok
(
pattern
)
=>
{
// TODO: Parse the resolution and colorize output
for
line
in
self
.repository
.search
(
&
pattern
)
{
response
.respond_message
(
&
line
)
?
;
}
response
.respond_message
(
"--"
)
?
;
}
Err
(
err
)
=>
{
...
...
@@ -173,17 +246,30 @@ impl BoardBot {
return
Ok
(());
}
fn
finish
(
&
self
,
response
:
&
Response
,
ballot
:
Ballot
)
->
Result
<
(),
CmdError
>
{
fn
finish
(
&
self
,
response
:
&
Response
,
ballot
:
Ballot
,
reason
:
FinishReason
)
->
Result
<
(),
CmdError
>
{
let
resolution
=
Resolution
::
from_ballot
(
&
ballot
);
self
.repository
.append
(
&
resolution
);
self
.announce
(
response
,
&
ballot
,
&
format!
(
"Resolution {}: {} [{}]"
,
resolution
.conclusion
(),
resolution
.text
(),
resolution
.opinions
()))
?
;
let
message
=
match
resolution
.conclusion
()
{
Opinion
::
Yes
=>
format!
(
"Accepted resolution: {}"
,
BallotFormat
::
quiet
(
&
ballot
)),
Opinion
::
No
=>
format!
(
"Rejected resolution: {}"
,
BallotFormat
::
quiet
(
&
ballot
)),
};
// Inform the channel
for
channel
in
self
.log_channels
.iter
()
{
response
.message
(
&
channel
,
&
format!
(
"{}"
,
&
message
))
?
;
}
response
.message
(
&
ballot
.creator
(),
&
format!
(
"Your proposed resolution: {} has been {} [{}]"
,
resolution
.text
(),
resolution
.conclusion
(),
resolution
.opinions
()))
?
;
// Inform the creator
response
.message
(
ballot
.creator
(),
&
format!
(
"{}"
,
&
message
))
?
;
// Inform the board
for
member
in
self
.board_members
.iter
()
{
match
reason
{
FinishReason
::
Completed
=>
response
.message
(
member
,
&
format!
(
"{} (completed)"
,
&
message
))
?
,
FinishReason
::
Committed
{
committer
}
=>
response
.message
(
member
,
&
format!
(
"{} (committed by {})"
,
&
message
,
committer
))
?
,
}
}
return
Ok
(());
}
...
...
@@ -199,30 +285,12 @@ impl BoardBot {
return
Ok
(());
}
fn
announce
(
&
self
,
response
:
&
Response
,
ballot
:
&
Ballot
,
message
:
&
str
)
->
Result
<
(),
Error
>
{
let
message
=
format!
(
"<{}> {}"
,
ballot
.id
(),
message
);
for
channel
in
self
.log_channels
.iter
()
{
response
.notice
(
&
channel
,
&
message
)
?
;
}
for
member
in
self
.board_members
.iter
()
{
response
.message
(
&
member
,
&
message
)
?
;
}
if
!
self
.board_members
.contains
(
ballot
.creator
())
{
response
.message
(
&
ballot
.creator
(),
&
message
)
?
;
}
return
Ok
(());
}
}
impl
CommandModule
for
BoardBot
{
type
Config
=
Config
;
fn
init
(
config
:
&
Config
,
initializer
:
&
mut
Initializer
)
->
Result
<
Self
,
failure
::
Error
>
{
fn
init
(
config
:
&
Config
,
_
:
&
mut
Initializer
)
->
Result
<
Self
,
failure
::
Error
>
{
return
Ok
(
BoardBot
{
log_channels
:
HashSet
::
from_iter
(
config
.log_channels
.clone
()),
board_members
:
HashSet
::
from_iter
(
config
.board_members
.clone
()),
...
...
src/board/opinion.rs
View file @
0cfeb8da
use
itertools
::
Itertools
;
use
std
::
collections
::
HashMap
;
#[derive(Serialize,
Deserialize,
Clone,
Copy,
PartialEq,
Eq)]
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
PartialEq,
Eq)]
pub
enum
Opinion
{
Yes
,
No
,
}
impl
std
::
fmt
::
Display
for
Opinion
{
fn
fmt
(
&
self
,
f
:
&
mut
std
::
fmt
::
Formatter
)
->
std
::
fmt
::
Result
{
match
self
{
&
Opinion
::
Yes
=>
write!
(
f
,
"accepted"
),
&
Opinion
::
No
=>
write!
(
f
,
"rejected"
),
}
}
}
#[derive(Serialize,
Deserialize,
Clone)]
#[derive(Clone,
Debug,
Serialize,
Deserialize)]
pub
struct
Opinions
(
HashMap
<
String
,
Opinion
>
);
impl
Opinions
{
...
...
@@ -44,18 +34,8 @@ impl Opinions {
return
if
y
>
n
{
Opinion
::
Yes
}
else
{
Opinion
::
No
};
}
}
impl
std
::
fmt
::
Display
for
Opinions
{
fn
fmt
(
&
self
,
f
:
&
mut
std
::
fmt
::
Formatter
)
->
Result
<
(),
std
::
fmt
::
Error
>
{
return
f
.write_str
(
&
self
.0
.iter
()
.map
(|(
member
,
opinion
)|
{
format!
(
"{}{}"
,
match
*
opinion
{
Opinion
::
Yes
=>
'+'
,
Opinion
::
No
=>
'-'
,
},
member
)
})
.join
(
" "
)
);
pub
fn
iter
(
&
self
)
->
impl
Iterator
<
Item
=
(
&
String
,
&
Opinion
)
>
{
return
self
.0
.iter
();
}
}
src/board/repo.rs
View file @
0cfeb8da
use
chrono
::{
Datelike
,
Local
,
NaiveDate
};
use
crate
::
ballot
::
*
;
use
crate
::
opinion
::
*
;
use
regex
::
Regex
;
use
std
::
fs
::
OpenOptions
;
use
std
::
fs
::
read_dir
;
use
std
::
io
::
BufRead
;
...
...
@@ -9,10 +5,18 @@ use std::io::BufReader;
use
std
::
io
::
Write
;
use
std
::
path
::{
Path
,
PathBuf
};
use
std
::
process
::
Command
;
use
log
::
debug
;
use
chrono
::{
Datelike
,
Local
,
NaiveDate
};
use
itertools
::
Itertools
;
use
regex
::
Regex
;
use
crate
::
ballot
::
*
;
use
crate
::
opinion
::
*
;
#[derive(Clone,
Debug)]
pub
struct
Repository
(
PathBuf
);
#[derive(Clone,
Debug)]
pub
struct
Resolution
{
id
:
u32
,
date
:
NaiveDate
,
...
...
@@ -72,7 +76,13 @@ impl Repository {
resolution
.date
()
.month
(),
resolution
.date
()
.day
(),
resolution
.text
(),
resolution
.opinions
())
resolution
.opinions
()
.iter
()
.format_with
(
" "
,
|(
nick
,
opinion
),
f
|
f
(
&
format_args!
(
"{}{}"
,
match
opinion
{
Opinion
::
Yes
=>
"+"
,
Opinion
::
No
=>
"-"
,
},
nick
)),
))
.expect
(
&
format!
(
"Failed to append to resolution file: {:?}"
,
&
path
));
Command
::
new
(
"git"
)
...
...
@@ -97,7 +107,7 @@ impl Repository {
self
.pull
();
return
read_dir
(
self
.0
.join
(
"resolutions"
))
.unwrap
()
.map
(|
entry
|
{
.
flat_
map
(|
entry
|
{
let
path
=
entry
.unwrap
()
.path
();
let
file
=
OpenOptions
::
new
()
.read
(
true
)
...
...
@@ -114,7 +124,6 @@ impl Repository {
}
});
})
.flatten
()
.collect
();
}
}
src/spacebot/cmd.rs
View file @
0cfeb8da
...
...
@@ -64,7 +64,7 @@ impl<M> Module for M
return
Ok
(());
};
if
let
Ok
(
args
)
=
shellwords
::
split
(
&
request
.
message
)
{
// FIXME: Error handling of mismatched quotes
if
let
Ok
(
args
)
=
shellwords
::
split
(
message
)
{
// FIXME: Error handling of mismatched quotes
let
app
=
App
::
new
(
""
)
.bin_name
(
"FIXME"
)
// FIXME: Use right name and set width to max line length
.setting
(
AppSettings
::
ArgRequiredElseHelp
)
...
...