From b023279afb2597b6377833b70bdf7845b87afa42 Mon Sep 17 00:00:00 2001 From: devgianlu Date: Fri, 19 Jan 2024 17:31:36 +0100 Subject: [PATCH 1/2] Rename havcebot to ctfbot --- Dockerfile | 6 +++--- cmd/{havcebotd => ctfbotd}/main.go | 22 ++++++++++----------- ctf.go | 2 +- havcebot.go => ctfbot.go | 2 +- havcebot.toml.sample => ctfbot.sample | 2 +- ctftime/client.go | 6 +++--- discord/ctf.go | 28 +++++++++++++-------------- discord/discord.go | 6 +++--- discord/info.go | 8 ++++---- discord/middlewares.go | 6 +++--- discord/server.go | 6 +++--- discord/utils.go | 6 +++--- docker-compose.yml | 4 ++-- error.go | 4 ++-- go.mod | 2 +- sqlite/ctf.go | 26 ++++++++++++------------- sqlite/sqlite.go | 2 +- utils.go | 2 +- 18 files changed, 70 insertions(+), 70 deletions(-) rename cmd/{havcebotd => ctfbotd}/main.go (90%) rename havcebot.go => ctfbot.go (71%) rename havcebot.toml.sample => ctfbot.sample (73%) diff --git a/Dockerfile b/Dockerfile index eedf2bb..6169b55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,10 +11,10 @@ COPY . . ENV CGO_ENABLED=0 -RUN go build -o havcebotd ./cmd/havcebotd +RUN go build -o ctfbotd ./cmd/ctfbotd FROM gcr.io/distroless/static-debian12 -COPY --from=builder /app/havcebotd /havcebotd +COPY --from=builder /app/ctfbotd /ctfbotd -ENTRYPOINT ["/havcebotd", "-config-path", "/havcebotd.toml"] +ENTRYPOINT ["/ctfbotd", "-config-path", "/ctfbotd.toml"] diff --git a/cmd/havcebotd/main.go b/cmd/ctfbotd/main.go similarity index 90% rename from cmd/havcebotd/main.go rename to cmd/ctfbotd/main.go index fe08e2f..d97662e 100644 --- a/cmd/havcebotd/main.go +++ b/cmd/ctfbotd/main.go @@ -13,10 +13,10 @@ import ( "strings" "github.com/BurntSushi/toml" - "github.com/havce/havcebot" - "github.com/havce/havcebot/ctftime" - "github.com/havce/havcebot/discord" - "github.com/havce/havcebot/sqlite" + "github.com/havce/ctfbot" + "github.com/havce/ctfbot/ctftime" + "github.com/havce/ctfbot/discord" + "github.com/havce/ctfbot/sqlite" ) // Build version, injected during build. @@ -39,8 +39,8 @@ type Config struct { } const ( - DefaultDSN = "~/havcebot.sqlite3" - DefaultConfigPath = "~/havcebot.toml" + DefaultDSN = "~/ctfbot.sqlite3" + DefaultConfigPath = "~/ctfbot.toml" ) const ( @@ -59,8 +59,8 @@ func DefaultConfig() Config { func main() { // Propagate build information to root package to share globally. - havcebot.Version = strings.TrimPrefix(version, "") - havcebot.Commit = commit + ctfbot.Version = strings.TrimPrefix(version, "") + ctfbot.Commit = commit ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) defer cancel() @@ -116,8 +116,8 @@ func (m *Main) Close(ctx context.Context) error { } func (m *Main) ParseFlagAndConfig(ctx context.Context, args []string) error { - f := flag.NewFlagSet("havcebotd", flag.ContinueOnError) - f.StringVar(&m.ConfigPath, "config-path", "~/.havcebot.toml", "config file path") + f := flag.NewFlagSet("ctfbotd", flag.ContinueOnError) + f.StringVar(&m.ConfigPath, "config-path", "~/.ctfbot.toml", "config file path") if err := f.Parse(args); err != nil { return err } @@ -204,7 +204,7 @@ func (m *Main) Run(ctx context.Context) (err error) { return err } - slog.Log(ctx, slog.LevelInfo, "havcebotd started") + slog.Log(ctx, slog.LevelInfo, "ctfbotd started") return nil } diff --git a/ctf.go b/ctf.go index e97b797..4c1c31c 100644 --- a/ctf.go +++ b/ctf.go @@ -1,4 +1,4 @@ -package havcebot +package ctfbot import ( "context" diff --git a/havcebot.go b/ctfbot.go similarity index 71% rename from havcebot.go rename to ctfbot.go index 0ebee70..404c498 100644 --- a/havcebot.go +++ b/ctfbot.go @@ -1,4 +1,4 @@ -package havcebot +package ctfbot var ( Version = "n/a" diff --git a/havcebot.toml.sample b/ctfbot.sample similarity index 73% rename from havcebot.toml.sample rename to ctfbot.sample index 33b570f..473895b 100644 --- a/havcebot.toml.sample +++ b/ctfbot.sample @@ -1,5 +1,5 @@ [db] -dsn = "/database/havcebot.sqlite" +dsn = "/database/ctfbot.sqlite" [discord] # Required diff --git a/ctftime/client.go b/ctftime/client.go index 63828ef..332b9c5 100644 --- a/ctftime/client.go +++ b/ctftime/client.go @@ -7,7 +7,7 @@ import ( "net/url" "strconv" - "github.com/havce/havcebot" + "github.com/havce/ctfbot" ) type Client struct { @@ -42,9 +42,9 @@ func (c *Client) FindEventByID(ctx context.Context, id int) (*Event, error) { } if resp.StatusCode > 400 && resp.StatusCode < 499 { - return nil, havcebot.Errorf(havcebot.ENOTFOUND, "Event not found.") + return nil, ctfbot.Errorf(ctfbot.ENOTFOUND, "Event not found.") } else if resp.StatusCode < 200 || resp.StatusCode > 299 { - return nil, havcebot.Errorf(havcebot.EINVALID, "Internal server error.") + return nil, ctfbot.Errorf(ctfbot.EINVALID, "Internal server error.") } event := &Event{} diff --git a/discord/ctf.go b/discord/ctf.go index b302a9c..5a6f48d 100644 --- a/discord/ctf.go +++ b/discord/ctf.go @@ -13,7 +13,7 @@ import ( "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/handler" "github.com/disgoorg/snowflake/v2" - "github.com/havce/havcebot" + "github.com/havce/ctfbot" ) const ( @@ -34,7 +34,7 @@ func (s *Server) handleCommandNewCTF(event *handler.CommandEvent) error { // Check if CTF is already present with the same name. _, err := s.CTFService.FindCTFByName(context.TODO(), ctfName) if err == nil { - return Error(event, havcebot.Errorf(havcebot.ECONFLICT, "A CTF with the same name has already been created.")) + return Error(event, ctfbot.Errorf(ctfbot.ECONFLICT, "A CTF with the same name has already been created.")) } _, err = event.CreateFollowupMessage(discord.NewMessageCreateBuilder(). @@ -188,7 +188,7 @@ func (s *Server) handleCreateCTF(event *handler.ComponentEvent) error { // Check again if CTF is already present with the same name. _, err = s.CTFService.FindCTFByName(context.TODO(), ctf) if err == nil { - return Error(event, havcebot.Errorf(havcebot.ECONFLICT, "A CTF with the same name has already been created.")) + return Error(event, ctfbot.Errorf(ctfbot.ECONFLICT, "A CTF with the same name has already been created.")) } // Create role with CTF name. @@ -293,7 +293,7 @@ func (s *Server) handleCreateCTF(event *handler.ComponentEvent) error { return Error(event, err) } - err = s.CTFService.CreateCTF(context.TODO(), &havcebot.CTF{ + err = s.CTFService.CreateCTF(context.TODO(), &ctfbot.CTF{ Name: ctf, Start: time.Now(), // Parse the role.ID as uint64 and then convert @@ -338,18 +338,18 @@ func (s *Server) handleJoinCTF(event *handler.ComponentEvent) error { } if !retrievedCTF.CanJoin { - return Error(event, havcebot.Errorf(havcebot.EUNAUTHORIZED, "Registrations are closed for `%s`. Ask an admin if you want to join.", ctf)) + return Error(event, ctfbot.Errorf(ctfbot.EUNAUTHORIZED, "Registrations are closed for `%s`. Ask an admin if you want to join.", ctf)) } role, found := s.client.Caches().Role(*event.GuildID(), roleID) if !found { return Error(event, - havcebot.Errorf(havcebot.ENOTFOUND, "Couldn't find player role for `%s`. Maybe it was deleted?", ctf)) + ctfbot.Errorf(ctfbot.ENOTFOUND, "Couldn't find player role for `%s`. Maybe it was deleted?", ctf)) } if slices.Contains(event.Member().RoleIDs, role.ID) { return Error(event, - havcebot.Errorf(havcebot.ECONFLICT, "You already joined `%s`", ctf)) + ctfbot.Errorf(ctfbot.ECONFLICT, "You already joined `%s`", ctf)) } // Add the roleID to the roleIDs of the user. @@ -376,7 +376,7 @@ func (s *Server) handleUpdateCanJoin(canJoin bool) func(event *handler.CommandEv // If you're not inside a CTF it will output a CTF not found error. _, err = s.CTFService.UpdateCTF(context.TODO(), parentChannel.Name(), - havcebot.CTFUpdate{ + ctfbot.CTFUpdate{ CanJoin: &canJoin, }) if err != nil { @@ -403,8 +403,8 @@ func (s *Server) handleFlag(blood bool) func(event *handler.CommandEvent) error } if !s.flagAllowed(event.Channel().Name()) { - return Error(event, havcebot.Errorf( - havcebot.EINVALID, "You cannot flag here.")) + return Error(event, ctfbot.Errorf( + ctfbot.EINVALID, "You cannot flag here.")) } // Check if someone has already flagged this. @@ -415,7 +415,7 @@ func (s *Server) handleFlag(blood bool) func(event *handler.CommandEvent) error blocklist := []string{flagEmoji, bloodEmoji} // Check against blocklist. if slices.Contains(blocklist, string(c)) { - return Error(event, havcebot.Errorf(havcebot.EINVALID, "Somebody has already %s this.", prefix)) + return Error(event, ctfbot.Errorf(ctfbot.EINVALID, "Somebody has already %s this.", prefix)) } } @@ -478,8 +478,8 @@ func (s *Server) handleNewChal(event *handler.CommandEvent) error { // If so, return an error. if found { - return Error(event, havcebot.Errorf( - havcebot.ECONFLICT, "Somebody has already created `%s`.", chalName)) + return Error(event, ctfbot.Errorf( + ctfbot.ECONFLICT, "Somebody has already created `%s`.", chalName)) } // We already validated the existence of parentChannel in the middleware. @@ -502,7 +502,7 @@ func (s *Server) handleNewChal(event *handler.CommandEvent) error { role, found := s.client.Caches().Role(*event.GuildID(), roleID) if !found { - return Error(event, havcebot.Errorf(havcebot.EINTERNAL, "Couldn't find player role for `%s`. Maybe it was deleted?", ctf.Name)) + return Error(event, ctfbot.Errorf(ctfbot.EINTERNAL, "Couldn't find player role for `%s`. Maybe it was deleted?", ctf.Name)) } // Create the channel with our custom permissions. diff --git a/discord/discord.go b/discord/discord.go index 9889d34..4bec13d 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -4,7 +4,7 @@ import ( "github.com/disgoorg/disgo/bot" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/rest" - "github.com/havce/havcebot" + "github.com/havce/ctfbot" ) type CreateFollowupMessager interface { @@ -14,9 +14,9 @@ type CreateFollowupMessager interface { func Error(event CreateFollowupMessager, err error) error { // Extract error code and message. - code, message := havcebot.ErrorCode(err), havcebot.ErrorMessage(err) + code, message := ctfbot.ErrorCode(err), ctfbot.ErrorMessage(err) - if code == havcebot.EINTERNAL { + if code == ctfbot.EINTERNAL { event.Client().Logger().Error("Internal server error", code, err) } diff --git a/discord/info.go b/discord/info.go index 97941e5..cda4138 100644 --- a/discord/info.go +++ b/discord/info.go @@ -7,8 +7,8 @@ import ( "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/handler" - "github.com/havce/havcebot" - "github.com/havce/havcebot/ctftime" + "github.com/havce/ctfbot" + "github.com/havce/ctfbot/ctftime" ) const ( @@ -46,7 +46,7 @@ func (s *Server) handleInfoCTF(vote bool) func(event *handler.CommandEvent) erro title := event.Title if vote { - title = havcebot.Itoe(i+1) + " " + title + title = ctfbot.Itoe(i+1) + " " + title } embed := discord.Embed{ @@ -110,7 +110,7 @@ func (s *Server) handleInfoCTF(vote bool) func(event *handler.CommandEvent) erro } for i := range embeds { - err = s.client.Rest().AddReaction(event.Channel().ID(), msg.ID, havcebot.Itoe(i+1)) + err = s.client.Rest().AddReaction(event.Channel().ID(), msg.ID, ctfbot.Itoe(i+1)) if err != nil { return Error(event, err) } diff --git a/discord/middlewares.go b/discord/middlewares.go index 7824c0f..47bed4a 100644 --- a/discord/middlewares.go +++ b/discord/middlewares.go @@ -7,7 +7,7 @@ import ( "github.com/disgoorg/disgo/events" "github.com/disgoorg/disgo/handler" "github.com/disgoorg/disgo/handler/middleware" - "github.com/havce/havcebot" + "github.com/havce/ctfbot" ) // AdminOnly restricts access to the routes to Administrators only. @@ -24,7 +24,7 @@ var AdminOnly handler.Middleware = func(next handler.Handler) handler.Handler { SetEphemeral(true). SetEmbeds(messageEmbedError("You're not authorized to run this command.")).Build()) - return havcebot.Errorf(havcebot.EUNAUTHORIZED, "You're not authorized to run this command.") + return ctfbot.Errorf(ctfbot.EUNAUTHORIZED, "You're not authorized to run this command.") } } @@ -35,7 +35,7 @@ func (s *Server) MustBeInsideCTFAndAdmin(next handler.Handler) handler.Handler { discord.NewMessageCreateBuilder(). SetEphemeral(true). SetEmbeds(messageEmbedError("You're not authorized to run this command.")).Build()) - return havcebot.Errorf(havcebot.EUNAUTHORIZED, "You're not authorized to run this command.") + return ctfbot.Errorf(ctfbot.EUNAUTHORIZED, "You're not authorized to run this command.") } return s.MustBeInsideCTF(next)(e) } diff --git a/discord/server.go b/discord/server.go index 4119632..c420e30 100644 --- a/discord/server.go +++ b/discord/server.go @@ -12,8 +12,8 @@ import ( "github.com/disgoorg/disgo/handler" "github.com/disgoorg/disgo/handler/middleware" "github.com/disgoorg/snowflake/v2" - "github.com/havce/havcebot" - "github.com/havce/havcebot/ctftime" + "github.com/havce/ctfbot" + "github.com/havce/ctfbot/ctftime" ) type Server struct { @@ -23,7 +23,7 @@ type Server struct { router handler.Router client bot.Client - CTFService havcebot.CTFService + CTFService ctfbot.CTFService CTFTimeClient *ctftime.Client // Channel default names. diff --git a/discord/utils.go b/discord/utils.go index 41a7137..fdf3115 100644 --- a/discord/utils.go +++ b/discord/utils.go @@ -7,7 +7,7 @@ import ( "github.com/disgoorg/disgo/discord" "github.com/disgoorg/snowflake/v2" - "github.com/havce/havcebot" + "github.com/havce/ctfbot" ) func formatTime(t *time.Time) string { @@ -17,11 +17,11 @@ func formatTime(t *time.Time) string { func (s *Server) parentChannel(channelID snowflake.ID) (discord.GuildChannel, error) { currentChannel, present := s.client.Caches().Channel(channelID) if !present { - return nil, havcebot.Errorf(havcebot.ENOTFOUND, "Channel not found.") + return nil, ctfbot.Errorf(ctfbot.ENOTFOUND, "Channel not found.") } parentChannel, present := s.client.Caches().Channel(*currentChannel.ParentID()) if !present { - return nil, havcebot.Errorf(havcebot.ENOTFOUND, "Parent channel of %s not found.", currentChannel.Name()) + return nil, ctfbot.Errorf(ctfbot.ENOTFOUND, "Parent channel of %s not found.", currentChannel.Name()) } return parentChannel, nil diff --git a/docker-compose.yml b/docker-compose.yml index f5b148d..6e1308b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ version: '3' services: - havcebot: + ctfbot: build: . volumes: - - "./havcebotd.toml:/havcebotd.toml:ro" + - "./ctfbotd.toml:/ctfbotd.toml:ro" - "database:/database" restart: unless-stopped diff --git a/error.go b/error.go index 00c2b1b..763bbd6 100644 --- a/error.go +++ b/error.go @@ -1,4 +1,4 @@ -package havcebot +package ctfbot import ( "errors" @@ -36,7 +36,7 @@ type Error struct { // Error implements the error interface. Not used by the application otherwise. func (e *Error) Error() string { - return fmt.Sprintf("havcebot error: code=%s message=%s", e.Code, e.Message) + return fmt.Sprintf("ctfbot error: code=%s message=%s", e.Code, e.Message) } // ErrorCode unwraps an application error and returns its code. diff --git a/go.mod b/go.mod index e512b37..db7e416 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/havce/havcebot +module github.com/havce/ctfbot go 1.21.6 diff --git a/sqlite/ctf.go b/sqlite/ctf.go index 8d6365f..450b038 100644 --- a/sqlite/ctf.go +++ b/sqlite/ctf.go @@ -4,7 +4,7 @@ import ( "context" "strings" - "github.com/havce/havcebot" + "github.com/havce/ctfbot" ) type CTFService struct { @@ -17,7 +17,7 @@ func NewCTFService(db *DB) *CTFService { } } -func (s *CTFService) FindCTFByName(ctx context.Context, name string) (*havcebot.CTF, error) { +func (s *CTFService) FindCTFByName(ctx context.Context, name string) (*ctfbot.CTF, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, err @@ -28,7 +28,7 @@ func (s *CTFService) FindCTFByName(ctx context.Context, name string) (*havcebot. return findCTFByName(ctx, tx, name) } -func (s *CTFService) FindCTFs(ctx context.Context, filter havcebot.CTFFilter) ([]*havcebot.CTF, int, error) { +func (s *CTFService) FindCTFs(ctx context.Context, filter ctfbot.CTFFilter) ([]*ctfbot.CTF, int, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, 0, err @@ -38,7 +38,7 @@ func (s *CTFService) FindCTFs(ctx context.Context, filter havcebot.CTFFilter) ([ return findCTFs(ctx, tx, filter) } -func (s *CTFService) CreateCTF(ctx context.Context, ctf *havcebot.CTF) error { +func (s *CTFService) CreateCTF(ctx context.Context, ctf *ctfbot.CTF) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err @@ -52,7 +52,7 @@ func (s *CTFService) CreateCTF(ctx context.Context, ctf *havcebot.CTF) error { return tx.Commit() } -func (s *CTFService) UpdateCTF(ctx context.Context, name string, upd havcebot.CTFUpdate) (*havcebot.CTF, error) { +func (s *CTFService) UpdateCTF(ctx context.Context, name string, upd ctfbot.CTFUpdate) (*ctfbot.CTF, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, err @@ -80,17 +80,17 @@ func (s *CTFService) DeleteCTF(ctx context.Context, name string) error { return tx.Commit() } -func findCTFByName(ctx context.Context, tx *Tx, name string) (*havcebot.CTF, error) { - ctfs, _, err := findCTFs(ctx, tx, havcebot.CTFFilter{Name: &name}) +func findCTFByName(ctx context.Context, tx *Tx, name string) (*ctfbot.CTF, error) { + ctfs, _, err := findCTFs(ctx, tx, ctfbot.CTFFilter{Name: &name}) if err != nil { return nil, err } else if len(ctfs) == 0 { - return nil, havcebot.Errorf(havcebot.ENOTFOUND, "CTF not found.") + return nil, ctfbot.Errorf(ctfbot.ENOTFOUND, "CTF not found.") } return ctfs[0], nil } -func findCTFs(ctx context.Context, tx *Tx, filter havcebot.CTFFilter) (_ []*havcebot.CTF, n int, err error) { +func findCTFs(ctx context.Context, tx *Tx, filter ctfbot.CTFFilter) (_ []*ctfbot.CTF, n int, err error) { // Build WHERE clause. Each part of the WHERE clause is AND-ed together. // Values are appended to an arg list to avoid SQL injection. where, args := []string{"1 = 1"}, []interface{}{} @@ -140,9 +140,9 @@ func findCTFs(ctx context.Context, tx *Tx, filter havcebot.CTFFilter) (_ []*havc defer rows.Close() // Iterate over rows and deserialize into CTF objects. - ctfs := make([]*havcebot.CTF, 0) + ctfs := make([]*ctfbot.CTF, 0) for rows.Next() { - var ctf havcebot.CTF + var ctf ctfbot.CTF if err := rows.Scan( &ctf.ID, &ctf.Name, @@ -166,7 +166,7 @@ func findCTFs(ctx context.Context, tx *Tx, filter havcebot.CTFFilter) (_ []*havc } // createCTF creates a new CTF. -func createCTF(ctx context.Context, tx *Tx, ctf *havcebot.CTF) error { +func createCTF(ctx context.Context, tx *Tx, ctf *ctfbot.CTF) error { // Set timestamps to current time. ctf.CreatedAt = tx.now ctf.UpdatedAt = ctf.CreatedAt @@ -212,7 +212,7 @@ func createCTF(ctx context.Context, tx *Tx, ctf *havcebot.CTF) error { } // updateCTF updates a ctf by name. Returns the new state of the ctf after update. -func updateCTF(ctx context.Context, tx *Tx, name string, upd havcebot.CTFUpdate) (*havcebot.CTF, error) { +func updateCTF(ctx context.Context, tx *Tx, name string, upd ctfbot.CTFUpdate) (*ctfbot.CTF, error) { // Fetch current object state. Return an error if current user is not owner. ctf, err := findCTFByName(ctx, tx, name) if err != nil { diff --git a/sqlite/sqlite.go b/sqlite/sqlite.go index 264f73a..0658295 100644 --- a/sqlite/sqlite.go +++ b/sqlite/sqlite.go @@ -218,7 +218,7 @@ func FormatLimitOffset(limit, offset int) string { return "" } -// FormatError returns err as a havcebot error, if possible. +// FormatError returns err as a ctfbot error, if possible. // Otherwise returns the original error. func FormatError(err error) error { if err == nil { diff --git a/utils.go b/utils.go index 86039e4..2a90e95 100644 --- a/utils.go +++ b/utils.go @@ -1,4 +1,4 @@ -package havcebot +package ctfbot import ( "strconv" From 9b2e87913b297723e779695b488b8fbbdfb8274f Mon Sep 17 00:00:00 2001 From: devgianlu Date: Fri, 19 Jan 2024 17:37:47 +0100 Subject: [PATCH 2/2] Add README --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d90639 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# ctfbot + +A Discord bot for managing CTFs in your team. + +Picture this: your Discord server has grown too much and there are too many people wandering around. You are tired of +gathering participation and manually setting your channel permissions. Then this what you are looking for! + +## Features + +The bot supports various commands: + +- `/new`: Create a new CTF (admin only) +- `/open`: Open the CTF for registration (admin only) +- `/close`: Close the CTF registration (admin only) +- `/delete`: Delete the CTF (admin only) +- `/info`: List CTFs available on CTFTime for the next weeks +- `/vote`: Start a vote for which CTF to play (admin only) +- `/chal`: Create a new challenge inside the CTF +- `/flag`: Mark the challenge as solved +- `/blood`: Mark the challenge as first blooded