├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── gaslighter │ └── gaslighter.go ├── gatekeeper │ ├── bex.go │ ├── command.go │ ├── discord.go │ ├── ed.go │ ├── gatekeeper.go │ ├── irc.go │ ├── minesweeper.go │ ├── mpv.go │ └── twitch.go └── mpv-client │ ├── .gitignore │ ├── flag.h │ ├── mpv-client.c │ └── nob.h ├── go.mod ├── go.sum ├── internal ├── carrotson.go └── postgres.go ├── logo ├── gatekeeper-256.png ├── gatekeeper.png └── gatekeeper.svg ├── sql ├── 01-init.sql ├── 02-carrotson.sql ├── 03-bex.sql ├── 04-discord-log.sql ├── 05-ed-state.sql ├── 06-command-count.sql └── 07-song-log.sql └── tools └── inflitrate.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /gatekeeper 3 | /gaslighter -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alexey Kutepov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gatekeeper 2 | 3 |

4 | logo 5 |

6 | 7 | The chat bot Zozin does not want you to know about. 8 | 9 | ## Quick Start 10 | 11 | ### Gatekeeper 12 | 13 | Gatekeeper is the bot process itself. 14 | 15 | ```console 16 | $ go build ./cmd/gatekeeper/ 17 | $ ./gatekeeper 18 | ``` 19 | 20 | ### Gaslighter 21 | 22 | Gaslighter is the bot configuration command line tool which works by connecting to the bot's database and modifying it. 23 | 24 | ```console 25 | $ go build ./cmd/gaslighter/ 26 | $ ./gaslighter 27 | ``` 28 | 29 | ## Environment Variables 30 | 31 | | Name | Description| 32 | |---|---| 33 | | `GATEKEEPER_DISCORD_TOKEN` | Discord token [https://discord.com/developers/docs/topics/oauth2](https://discord.com/developers/docs/topics/oauth2)| 34 | | `GATEKEEPER_PGSQL_CONNECTION` | PostgreSQL connection URL [https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6](https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6) | 35 | | `GATEKEEPER_TWITCH_IRC_NICK` | Twitch Login | 36 | | `GATEKEEPER_TWITCH_IRC_PASS` | Twitch Password [https://twitchapps.com/tmi/](https://twitchapps.com/tmi/) | 37 | | `GATEKEEPER_MPV_IPC_ADDRESS` | Address for the MPV IPC control to listen to. Format is `:` | 38 | 39 | ## MPV Control 40 | 41 | When `GATEKEEPER_MPV_IPC_ADDRESS` is provided bot starts listening for [MPV IPC](https://mpv.io/manual/stable/#json-ipc) on that address and port. It is intended to be used in conjunction with [mpv-client](./cmd/mpv-client) to make the bot report currently playing songs. 42 | 43 | ### Security Considerations 44 | 45 | The connection and the protocol are insecure and lack any authentication or encryption. Because of that it is highly advised to set the address of `GATEKEEPER_MPV_IPC_ADDRESS` to `127.0.0.1:` and connect the mpv-client through an [SSH tunnel](https://www.ssh.com/academy/ssh/tunneling) if the bot is hosted on a remote machine. 46 | 47 | ### Quick Start via SSH tunnel 48 | 49 | Make sure `GATEKEEPER_MPV_IPC_ADDRESS` is set to `127.0.0.1:8080` on your `remotemachine` host. 50 | 51 | Build the mpv-client: 52 | 53 | ```console 54 | $ cc -o mpv-client ./cmd/mpv-client/mpv-client.c 55 | ``` 56 | 57 | Establish the SSH tunnel: 58 | 59 | ```console 60 | $ ssh user@remotemachine -N -L 8080:127.0.0.1:8080 61 | ``` 62 | 63 | Play the song with the mpv-client (make sure [mpv](https://mpv.io/) is installed as the mpv-client wraps around it). 64 | 65 | ```console 66 | $ ./mpv-client song.mp3 67 | ``` 68 | -------------------------------------------------------------------------------- /cmd/gaslighter/gaslighter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "time" 7 | "math/rand" 8 | "github.com/tsoding/gatekeeper/internal" 9 | "database/sql" 10 | "flag" 11 | ) 12 | 13 | func carrotsonTraverseTree(db *sql.DB, message []rune, limit int, walk func([]rune) error) (err error) { 14 | err = walk(message) 15 | if err != nil { 16 | return 17 | } 18 | if len(message) < limit { 19 | var branches []internal.Branch 20 | branches, err = internal.QueryBranchesFromContext(db, internal.ContextOfMessage(message)) 21 | if err != nil { 22 | return 23 | } 24 | for _, branch := range branches { 25 | err = carrotsonTraverseTree(db, append(message, branch.Follows), limit, walk) 26 | if err != nil { 27 | return 28 | } 29 | } 30 | } 31 | return 32 | } 33 | 34 | type Subcmd struct { 35 | Run func(args []string) int 36 | } 37 | 38 | var Subcmds = map[string]Subcmd{ 39 | "uncarrot": Subcmd{ 40 | Run: func(args []string) int { 41 | subFlag := flag.NewFlagSet("uncarrot", flag.ExitOnError) 42 | message := subFlag.String("p", "", "Message to remove from the Carrotson model") 43 | 44 | subFlag.Parse(args) 45 | 46 | if len(*message) == 0 { 47 | fmt.Fprintf(os.Stderr, "ERROR: no message was provided to uncarrot. Use flag -m to provide the message.") 48 | return 1 49 | } 50 | 51 | db := internal.StartPostgreSQL() 52 | if db == nil { 53 | return 1 54 | } 55 | defer db.Close() 56 | 57 | // TODO: uncarrot does not properly remove the prefix of the message 58 | runesOfMessage := []rune(*message) 59 | for i := 0; i + internal.ContextSize < len(runesOfMessage); i += 1 { 60 | context := string(runesOfMessage[i:i + internal.ContextSize]) 61 | follows := string(runesOfMessage[i + internal.ContextSize:i + internal.ContextSize + 1]) 62 | _, err := db.Exec("UPDATE Carrotson_Branches SET frequency = 0 WHERE context = $1 and follows = $2", 63 | context, follows) 64 | if err != nil { 65 | fmt.Fprintf(os.Stderr, "ERROR: could not remove branch (%v, %v) from model: %s\n", context, follows, err) 66 | return 1 67 | } 68 | } 69 | 70 | return 0 71 | }, 72 | }, 73 | "carrotree": Subcmd{ 74 | Run: func(args []string) int { 75 | subFlag := flag.NewFlagSet("carrotree", flag.ExitOnError) 76 | prefix := subFlag.String("p", "", "Prefix") 77 | limit := subFlag.Int("l", 1024, "Limit") 78 | 79 | subFlag.Parse(args) 80 | 81 | db := internal.StartPostgreSQL() 82 | if db == nil { 83 | return 1 84 | } 85 | defer db.Close() 86 | 87 | err := carrotsonTraverseTree(db, []rune(*prefix), *limit, func(message []rune) error { 88 | fmt.Println("CARROTSON:", string(message)) 89 | return nil 90 | }) 91 | if err != nil { 92 | fmt.Fprintln(os.Stderr, err) 93 | return 1 94 | } 95 | 96 | return 0 97 | }, 98 | }, 99 | "carrotadd": Subcmd{ 100 | Run: func(args []string) int { 101 | db := internal.StartPostgreSQL() 102 | if db == nil { 103 | return 1 104 | } 105 | defer db.Close() 106 | 107 | for _, arg := range args { 108 | internal.FeedMessageToCarrotson(db, arg) 109 | } 110 | 111 | return 0 112 | }, 113 | }, 114 | "carrot": Subcmd{ 115 | Run: func(args []string) int { 116 | subFlag := flag.NewFlagSet("carrot", flag.ExitOnError) 117 | prefix := subFlag.String("p", "", "Prefix") 118 | limit := subFlag.Int("l", 1024, "Limit") 119 | 120 | subFlag.Parse(args) 121 | 122 | db := internal.StartPostgreSQL() 123 | if db == nil { 124 | return 1 125 | } 126 | defer db.Close() 127 | 128 | message, err := internal.CarrotsonGenerate(db, *prefix, *limit) 129 | if err != nil { 130 | fmt.Fprintln(os.Stderr, "ERROR: could not generate Carrotson message:", err) 131 | return 1; 132 | } 133 | 134 | fmt.Println("CARROTSON:", message) 135 | return 0 136 | }, 137 | }, 138 | } 139 | 140 | func topUsage(program string) { 141 | fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n", program); 142 | fmt.Fprintf(os.Stderr, "SUBCOMMANDS:\n"); 143 | for name, _ := range(Subcmds) { 144 | fmt.Fprintf(os.Stderr, " %s\n", name) 145 | } 146 | } 147 | 148 | func main() { 149 | rand.Seed(time.Now().UnixNano()) 150 | 151 | if len(os.Args) <= 0 { 152 | panic("Empty command line arguments") 153 | } 154 | program := "gaslighter" 155 | 156 | if len(os.Args) < 2 { 157 | topUsage(program) 158 | fmt.Fprintf(os.Stderr, "ERROR: no subcommand is provided\n"); 159 | os.Exit(1) 160 | } 161 | subcmdName := os.Args[1] 162 | 163 | if subcmd, ok := Subcmds[subcmdName]; ok { 164 | os.Exit(subcmd.Run(os.Args[2:])) 165 | } else { 166 | topUsage(program) 167 | fmt.Fprintf(os.Stderr, "ERROR: unknown subcommand `%s`\n", subcmdName); 168 | os.Exit(1) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /cmd/gatekeeper/bex.go: -------------------------------------------------------------------------------- 1 | // Stolen from: https://gitlab.com/tsoding/bex/ 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "errors" 7 | "unicode" 8 | "strings" 9 | "strconv" 10 | ) 11 | 12 | type ExprType int 13 | 14 | const ( 15 | ExprVoid ExprType = iota 16 | ExprInt 17 | ExprStr 18 | ExprFuncall 19 | ) 20 | 21 | type Expr struct { 22 | Type ExprType 23 | AsInt int 24 | AsStr string 25 | AsFuncall Funcall 26 | } 27 | 28 | func NewExprStr(str string) Expr { 29 | return Expr{ 30 | Type: ExprStr, 31 | AsStr: str, 32 | } 33 | } 34 | 35 | func NewExprInt(num int) Expr { 36 | return Expr{ 37 | Type: ExprInt, 38 | AsInt: num, 39 | } 40 | } 41 | 42 | func ExprTypeName(typ ExprType) string { 43 | switch typ { 44 | case ExprVoid: 45 | return "Void" 46 | case ExprInt: 47 | return "Int" 48 | case ExprStr: 49 | return "Str" 50 | case ExprFuncall: 51 | return "Funcall" 52 | default: 53 | panic("unreachable") 54 | } 55 | } 56 | 57 | func (expr *Expr) Dump(level int) { 58 | for i := 0; i < level; i += 1 { 59 | fmt.Printf(" "); 60 | } 61 | 62 | switch expr.Type { 63 | case ExprVoid: 64 | fmt.Printf("Void\n"); 65 | case ExprInt: 66 | fmt.Printf("Int: %d\n", expr.AsInt); 67 | case ExprStr: 68 | // TODO: Expr.Dump() does not escape strings 69 | fmt.Printf("Str: \"%s\"\n", expr.AsStr); 70 | case ExprFuncall: 71 | fmt.Printf("Funcall: %s\n", expr.AsFuncall.Name) 72 | for _, arg := range expr.AsFuncall.Args { 73 | arg.Dump(level + 1) 74 | } 75 | } 76 | panic("unreachable") 77 | } 78 | 79 | func (expr *Expr) String() string { 80 | switch expr.Type { 81 | case ExprVoid: return "" 82 | case ExprInt: return fmt.Sprintf("%d", expr.AsInt) 83 | // TODO: Expr.String() does not escape string 84 | case ExprStr: return fmt.Sprintf("\"%s\"", expr.AsStr) 85 | case ExprFuncall: return expr.AsFuncall.String() 86 | } 87 | panic("unreachable") 88 | } 89 | 90 | type Funcall struct { 91 | Name string 92 | Args []Expr 93 | } 94 | 95 | func (funcall *Funcall) String() string { 96 | var result strings.Builder 97 | fmt.Fprintf(&result, "%s", funcall.Name) 98 | if len(funcall.Args) > 0 { 99 | fmt.Fprintf(&result, "(") 100 | for i, arg := range funcall.Args { 101 | if i > 0 { 102 | fmt.Fprintf(&result, ", ") 103 | } 104 | fmt.Fprintf(&result, "%s", arg.String()) 105 | } 106 | fmt.Fprintf(&result, ")") 107 | } 108 | return result.String() 109 | } 110 | 111 | func spanRunes(runes []rune, predicate func(rune) bool) ([]rune, []rune) { 112 | for i := range runes { 113 | if !predicate(runes[i]) { 114 | return runes[:i], runes[i:] 115 | } 116 | } 117 | return runes, []rune{} 118 | } 119 | 120 | func trimRunes(runes []rune) []rune { 121 | _, s := spanRunes(runes, unicode.IsSpace) 122 | return s 123 | } 124 | 125 | var EndOfSource = errors.New("EndOfSource") 126 | 127 | func parseFuncallArgs(sourceRunes []rune) ([]rune, []Expr, error) { 128 | args := []Expr{} 129 | 130 | sourceRunes = trimRunes(sourceRunes) 131 | if !(len(sourceRunes) > 0 && sourceRunes[0] == '(') { 132 | return sourceRunes, args, errors.New("Expected (") 133 | } 134 | sourceRunes = sourceRunes[1:] 135 | 136 | sourceRunes = trimRunes(sourceRunes) 137 | if len(sourceRunes) <= 0 { 138 | return sourceRunes, args, errors.New("Expected )") 139 | } 140 | 141 | if sourceRunes[0] == ')' { 142 | sourceRunes = sourceRunes[1:] 143 | return sourceRunes, args, nil 144 | } 145 | 146 | for { 147 | restRunes, arg, err := parseExpr(sourceRunes) 148 | args = append(args, arg) 149 | if err != nil { 150 | return restRunes, args, err 151 | } 152 | sourceRunes = restRunes 153 | 154 | sourceRunes = trimRunes(sourceRunes) 155 | if len(sourceRunes) <= 0 { 156 | return sourceRunes, args, errors.New("Expected )") 157 | } 158 | 159 | if sourceRunes[0] == ')' { 160 | sourceRunes = sourceRunes[1:] 161 | return sourceRunes, args, nil 162 | } 163 | 164 | if sourceRunes[0] != ',' { 165 | return sourceRunes, args, errors.New("Expected ,") 166 | } 167 | sourceRunes = sourceRunes[1:] 168 | sourceRunes = trimRunes(sourceRunes) 169 | } 170 | 171 | panic("parseFuncallArgs: unreachable") 172 | } 173 | 174 | func parseExpr(sourceRunes []rune) ([]rune, Expr, error) { 175 | sourceRunes = trimRunes(sourceRunes) 176 | expr := Expr{} 177 | if len(sourceRunes) > 0 { 178 | if sourceRunes[0] == '"' { 179 | expr.Type = ExprStr 180 | sourceRunes = sourceRunes[1:] 181 | literalRunes := []rune{} 182 | i := 0 183 | span: for i < len(sourceRunes) { 184 | switch sourceRunes[i] { 185 | case '"': 186 | break span 187 | case '\\': 188 | i += 1 189 | if i >= len(sourceRunes) { 190 | return sourceRunes[i:], expr, errors.New("Unfinished escape sequence") 191 | } 192 | // TODO: support all common escape sequences 193 | switch sourceRunes[i] { 194 | case 'n': 195 | literalRunes = append(literalRunes, '\n') 196 | i += 1 197 | case '\\': 198 | literalRunes = append(literalRunes, '\\') 199 | i += 1 200 | case '"': 201 | literalRunes = append(literalRunes, '"') 202 | i += 1 203 | default: 204 | return sourceRunes[i:], expr, errors.New(fmt.Sprintf("Unknown escape sequence starting with `%c`", sourceRunes[i])) 205 | } 206 | default: 207 | literalRunes = append(literalRunes, sourceRunes[i]) 208 | i += 1 209 | } 210 | } 211 | if i >= len(sourceRunes) { 212 | return sourceRunes[i:], expr, errors.New("Expected \"") 213 | } 214 | i += 1; 215 | sourceRunes = sourceRunes[i:] 216 | expr.AsStr = string(literalRunes) 217 | return sourceRunes, expr, nil 218 | } else if unicode.IsDigit(sourceRunes[0]) { 219 | expr.Type = ExprInt 220 | digits, restRunes := spanRunes(sourceRunes, func(x rune) bool { return unicode.IsDigit(x) }) 221 | sourceRunes = restRunes 222 | val, err := strconv.ParseInt(string(digits), 10, 32) 223 | if err != nil { 224 | return sourceRunes, Expr{}, err 225 | } 226 | expr.AsInt = int(val) 227 | return sourceRunes, expr, nil 228 | } else if unicode.IsLetter(sourceRunes[0]) { 229 | name, restRunes := spanRunes(sourceRunes, func(x rune) bool { 230 | return unicode.IsLetter(x) || unicode.IsDigit(x) || x == '_' 231 | }) 232 | sourceRunes = restRunes 233 | 234 | expr.Type = ExprFuncall 235 | expr.AsFuncall.Name = string(name) 236 | 237 | sourceRunes = trimRunes(sourceRunes) 238 | if len(sourceRunes) > 0 && sourceRunes[0] == '(' { 239 | restRunes, funcallArgs, err := parseFuncallArgs(sourceRunes) 240 | sourceRunes = restRunes 241 | expr.AsFuncall.Args = funcallArgs 242 | return restRunes, expr, err 243 | } 244 | 245 | return sourceRunes, expr, nil 246 | } else { 247 | return sourceRunes, Expr{}, errors.New(fmt.Sprintf("Unexpected character %q", sourceRunes[0])) 248 | } 249 | } 250 | 251 | return sourceRunes, Expr{}, EndOfSource 252 | } 253 | 254 | func ParseAllExprs(source string) ([]Expr, error) { 255 | sourceRunes := []rune(source) 256 | exprs := []Expr{} 257 | for { 258 | restRunes, expr, err := parseExpr(sourceRunes) 259 | if err != nil { 260 | if err == EndOfSource { 261 | err = nil 262 | } 263 | return exprs, err 264 | } 265 | sourceRunes = restRunes 266 | exprs = append(exprs, expr) 267 | } 268 | } 269 | 270 | type EvalScope struct { 271 | Funcs map[string]Func 272 | } 273 | 274 | type EvalContext struct { 275 | Scopes []EvalScope 276 | EvalPoints int 277 | } 278 | 279 | func (context *EvalContext) LookUpFunc(name string) (Func, bool) { 280 | scopes := context.Scopes 281 | for len(scopes) > 0 { 282 | n := len(scopes) 283 | fun, ok := scopes[n-1].Funcs[name] 284 | if ok { 285 | return fun, true 286 | } 287 | scopes = scopes[:n-1] 288 | } 289 | return nil, false 290 | } 291 | 292 | func (context *EvalContext) PushScope(scope EvalScope) { 293 | context.Scopes = append(context.Scopes, scope) 294 | } 295 | 296 | func (context *EvalContext) PopScope() { 297 | n := len(context.Scopes) 298 | context.Scopes = context.Scopes[0:n-1] 299 | } 300 | 301 | type Func = func(context *EvalContext, args []Expr) (Expr, error) 302 | 303 | func (context *EvalContext) EvalExpr(expr Expr) (Expr, error) { 304 | if context.EvalPoints <= 0 { 305 | return Expr{}, errors.New(fmt.Sprintf("This expression is too complicated for you")); 306 | } 307 | context.EvalPoints -= 1; 308 | 309 | switch expr.Type { 310 | case ExprVoid, ExprInt, ExprStr: 311 | return expr, nil 312 | case ExprFuncall: 313 | fun, ok := context.LookUpFunc(expr.AsFuncall.Name) 314 | if !ok { 315 | return Expr{}, errors.New(fmt.Sprintf("Unknown function `%s`", expr.AsFuncall.Name)) 316 | } 317 | return fun(context, expr.AsFuncall.Args) 318 | } 319 | panic("unreachable") 320 | } 321 | 322 | func (context *EvalContext) EvalExprs(exprs []Expr) (result Expr, err error) { 323 | for _, expr := range exprs { 324 | result, err = context.EvalExpr(expr) 325 | if err != nil { 326 | return 327 | } 328 | } 329 | return 330 | } 331 | -------------------------------------------------------------------------------- /cmd/gatekeeper/command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "github.com/tsoding/gatekeeper/internal" 8 | "io/ioutil" 9 | "log" 10 | "math/rand" 11 | "net/http" 12 | "net/url" 13 | "regexp" 14 | "runtime/debug" 15 | "time" 16 | "strings" 17 | "strconv" 18 | "math" 19 | ) 20 | 21 | var ( 22 | // TODO: make the CommandPrefix configurable from the database, so we can set it per instance 23 | CommandPrefix = "[\\$\\!]" 24 | CommandDef = "([a-zA-Z0-9\\-_]+)( +(.*))?" 25 | CommandRegexp = regexp.MustCompile("^ *("+CommandPrefix+") *"+CommandDef+"$") 26 | CommandNoPrefixRegexp = regexp.MustCompile("^ *"+CommandDef+"$") 27 | Commit = func() string { 28 | if info, ok := debug.ReadBuildInfo(); ok { 29 | for _, setting := range info.Settings { 30 | if setting.Key == "vcs.revision" { 31 | return setting.Value 32 | } 33 | } 34 | } 35 | return "" 36 | }() 37 | ) 38 | 39 | type Command struct { 40 | Prefix string 41 | Name string 42 | Args string 43 | } 44 | 45 | func parseCommand(source string) (Command, bool) { 46 | matches := CommandRegexp.FindStringSubmatch(source) 47 | if len(matches) == 0 { 48 | return Command{}, false 49 | } 50 | return Command{ 51 | Prefix: matches[1], 52 | Name: matches[2], 53 | Args: matches[4], 54 | }, true 55 | } 56 | 57 | type CommandEnvironment interface { 58 | AtAdmin() string 59 | AtAuthor() string 60 | AuthorUserId() string 61 | IsAuthorAdmin() bool 62 | AsDiscord() *DiscordEnvironment 63 | SendMessage(message string) 64 | } 65 | 66 | type CyrillifyEnvironment struct { 67 | InnerEnv CommandEnvironment 68 | } 69 | 70 | func (env *CyrillifyEnvironment) AuthorUserId() string { 71 | return env.InnerEnv.AuthorUserId() 72 | } 73 | 74 | func (env *CyrillifyEnvironment) AsDiscord() *DiscordEnvironment { 75 | return env.InnerEnv.AsDiscord() 76 | } 77 | 78 | func (env *CyrillifyEnvironment) AtAdmin() string { 79 | return env.InnerEnv.AtAdmin() 80 | } 81 | 82 | func (env *CyrillifyEnvironment) AtAuthor() string { 83 | return env.InnerEnv.AtAuthor() 84 | } 85 | 86 | func (env *CyrillifyEnvironment) IsAuthorAdmin() bool { 87 | return env.InnerEnv.IsAuthorAdmin() 88 | } 89 | 90 | func (env CyrillifyEnvironment) SendMessage(message string) { 91 | env.InnerEnv.SendMessage(Cyrillify(message)) 92 | } 93 | 94 | func Cyrillify(message string) string { 95 | result := []rune{} 96 | for _, x := range []rune(message) { 97 | if y, ok := CyrilMap[x]; ok { 98 | result = append(result, y) 99 | } else { 100 | result = append(result, x) 101 | } 102 | } 103 | return string(result) 104 | } 105 | 106 | var CyrilMap = map[rune]rune{ 107 | 'a': 'д', 108 | 'e': 'ё', 109 | 'b': 'б', 110 | 'h': 'н', 111 | 'k': 'к', 112 | 'm': 'м', 113 | 'n': 'п', 114 | 'o': 'ф', 115 | 'r': 'г', 116 | 't': 'т', 117 | 'u': 'ц', 118 | 'x': 'ж', 119 | 'w': 'ш', 120 | 'A': 'Д', 121 | 'G': 'Б', 122 | 'E': 'Ё', 123 | 'N': 'Й', 124 | 'O': 'Ф', 125 | 'R': 'Я', 126 | 'U': 'Ц', 127 | 'W': 'Ш', 128 | 'X': 'Ж', 129 | 'Y': 'У', 130 | } 131 | 132 | 133 | func fancyRune(chr rune) rune { 134 | if chr >= 'A' && chr <= 'Z' { 135 | return '𝓐' + chr - 'A' 136 | } 137 | 138 | if chr >= 'a' && chr <= 'z' { 139 | return '𝓪' + chr - 'a' 140 | } 141 | 142 | return chr 143 | } 144 | 145 | func fancyString(peasantString string) string { 146 | fancyRunes := make([]rune, len(peasantString)) 147 | 148 | for i, peasantRune := range peasantString { 149 | fancyRunes[i] = fancyRune(peasantRune) 150 | } 151 | 152 | return string(fancyRunes) 153 | } 154 | 155 | var discordEmojiRegex = regexp.MustCompile(`(<:[a-zA-Z_0-9]+:[0-9]+>)`) 156 | 157 | func fancyDiscordMessage(peasantMessage string) string { 158 | peasantEmojis := discordEmojiRegex.FindAllString(peasantMessage, -1) 159 | peasantNonEmojis := discordEmojiRegex.Split(peasantMessage, -1) 160 | 161 | i, j := 0, 0 162 | 163 | fancyResult := []string{} 164 | 165 | for i < len(peasantNonEmojis) { 166 | fancyResult = append(fancyResult, fancyString(peasantNonEmojis[i])) 167 | i++ 168 | 169 | if j < len(peasantEmojis) { 170 | fancyResult = append(fancyResult, peasantEmojis[j]) 171 | j++ 172 | } 173 | } 174 | 175 | return strings.Join(fancyResult, "") 176 | } 177 | 178 | func EvalContextFromCommandEnvironment(env CommandEnvironment, command Command, count int64) EvalContext { 179 | return EvalContext{ 180 | EvalPoints: 100, 181 | Scopes: []EvalScope{ 182 | EvalScope{ 183 | Funcs: map[string]Func{ 184 | "count": func(context *EvalContext, args []Expr) (Expr, error) { 185 | return NewExprInt(int(count)), nil 186 | }, 187 | "days_left_until": func(context *EvalContext, args []Expr) (Expr, error) { 188 | if len(args) != 1 { 189 | return Expr{}, fmt.Errorf("Expected 1 arguments") 190 | } 191 | result, err := context.EvalExpr(args[0]) 192 | if err != nil { 193 | return Expr{}, err 194 | } 195 | if result.Type != ExprStr { 196 | return Expr{}, fmt.Errorf("%s is not a String. Expected a String in a format YYYY-MM-DD.", result.String()) 197 | } 198 | date, err := time.Parse("2006-01-02", result.AsStr) 199 | if err != nil { 200 | return Expr{}, fmt.Errorf("`%s` is not a valid date. Expected format YYYY-MM-DD.", result.AsStr) 201 | } 202 | return NewExprInt(int(math.Ceil(date.Sub(time.Now()).Hours()/24))), nil 203 | }, 204 | "twitch_or_discord": func(context *EvalContext, args []Expr) (result Expr, err error) { 205 | if len(args) != 2 { 206 | return Expr{}, fmt.Errorf("Expected 2 arguments") 207 | } 208 | 209 | if env.AsDiscord() == nil { 210 | result, err = context.EvalExpr(args[0]) 211 | } else { 212 | result, err = context.EvalExpr(args[1]) 213 | } 214 | return 215 | }, 216 | "input": func(context *EvalContext, args []Expr) (Expr, error) { 217 | if len(args) > 0 { 218 | return Expr{}, fmt.Errorf("Too many arguments") 219 | } 220 | return NewExprStr(command.Args), nil 221 | }, 222 | "replace": func(context *EvalContext, args[]Expr) (Expr, error) { 223 | arity := 3; 224 | if len(args) != arity { 225 | return Expr{}, fmt.Errorf("replace: Expected %d arguments but got %d", arity, len(args)) 226 | } 227 | 228 | regExpr, err := context.EvalExpr(args[0]) 229 | if err != nil { 230 | return Expr{}, err 231 | } 232 | if regExpr.Type != ExprStr { 233 | return Expr{}, fmt.Errorf("replace: Argument 1 is expected to be %s, but got %s", ExprTypeName(ExprStr), ExprTypeName(regExpr.Type)) 234 | } 235 | 236 | srcExpr, err := context.EvalExpr(args[1]) 237 | if err != nil { 238 | return Expr{}, err 239 | } 240 | if srcExpr.Type != ExprStr { 241 | return Expr{}, fmt.Errorf("replace: Argument 2 is expected to be %s, but got %s", ExprTypeName(ExprStr), ExprTypeName(srcExpr.Type)) 242 | } 243 | 244 | replExpr, err := context.EvalExpr(args[2]) 245 | if err != nil { 246 | return Expr{}, err 247 | } 248 | if replExpr.Type != ExprStr { 249 | return Expr{}, fmt.Errorf("replace: Argument 3 is expected to be %s, but got %s", ExprTypeName(ExprStr), ExprTypeName(replExpr.Type)) 250 | } 251 | 252 | reg, err := regexp.Compile(regExpr.AsStr); 253 | if err != nil { 254 | return Expr{}, fmt.Errorf("replace: Could not compile regexp `%s`: %w", regExpr.AsStr, err) 255 | } 256 | 257 | return NewExprStr(string(reg.ReplaceAll([]byte(srcExpr.AsStr), []byte(replExpr.AsStr)))), nil 258 | }, 259 | "year": func(context *EvalContext, args []Expr) (Expr, error) { 260 | if len(args) > 0 { 261 | return Expr{}, fmt.Errorf("Too many arguments"); 262 | } 263 | return NewExprInt(time.Now().Year()), nil 264 | }, 265 | "do": func(context *EvalContext, args []Expr) (result Expr, err error) { 266 | for _, arg := range args { 267 | result, err := context.EvalExpr(arg) 268 | if err != nil { 269 | return result, err 270 | } 271 | } 272 | return Expr{}, nil 273 | }, 274 | "concat": func(context *EvalContext, args []Expr) (Expr, error) { 275 | sb := strings.Builder{} 276 | for _, arg := range args { 277 | result, err := context.EvalExpr(arg) 278 | if err != nil { 279 | return result, err 280 | } 281 | switch result.Type { 282 | case ExprVoid: 283 | case ExprInt: sb.WriteString(strconv.Itoa(result.AsInt)) 284 | case ExprStr: sb.WriteString(result.AsStr) 285 | case ExprFuncall: return Expr{}, fmt.Errorf("`%s` is neither String nor Integer") 286 | } 287 | } 288 | return NewExprStr(sb.String()), nil 289 | }, 290 | "add": func(context *EvalContext, args []Expr) (Expr, error) { 291 | sum := 0 292 | for _, arg := range args { 293 | result, err := context.EvalExpr(arg) 294 | if err != nil { 295 | return result, err 296 | } 297 | if result.Type != ExprInt { 298 | return Expr{}, fmt.Errorf("%s is not an integer", result.String()) 299 | } 300 | sum += result.AsInt 301 | } 302 | return NewExprInt(sum), nil 303 | }, 304 | "sub": func(context *EvalContext, args []Expr) (Expr, error) { 305 | if len(args) == 0 { 306 | return NewExprInt(0), nil 307 | } 308 | first, err := context.EvalExpr(args[0]) 309 | if err != nil { 310 | return first, err 311 | } 312 | if first.Type != ExprInt { 313 | return Expr{}, fmt.Errorf("%s is not an integer", first.String()) 314 | } 315 | if len(args) == 1 { 316 | return NewExprInt(-first.AsInt), nil 317 | } 318 | sum := first.AsInt 319 | for _, arg := range args[1:] { 320 | result, err := context.EvalExpr(arg) 321 | if err != nil { 322 | return result, err 323 | } 324 | if result.Type != ExprInt { 325 | return Expr{}, fmt.Errorf("%s is not an integer", result.String()) 326 | } 327 | sum -= result.AsInt 328 | } 329 | return NewExprInt(sum), nil 330 | }, 331 | "author": func(context *EvalContext, args []Expr) (Expr, error) { 332 | if len(args) > 0 { 333 | return Expr{}, fmt.Errorf("Too many arguments"); 334 | } 335 | return NewExprStr(env.AtAuthor()), nil 336 | }, 337 | "or": func(context *EvalContext, args []Expr) (Expr, error) { 338 | for _, arg := range args { 339 | result, err := context.EvalExpr(arg) 340 | if err != nil { 341 | return Expr{}, err 342 | } 343 | switch result.Type { 344 | case ExprInt: 345 | if result.AsInt != 0 { 346 | return result, nil 347 | } 348 | case ExprStr: 349 | if len(result.AsStr) != 0 { 350 | return result, nil 351 | } 352 | case ExprFuncall: 353 | return result, nil 354 | } 355 | } 356 | return Expr{}, nil 357 | }, 358 | "uppercase": func(context *EvalContext, args []Expr) (Expr, error) { 359 | sb := strings.Builder{} 360 | for _, arg := range args { 361 | result, err := context.EvalExpr(arg) 362 | if err != nil { 363 | return Expr{}, err 364 | } 365 | 366 | switch result.Type { 367 | case ExprVoid: 368 | case ExprInt: 369 | sb.WriteString(strconv.Itoa(result.AsInt)) 370 | case ExprStr: 371 | sb.WriteString(strings.ToUpper(result.AsStr)); 372 | default: 373 | return Expr{}, fmt.Errorf("%s evaluated into %s which is neither Int, Str, nor Void. `uppercase` command cannot display that.", arg.String(), result.String()); 374 | } 375 | } 376 | 377 | return NewExprStr(sb.String()), nil 378 | }, 379 | "urlencode": func(context *EvalContext, args []Expr) (Expr, error) { 380 | sb := strings.Builder{} 381 | for _, arg := range args { 382 | result, err := context.EvalExpr(arg) 383 | if err != nil { 384 | return Expr{}, err 385 | } 386 | 387 | switch result.Type { 388 | case ExprVoid: 389 | case ExprInt: 390 | sb.WriteString(strconv.Itoa(result.AsInt)) 391 | case ExprStr: 392 | sb.WriteString(result.AsStr); 393 | default: 394 | return Expr{}, fmt.Errorf("%s evaluated into %s which is neither Int, Str, nor Void. `urlencode` command cannot display that.", arg.String(), result.String()); 395 | } 396 | } 397 | return NewExprStr(url.PathEscape(sb.String())), nil 398 | }, 399 | "say": func(context *EvalContext, args []Expr) (Expr, error) { 400 | sb := strings.Builder{} 401 | for _, arg := range args { 402 | result, err := context.EvalExpr(arg) 403 | if err != nil { 404 | return Expr{}, err 405 | } 406 | 407 | switch result.Type { 408 | case ExprVoid: 409 | case ExprInt: 410 | sb.WriteString(strconv.Itoa(result.AsInt)) 411 | case ExprStr: 412 | sb.WriteString(result.AsStr); 413 | default: 414 | return Expr{}, fmt.Errorf("%s evaluated into %s which is neither Int, Str, nor Void. `say` command cannot display that.", arg.String(), result.String()); 415 | } 416 | } 417 | env.SendMessage(sb.String()) 418 | return Expr{}, nil 419 | }, 420 | "discord": func(context *EvalContext, args []Expr) (result Expr, err error) { 421 | if env.AsDiscord() == nil { 422 | env.SendMessage(env.AtAuthor() + " This command is only for discord, sorry") 423 | return 424 | } 425 | result, err = context.EvalExprs(args) 426 | return 427 | }, 428 | "choice": func(context *EvalContext, args []Expr) (result Expr, err error) { 429 | if len(args) <= 0 { 430 | return Expr{}, fmt.Errorf("Can't choose among zero options") 431 | } 432 | return context.EvalExpr(args[rand.Intn(len(args))]) 433 | }, 434 | "let": func(context *EvalContext, args []Expr) (result Expr, err error) { 435 | if len(args) <= 0 { 436 | return Expr{}, nil 437 | } 438 | binds := args[:len(args)-1] 439 | body := args[len(args)-1] 440 | context.PushScope(EvalScope{ 441 | Funcs: map[string]Func{}, 442 | }) 443 | defer context.PopScope() 444 | scope := &context.Scopes[len(context.Scopes)-1] 445 | for _, bind := range binds { 446 | if bind.Type != ExprFuncall { 447 | return Expr{}, fmt.Errorf("`%s` is not a Funcall. Bindings must be Funcalls. For example: let(x(34), y(35), say(add(x, y))).", bind.String()) 448 | } 449 | value := Expr{} 450 | for _, arg := range bind.AsFuncall.Args { 451 | value, err = context.EvalExpr(arg) 452 | if err != nil { 453 | return Expr{}, err 454 | } 455 | } 456 | _, exists := context.LookUpFunc(bind.AsFuncall.Name) 457 | if exists { 458 | return Expr{}, fmt.Errorf("Redefinition of the let-binding `%s`", bind.AsFuncall.Name) 459 | } 460 | scope.Funcs[bind.AsFuncall.Name] = func(context *EvalContext, args []Expr) (Expr, error) { 461 | if len(args) > 0 { 462 | return Expr{}, fmt.Errorf("Let binding `%s` accepts 0 arguments, but you provided", bind.AsFuncall.Name, len(args)) 463 | } 464 | return value, nil 465 | }; 466 | } 467 | if body.Type == ExprFuncall && body.AsFuncall.Name != "do" { 468 | return Expr{}, fmt.Errorf("Wrap `%s` in `do(%s)`", body.String(), body.String()) 469 | } 470 | return context.EvalExpr(body) 471 | }, 472 | "fancy": func(context *EvalContext, args []Expr) (result Expr, err error) { 473 | sb := strings.Builder{} 474 | for _, arg := range args { 475 | result, err := context.EvalExpr(arg) 476 | if err != nil { 477 | return Expr{}, err 478 | } 479 | 480 | switch result.Type { 481 | case ExprVoid: 482 | case ExprInt: 483 | sb.WriteString(strconv.Itoa(result.AsInt)) 484 | case ExprStr: 485 | if env.AsDiscord() == nil { 486 | sb.WriteString(fancyString(result.AsStr)); 487 | } else { 488 | sb.WriteString(fancyDiscordMessage(result.AsStr)); 489 | } 490 | default: 491 | return Expr{}, fmt.Errorf("%s evaluated into %s which is neither Int, Str, nor Void. `fancy` command cannot display that.", arg.String(), result.String()); 492 | } 493 | } 494 | return NewExprStr(sb.String()), nil 495 | }, 496 | }, 497 | }, 498 | }, 499 | } 500 | } 501 | 502 | func EvalBuiltinCommand(db *sql.DB, command Command, env CommandEnvironment, context EvalContext) { 503 | switch command.Name { 504 | case "bottomspammers": 505 | discordEnv := env.AsDiscord() 506 | if discordEnv == nil { 507 | env.SendMessage(env.AtAuthor() + " This command only works in Discord, sorry") 508 | return 509 | } 510 | 511 | name := strings.TrimSpace(command.Args) 512 | 513 | if len(name) == 0 { 514 | rows, err := db.Query("select user_name, count(text) as count from discord_log group by user_name order by count asc limit 10"); 515 | if err != nil { 516 | log.Printf("%s\n", err) 517 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 518 | return 519 | } 520 | defer rows.Close() 521 | 522 | sb := strings.Builder{} 523 | for index := 1; rows.Next(); index += 1 { 524 | var userName string 525 | var count int 526 | err := rows.Scan(&userName, &count) 527 | if err != nil { 528 | log.Printf("%s\n", err) 529 | } else { 530 | sb.WriteString(fmt.Sprintf("%d. %s (%d)\n", index, userName, count)) 531 | } 532 | } 533 | env.SendMessage(env.AtAuthor() + " Bottom Spammers:\n"+sb.String()) 534 | } else { 535 | rows, err := db.Query("select user_name, count(text) as count from discord_log where user_name = $1 group by user_name;", name); 536 | if err != nil { 537 | log.Printf("%s\n", err) 538 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 539 | return 540 | } 541 | defer rows.Close() 542 | 543 | sb := strings.Builder{} 544 | for rows.Next() { 545 | var userName string 546 | var count int 547 | err := rows.Scan(&userName, &count) 548 | if err != nil { 549 | log.Printf("%s\n", err) 550 | } else { 551 | sb.WriteString(fmt.Sprintf("%s (%d)\n", userName, count)) 552 | } 553 | } 554 | env.SendMessage(env.AtAuthor() + " " + sb.String()) 555 | } 556 | case "topspammers": 557 | discordEnv := env.AsDiscord() 558 | if discordEnv == nil { 559 | env.SendMessage(env.AtAuthor() + " This command only works in Discord, sorry") 560 | return 561 | } 562 | 563 | name := strings.TrimSpace(command.Args) 564 | 565 | if len(name) == 0 { 566 | rows, err := db.Query("select user_name, count(text) as count from discord_log group by user_name order by count desc limit 10"); 567 | if err != nil { 568 | log.Printf("%s\n", err) 569 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 570 | return 571 | } 572 | defer rows.Close() 573 | 574 | sb := strings.Builder{} 575 | for index := 1; rows.Next(); index += 1 { 576 | var userName string 577 | var count int 578 | err := rows.Scan(&userName, &count) 579 | if err != nil { 580 | log.Printf("%s\n", err) 581 | } else { 582 | sb.WriteString(fmt.Sprintf("%d. %s (%d)\n", index, userName, count)) 583 | } 584 | } 585 | env.SendMessage(env.AtAuthor() + " Top Spammers:\n"+sb.String()) 586 | } else { 587 | rows, err := db.Query("select user_name, count(text) as count from discord_log where user_name = $1 group by user_name;", name); 588 | if err != nil { 589 | log.Printf("%s\n", err) 590 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 591 | return 592 | } 593 | defer rows.Close() 594 | 595 | sb := strings.Builder{} 596 | for rows.Next() { 597 | var userName string 598 | var count int 599 | err := rows.Scan(&userName, &count) 600 | if err != nil { 601 | log.Printf("%s\n", err) 602 | } else { 603 | sb.WriteString(fmt.Sprintf("%s (%d)\n", userName, count)) 604 | } 605 | } 606 | env.SendMessage(env.AtAuthor() + " " + sb.String()) 607 | } 608 | case "actualban": 609 | if !env.IsAuthorAdmin() { 610 | env.SendMessage(env.AtAuthor() + " only for " + env.AtAdmin()) 611 | return 612 | } 613 | 614 | discordEnv := env.AsDiscord() 615 | if discordEnv == nil { 616 | env.SendMessage(env.AtAuthor() + " This command only works in Discord, sorry") 617 | return 618 | } 619 | 620 | prefix := strings.TrimSpace(command.Args) 621 | 622 | if len(prefix) == 0 { 623 | env.SendMessage(env.AtAuthor() + " Prefix cannot be empty") 624 | return 625 | } 626 | 627 | st, err := discordEnv.dg.GuildMembersSearch(discordEnv.m.GuildID, prefix, 1000); 628 | if err != nil { 629 | log.Printf("%s\n", err) 630 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 631 | return 632 | } 633 | 634 | if len(st) == 0 { 635 | break 636 | } 637 | 638 | for i := range st { 639 | err = discordEnv.dg.GuildBanCreate(discordEnv.m.GuildID, st[i].User.ID, 0) 640 | if err != nil { 641 | log.Printf("%s\n", err) 642 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 643 | return 644 | } 645 | 646 | env.SendMessage(env.AtAuthor() + " " + st[i].User.Username + " is banned") 647 | } 648 | 649 | env.SendMessage(env.AtAuthor() + " Done 🙂") 650 | case "song": 651 | song := LastSongPlayed(db) 652 | if song != nil { 653 | env.SendMessage(env.AtAuthor() + " " + fmt.Sprintf("🎶 🎵 Last Song: \"%s\" by %s 🎵 🎶", song.title, song.artist)) 654 | } else { 655 | env.SendMessage(env.AtAuthor() + " No song has been played so far") 656 | } 657 | case "search": 658 | if !env.IsAuthorAdmin() { 659 | env.SendMessage(env.AtAuthor() + " only for " + env.AtAdmin()) 660 | return 661 | } 662 | 663 | discordEnv := env.AsDiscord() 664 | if discordEnv == nil { 665 | env.SendMessage(env.AtAuthor() + " This command only works in Discord, sorry") 666 | return 667 | } 668 | 669 | prefix := command.Args 670 | st, err := discordEnv.dg.GuildMembersSearch(discordEnv.m.GuildID, prefix, 1000); 671 | if err != nil { 672 | log.Printf("%s\n", err) 673 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 674 | return 675 | } 676 | 677 | env.SendMessage(env.AtAuthor() + " There are "+strconv.Itoa(len(st))+" members that start with "+prefix); 678 | if 0 < len(st) && len(st) <= 100 { 679 | sb := strings.Builder{} 680 | for _, s := range st { 681 | sb.WriteString(s.User.Username) 682 | sb.WriteString(" ") 683 | } 684 | env.SendMessage("Their names are: "+sb.String()); 685 | } 686 | case "edlimit": 687 | env.SendMessage(fmt.Sprintf("%s Line Count: %d, Line Size: %d", env.AtAuthor(), EdLineCountLimit, EdLineSizeLimit)) 688 | return; 689 | case "ed": 690 | userId := env.AuthorUserId() 691 | ed, err := LoadEdStateByUserId(db, userId) 692 | if err != nil { 693 | log.Printf("Could not load Ed_State of user %s: %s\n", userId, err) 694 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 695 | return 696 | } 697 | ed.ExecCommand(env, command.Args); 698 | err = SaveEdStateByUserId(db, userId, ed) 699 | if err != nil { 700 | log.Printf("Could not save %#v of user %s: %s\n", ed, userId, err) 701 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 702 | return 703 | } 704 | return 705 | case "showcmd": 706 | regexp.MustCompile("^"+CommandDef+"$") 707 | matches := CommandNoPrefixRegexp.FindStringSubmatch(command.Args) 708 | if len(matches) == 0 { 709 | // TODO: give more info on the syntactic error to the user 710 | env.SendMessage(env.AtAuthor() + " syntax error") 711 | return 712 | } 713 | 714 | name := matches[1] 715 | row := db.QueryRow("SELECT bex FROM commands WHERE name = $1", name); 716 | var bex string 717 | err := row.Scan(&bex) 718 | if err == sql.ErrNoRows { 719 | env.SendMessage(fmt.Sprintf("%s command %s does not exist", env.AtAuthor(), name)) 720 | return 721 | } 722 | if err != nil { 723 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 724 | log.Printf("Error while querying command %s: %s\n", command.Name, err); 725 | return 726 | } 727 | env.SendMessage(fmt.Sprintf("%s %s", env.AtAuthor(), bex)) 728 | case "addcmd": 729 | fallthrough 730 | case "updcmd": 731 | if !env.IsAuthorAdmin() { 732 | env.SendMessage(env.AtAuthor() + " only for " + env.AtAdmin()) 733 | return 734 | } 735 | 736 | regexp.MustCompile("^"+CommandDef+"$") 737 | matches := CommandNoPrefixRegexp.FindStringSubmatch(command.Args) 738 | if len(matches) == 0 { 739 | // TODO: give more info on the syntactic error to the user 740 | env.SendMessage(env.AtAuthor() + " syntax error") 741 | return 742 | } 743 | 744 | name := matches[1] 745 | bex := matches[3] 746 | 747 | _, err := db.Exec("INSERT INTO Commands (name, bex) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET bex = EXCLUDED.bex;", name, bex); 748 | if err != nil { 749 | log.Printf("Could not update command %s: %s\n", name, err) 750 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 751 | return 752 | } 753 | // TODO: report "added" instead of "updated" when the command didn't exist but was newly created 754 | env.SendMessage(fmt.Sprintf("%s command %s is updated", env.AtAuthor(), name)) 755 | case "delcmd": 756 | if !env.IsAuthorAdmin() { 757 | env.SendMessage(env.AtAuthor() + " only for " + env.AtAdmin()) 758 | return 759 | } 760 | 761 | regexp.MustCompile("^"+CommandDef+"$") 762 | matches := CommandNoPrefixRegexp.FindStringSubmatch(command.Args) 763 | if len(matches) == 0 { 764 | // TODO: give more info on the syntactic error to the user 765 | env.SendMessage(env.AtAuthor() + " syntax error") 766 | return 767 | } 768 | 769 | name := matches[1] 770 | _, err := db.Exec("DELETE FROM commands WHERE name = $1", name); 771 | if err != nil { 772 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 773 | log.Printf("Error while querying command %s: %s\n", command.Name, err); 774 | return 775 | } 776 | // TODO: report "does not exist" when the deleted command didn't exist 777 | env.SendMessage(fmt.Sprintf("%s deleted %s", env.AtAuthor(), name)) 778 | return 779 | case "eval": 780 | if !env.IsAuthorAdmin() { 781 | env.SendMessage(env.AtAuthor() + " only for " + env.AtAdmin()) 782 | return 783 | } 784 | exprs, err := ParseAllExprs(command.Args) 785 | if err != nil { 786 | env.SendMessage(fmt.Sprintf("%s could not parse expression `%s`: %s", env.AtAuthor(), command.Args, err)) 787 | return 788 | } 789 | if len(exprs) == 0 { 790 | env.SendMessage(fmt.Sprintf("%s no expressions were provided for evaluation", env.AtAuthor())) 791 | return 792 | } 793 | for _, expr := range exprs { 794 | _, err := context.EvalExpr(expr) 795 | if err != nil { 796 | env.SendMessage(fmt.Sprintf("%s could not evaluate expression `%s`: %s", env.AtAuthor(), command.Args, err)) 797 | return 798 | } 799 | } 800 | // TODO: uncarrot discord message by its id 801 | case "carrot": 802 | if db == nil { 803 | // TODO: add some sort of cooldown for the @admin pings 804 | env.SendMessage(env.AtAuthor() + " Something went wrong with the database. Commands that require it won't work. Please ask " + env.AtAdmin() + " to check the logs") 805 | return 806 | } 807 | 808 | message, err := internal.CarrotsonGenerate(db, command.Args, 256) 809 | if err != nil { 810 | log.Printf("%s\n", err) 811 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 812 | return 813 | } 814 | 815 | env.SendMessage(env.AtAuthor() + " " + maskDiscordPings(message)) 816 | case "profile": 817 | if !env.IsAuthorAdmin() { 818 | env.SendMessage(env.AtAuthor() + " only for " + env.AtAdmin()) 819 | return 820 | } 821 | 822 | innerCommand, ok := parseCommand(command.Args) 823 | if !ok { 824 | env.SendMessage(env.AtAuthor() + " failed to parse inner command") 825 | return 826 | } 827 | start := time.Now() 828 | EvalCommand(db, innerCommand, env) 829 | elapsed := time.Since(start) 830 | env.SendMessage(env.AtAuthor() + " `" + command.Args + "` took " + elapsed.String() + " to executed") 831 | case "cyril": 832 | innerCommand, ok := parseCommand(command.Args) 833 | if !ok { 834 | env.SendMessage(Cyrillify(command.Args)) 835 | } else { 836 | EvalCommand(db, innerCommand, &CyrillifyEnvironment{ 837 | InnerEnv: env, 838 | }) 839 | } 840 | case "weather": 841 | place := command.Args 842 | 843 | var response string 844 | var err error 845 | if len(place) > 0 { 846 | response, err = checkWeatherOf(place) 847 | if err == PlaceNotFound { 848 | response = "Could not find `" + place + "`" 849 | } else if err == SomebodyTryingToHackWeather { 850 | response = "Are you trying to hack me or something? ._." 851 | } else if err != nil { 852 | response = "Something went wrong while querying the weather for `" + place + "`. " + env.AtAdmin() + " please check the logs." 853 | log.Println("Error while checking the weather for `"+place+"`:", err) 854 | } 855 | } else { 856 | response = "No place is provided for the weather command" 857 | } 858 | 859 | env.SendMessage(env.AtAuthor() + " " + response) 860 | case "version": 861 | env.SendMessage(env.AtAuthor() + " " + Commit) 862 | case "count": 863 | if db == nil { 864 | env.SendMessage(env.AtAuthor() + " Something went wrong with the database. Commands that require it won't work. Please ask " + env.AtAdmin() + " to check the logs") 865 | return 866 | } 867 | 868 | discordEnv := env.AsDiscord() 869 | if discordEnv == nil { 870 | env.SendMessage(env.AtAuthor() + " This command only works in Discord, sorry") 871 | return 872 | } 873 | 874 | if !isMemberTrusted(discordEnv.m.Member) { 875 | env.SendMessage(env.AtAuthor() + " Only trusted users can trust others") 876 | return 877 | } 878 | count, err := TrustedTimesOfUser(db, discordEnv.m.Author) 879 | if err != nil { 880 | log.Println("Could not get amount of trusted times:", err) 881 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 882 | return 883 | } 884 | if count > MaxTrustedTimes { 885 | env.SendMessage(fmt.Sprintf("%s Used %d out of %d trusts <:tsodinSus:940724160680845373>", env.AtAuthor(), count, MaxTrustedTimes)) 886 | } else { 887 | env.SendMessage(fmt.Sprintf("%s Used %d out of %d trusts", env.AtAuthor(), count, MaxTrustedTimes)) 888 | } 889 | /* 890 | case "trust": 891 | if db == nil { 892 | env.SendMessage(env.AtAuthor() + " Something went wrong with the database. Commands that require it won't work. Please ask " + env.AtAdmin() + " to check the logs") 893 | return 894 | } 895 | 896 | discordEnv := env.AsDiscord() 897 | if discordEnv == nil { 898 | env.SendMessage(env.AtAuthor() + " This command only works in Discord, sorry") 899 | return 900 | } 901 | 902 | if !isMemberTrusted(discordEnv.m.Member) { 903 | env.SendMessage(env.AtAuthor() + " Only trusted users can trust others") 904 | return 905 | } 906 | 907 | if len(discordEnv.m.Mentions) == 0 { 908 | env.SendMessage(env.AtAuthor() + " Please ping the user you want to trust") 909 | return 910 | } 911 | 912 | if len(discordEnv.m.Mentions) > 1 { 913 | env.SendMessage(env.AtAuthor() + " You can't trust several people simultaneously") 914 | return 915 | } 916 | 917 | mention := discordEnv.m.Mentions[0] 918 | 919 | count, err := TrustedTimesOfUser(db, discordEnv.m.Author) 920 | if err != nil { 921 | log.Println("Could not get amount of trusted times:", err) 922 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 923 | return 924 | } 925 | if count >= MaxTrustedTimes { 926 | if !env.IsAuthorAdmin() { 927 | env.SendMessage(fmt.Sprintf("%s You ran out of trusts. Used %d out of %d", env.AtAuthor(), count, MaxTrustedTimes)) 928 | return 929 | } else { 930 | env.SendMessage(fmt.Sprintf("%s You ran out of trusts. Used %d out of %d. But since you are the %s I'll make an exception for you.", env.AtAuthor(), count, MaxTrustedTimes, env.AtAdmin())) 931 | } 932 | } 933 | 934 | if mention.ID == discordEnv.m.Author.ID { 935 | env.SendMessage(env.AtAuthor() + " On this server you can't trust yourself!") 936 | return 937 | } 938 | 939 | mentionMember, err := discordEnv.dg.GuildMember(discordEnv.m.GuildID, mention.ID) 940 | if err != nil { 941 | log.Printf("Could not get roles of user %s: %s\n", mention.ID, err) 942 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 943 | return 944 | } 945 | 946 | if isMemberTrusted(mentionMember) { 947 | env.SendMessage(env.AtAuthor() + " That member is already trusted") 948 | return 949 | } 950 | 951 | // TODO: do all of that in a transation that is rollbacked when GuildMemberRoleAdd fails 952 | // TODO: add record to trusted users table 953 | _, err = db.Exec("INSERT INTO TrustLog (trusterId, trusteeId) VALUES ($1, $2);", discordEnv.m.Author.ID, mention.ID) 954 | if err != nil { 955 | log.Printf("Could not save a TrustLog entry: %s\n", err) 956 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 957 | return 958 | } 959 | 960 | err = discordEnv.dg.GuildMemberRoleAdd(discordEnv.m.GuildID, mention.ID, TrustedRoleId) 961 | if err != nil { 962 | log.Printf("Could not assign role %s to user %s: %s\n", TrustedRoleId, mention.ID, err) 963 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 964 | return 965 | } 966 | 967 | env.SendMessage(fmt.Sprintf("%s Trusted %s. Used %d out of %d trusts.", env.AtAuthor(), AtUser(mention), count+1, MaxTrustedTimes)) 968 | */ 969 | case "mine": 970 | if env.AsDiscord() == nil { 971 | env.SendMessage(env.AtAuthor() + " This command only works in Discord, sorry") 972 | return 973 | } 974 | 975 | // TODO: make the field size customizable via the command parameters 976 | var seed string 977 | if len(command.Args) > 0 { 978 | seed = command.Args 979 | } else { 980 | seed = randomMinesweeperSeed() 981 | } 982 | 983 | r := rand.New(seedAsSource(seed)) 984 | env.SendMessage(renderMinesweeperFieldForDiscord(randomMinesweeperField(r), seed)) 985 | case "mineopen": 986 | if env.AsDiscord() == nil { 987 | env.SendMessage(env.AtAuthor() + " This command only works in Discord, sorry") 988 | return 989 | } 990 | 991 | if len(command.Args) == 0 { 992 | env.SendMessage(env.AtAuthor() + " please provide the seed") 993 | return 994 | } 995 | 996 | seed := command.Args 997 | r := rand.New(seedAsSource(seed)) 998 | env.SendMessage(renderOpenMinesweeperFieldForDiscord(randomMinesweeperField(r), seed)) 999 | default: 1000 | env.SendMessage(fmt.Sprintf("%s command `%s` does not exist", env.AtAuthor(), command.Name)) 1001 | } 1002 | } 1003 | 1004 | func EvalCommand(db *sql.DB, command Command, env CommandEnvironment) { 1005 | row := db.QueryRow("SELECT bex, count FROM commands WHERE name = $1", command.Name); 1006 | var bex string 1007 | var count int64 1008 | err := row.Scan(&bex, &count) 1009 | if err == sql.ErrNoRows { 1010 | EvalBuiltinCommand(db, command, env, EvalContextFromCommandEnvironment(env, command, 0)) 1011 | return 1012 | } 1013 | if err != nil { 1014 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 1015 | log.Printf("Error while querying command %s: %s\n", command.Name, err); 1016 | return 1017 | } 1018 | 1019 | exprs, err := ParseAllExprs(bex) 1020 | if err != nil { 1021 | env.SendMessage(fmt.Sprintf("%s Error while parsing `%s` command: %s", env.AtAuthor(), command.Name, err)); 1022 | return 1023 | } 1024 | 1025 | count += 1 1026 | context := EvalContextFromCommandEnvironment(env, command, count) 1027 | 1028 | for _, expr := range exprs { 1029 | _, err := context.EvalExpr(expr) 1030 | if err != nil { 1031 | env.SendMessage(fmt.Sprintf("%s Could not evaluate command's expression `%s`: %s", env.AtAuthor(), bex, err)); 1032 | return 1033 | } 1034 | } 1035 | 1036 | _, err = db.Exec("UPDATE commands SET count = $1 WHERE name = $2;", count, command.Name); 1037 | if err != nil { 1038 | env.SendMessage(env.AtAuthor() + " Something went wrong. Please ask " + env.AtAdmin() + " to check the logs") 1039 | log.Printf("Error while querying command %s: %s\n", command.Name, err); 1040 | return 1041 | } 1042 | } 1043 | 1044 | var ( 1045 | PlaceNotFound = errors.New("PlaceNotFound") 1046 | SomebodyTryingToHackWeather = errors.New("SomebodyTryingToHackWeather") 1047 | ) 1048 | 1049 | func checkWeatherOf(place string) (string, error) { 1050 | res, err := http.Get("https://wttr.in/" + url.PathEscape(place) + "?format=4") 1051 | if err != nil { 1052 | return "", err 1053 | } 1054 | defer res.Body.Close() 1055 | 1056 | body, err := ioutil.ReadAll(res.Body) 1057 | if err != nil { 1058 | return "", err 1059 | } 1060 | 1061 | if res.StatusCode == 404 { 1062 | return "", PlaceNotFound 1063 | } else if res.StatusCode == 400 { 1064 | return "", SomebodyTryingToHackWeather 1065 | } else if res.StatusCode > 400 { 1066 | return "", fmt.Errorf("Unsuccesful response from wttr.in with code %d: %s", res.StatusCode, string(body)) 1067 | } 1068 | 1069 | return string(body), nil 1070 | } 1071 | -------------------------------------------------------------------------------- /cmd/gatekeeper/discord.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bwmarrin/discordgo" 5 | "github.com/tsoding/gatekeeper/internal" 6 | "database/sql" 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "log" 11 | ) 12 | 13 | var DiscordPingRegexp = regexp.MustCompile("<@[0-9]+>") 14 | 15 | const ( 16 | RolesChannelId = "777109841416159264" 17 | 18 | PingEmojiId = "777109447600111656" 19 | PingedRoleId = "777108731766505472" 20 | 21 | AocEmojiId = "🌲" 22 | AocRoleId = "783548342390620201" 23 | 24 | IntrovertEmojiId = "👀" 25 | IntrovertRoleId = "791706084654055517" 26 | ) 27 | 28 | func maskDiscordPings(message string) string { 29 | return DiscordPingRegexp.ReplaceAllString(message, "@[DISCORD PING REDACTED]") 30 | } 31 | 32 | func isMemberTrusted(member *discordgo.Member) bool { 33 | for _, roleId := range member.Roles { 34 | if roleId == TrustedRoleId { 35 | return true 36 | } 37 | } 38 | return false 39 | } 40 | 41 | func AtID(id string) string { 42 | return "<@"+id+">" 43 | } 44 | 45 | func AtUser(user *discordgo.User) string { 46 | return AtID(user.ID) 47 | } 48 | 49 | func TrustedTimesOfUser(db *sql.DB, user *discordgo.User) (int, error) { 50 | rows, err := db.Query("SELECT count(*) FROM TrustLog WHERE trusterId = $1", user.ID) 51 | if err != nil { 52 | return 0, err 53 | } 54 | defer rows.Close() 55 | 56 | if rows.Next() { 57 | var count int 58 | if err := rows.Scan(&count); err != nil { 59 | return 0, err 60 | } 61 | return count, nil 62 | } 63 | 64 | return 0, fmt.Errorf("TrustedTimesOfUser: expected at least one row with result") 65 | } 66 | 67 | type DiscordEnvironment struct { 68 | dg *discordgo.Session 69 | m *discordgo.MessageCreate 70 | } 71 | 72 | func (env *DiscordEnvironment) AsDiscord() *DiscordEnvironment { 73 | return env 74 | } 75 | 76 | func (env *DiscordEnvironment) AtAdmin() string { 77 | return AtID(AdminID) 78 | } 79 | 80 | func (env *DiscordEnvironment) AuthorUserId() string { 81 | return "discord#"+env.m.Author.ID 82 | } 83 | 84 | func (env *DiscordEnvironment) AtAuthor() string { 85 | return AtUser(env.m.Author) 86 | } 87 | 88 | func (env *DiscordEnvironment) IsAuthorAdmin() bool { 89 | return env.m.Author.ID == AdminID 90 | } 91 | 92 | func (env *DiscordEnvironment) SendMessage(message string) { 93 | _, err := env.dg.ChannelMessageSend(env.m.ChannelID, message) 94 | if err != nil { 95 | log.Println("Error during sending discord message", err) 96 | } 97 | } 98 | 99 | func logDiscordMessage(db *sql.DB, m *discordgo.MessageCreate) { 100 | _, err := db.Exec("INSERT INTO Discord_Log (message_id, user_id, user_name, text) VALUES ($1, $2, $3, $4)", m.ID, m.Author.ID, m.Author.Username, m.Content); 101 | if err != nil { 102 | log.Println("ERROR: logDiscordMessage: could not insert element", m.Author.ID, m.Author.Username, m.Content, ":", err); 103 | return 104 | } 105 | } 106 | 107 | func handleDiscordMessage(db *sql.DB, dg *discordgo.Session, m *discordgo.MessageCreate) { 108 | if m.Author.Bot { 109 | return 110 | } 111 | 112 | logDiscordMessage(db, m); 113 | 114 | command, ok := parseCommand(m.Content) 115 | if !ok { 116 | if db != nil { 117 | internal.FeedMessageToCarrotson(db, m.Content) 118 | } 119 | return 120 | } 121 | 122 | env := &DiscordEnvironment{ 123 | dg: dg, 124 | m: m, 125 | } 126 | EvalCommand(db, command, env); 127 | } 128 | 129 | var ( 130 | // TODO: unhardcode these parameters (config, database, or something else) 131 | AdminID = "180406039500292096" 132 | MaxTrustedTimes = 1 133 | TrustedRoleId = "543864981171470346" 134 | ) 135 | 136 | func roleOfEmoji(emoji *discordgo.Emoji) (string, bool) { 137 | emojiId := emoji.ID 138 | if emojiId == "" { 139 | emojiId = emoji.Name 140 | } 141 | switch emojiId { 142 | case PingEmojiId: return PingedRoleId, true 143 | case AocEmojiId: return AocRoleId, true 144 | case IntrovertEmojiId: return IntrovertRoleId, true 145 | default: return "", false 146 | } 147 | } 148 | 149 | func startDiscord(db *sql.DB) (*discordgo.Session, error) { 150 | discordToken, found := os.LookupEnv("GATEKEEPER_DISCORD_TOKEN") 151 | if !found { 152 | return nil, fmt.Errorf("Could not find GATEKEEPER_DISCORD_TOKEN variable") 153 | } 154 | 155 | dg, err := discordgo.New("Bot " + discordToken) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | dg.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentGuildMessageReactions 161 | 162 | dg.AddHandler(func (s *discordgo.Session, m *discordgo.MessageReactionAdd) { 163 | if m.MessageReaction.ChannelID == RolesChannelId { 164 | roleId, found := roleOfEmoji(&m.MessageReaction.Emoji) 165 | if found { 166 | log.Println("Adding role", roleId, "to user", m.MessageReaction.UserID); 167 | err := s.GuildMemberRoleAdd(m.MessageReaction.GuildID, m.MessageReaction.UserID, roleId) 168 | if err != nil { 169 | log.Println("Error adding role:", err) 170 | } 171 | } 172 | } 173 | }) 174 | dg.AddHandler(func (s *discordgo.Session, m *discordgo.MessageReactionRemove) { 175 | if m.MessageReaction.ChannelID == RolesChannelId { 176 | roleId, found := roleOfEmoji(&m.MessageReaction.Emoji) 177 | if found { 178 | log.Println("Removing role", roleId, "from user", m.MessageReaction.UserID); 179 | err := s.GuildMemberRoleRemove(m.MessageReaction.GuildID, m.MessageReaction.UserID, roleId) 180 | if err != nil { 181 | log.Println("Error removing role:", err) 182 | } 183 | } 184 | } 185 | }) 186 | dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate){ 187 | handleDiscordMessage(db, s, m) 188 | }) 189 | 190 | err = dg.Open() 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | return dg, nil 196 | } 197 | -------------------------------------------------------------------------------- /cmd/gatekeeper/ed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "strings" 6 | "slices" 7 | "strconv" 8 | "unicode/utf8" 9 | "log" 10 | "fmt" 11 | ) 12 | 13 | const ( 14 | // NOTE: if these values are modified the size of the buffer of the Ed_State table 15 | // should be adjusted as well. Ideally it should be equal or bigger than 16 | // 2*EdLineCountLimit*EdLineSizeLimit (the 2 is to accomodate the newlines) 17 | EdLineCountLimit = 6 18 | EdLineSizeLimit = 101 19 | ) 20 | 21 | type EdMode int 22 | 23 | const ( 24 | EdCommandMode EdMode = iota 25 | EdInsertMode 26 | ) 27 | 28 | type EdState struct { 29 | Buffer []string 30 | Cursor int 31 | Mode EdMode 32 | } 33 | 34 | func SaveEdStateByUserId(db *sql.DB, userId string, ed EdState) error { 35 | _, err := db.Exec("INSERT INTO Ed_State (user_id, buffer, cur, mode) VALUES ($1, $2, $3, $4) ON CONFLICT (user_id) DO UPDATE SET buffer = EXCLUDED.buffer, cur = EXCLUDED.cur, mode = EXCLUDED.mode;", userId, strings.Join(ed.Buffer, "\n"), ed.Cursor, ed.Mode) 36 | if err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | func LoadEdStateByUserId(db *sql.DB, userId string) (EdState, error) { 43 | row := db.QueryRow("SELECT buffer, cur, mode FROM Ed_State WHERE user_id = $1", userId); 44 | var buffer string 45 | var cursor int 46 | var mode int 47 | err := row.Scan(&buffer, &cursor, &mode); 48 | if err == sql.ErrNoRows { 49 | return EdState{}, nil 50 | } 51 | if err != nil { 52 | return EdState{}, err 53 | } 54 | ed := EdState{ 55 | Cursor: cursor, 56 | Mode: EdMode(mode), 57 | } 58 | if len(buffer) > 0 { 59 | ed.Buffer = strings.Split(buffer, "\n"); 60 | } 61 | return ed, nil 62 | } 63 | 64 | func (ed *EdState) Print(env CommandEnvironment, line string) { 65 | env.SendMessage(env.AtAuthor()+" "+line); 66 | } 67 | 68 | func (ed *EdState) LineAt(index int) (string, bool) { 69 | if 0 <= index && index < len(ed.Buffer) { 70 | return ed.Buffer[index], true 71 | } else { 72 | return "", false 73 | } 74 | } 75 | 76 | func (ed *EdState) Huh(env CommandEnvironment) { 77 | env.SendMessage(env.AtAuthor()+" ?") 78 | } 79 | 80 | func (ed *EdState) ExecCommand(env CommandEnvironment, command string) { 81 | switch ed.Mode { 82 | case EdCommandMode: 83 | switch command { 84 | case "": 85 | newCursor := ed.Cursor + 1 86 | if line, ok := ed.LineAt(newCursor); ok { 87 | ed.Cursor = newCursor 88 | ed.Print(env, line) 89 | } else { 90 | ed.Huh(env) 91 | } 92 | case "a": 93 | ed.Mode = EdInsertMode 94 | case "d": 95 | if _, ok := ed.LineAt(ed.Cursor); ok { 96 | ed.Buffer = slices.Delete(ed.Buffer, ed.Cursor, ed.Cursor+1) 97 | if ed.Cursor >= len(ed.Buffer) && ed.Cursor > 0 { // Cursor overflew after deleting last line 98 | ed.Cursor -= 1 99 | } 100 | } else { 101 | ed.Huh(env) 102 | } 103 | case ",p": 104 | if len(ed.Buffer) > 0 { 105 | for _, line := range(ed.Buffer) { 106 | ed.Print(env, line) 107 | } 108 | } else { 109 | ed.Huh(env) 110 | } 111 | case "p": 112 | if line, ok := ed.LineAt(ed.Cursor); ok { 113 | ed.Print(env, line) 114 | } else { 115 | ed.Huh(env) 116 | } 117 | case ",n": 118 | if len(ed.Buffer) > 0 { 119 | for cursor, line := range(ed.Buffer) { 120 | ed.Print(env, fmt.Sprintf("%-8d %s", cursor + 1, line)) 121 | } 122 | } else { 123 | ed.Huh(env) 124 | } 125 | case "n": 126 | if line, ok := ed.LineAt(ed.Cursor); ok { 127 | ed.Print(env, fmt.Sprintf("%-8d %s", ed.Cursor + 1, line)) 128 | } else { 129 | ed.Huh(env) 130 | } 131 | default: 132 | i, err := strconv.Atoi(command) 133 | if err != nil { 134 | ed.Huh(env) 135 | return 136 | } 137 | newCursor := i - 1 // 1-based indexing 138 | if line, ok := ed.LineAt(newCursor); ok { 139 | ed.Cursor = newCursor 140 | ed.Print(env, line) 141 | } else { 142 | ed.Huh(env) 143 | } 144 | } 145 | case EdInsertMode: 146 | if command == "." { 147 | ed.Mode = EdCommandMode 148 | } else { 149 | // NOTE: Keep in mind that to check the EdLineCountLimit 150 | // we use `>=`, but to check EdLineSizelimit we use 151 | // `>`. This is due to EdLineCountLimit being about 152 | // checking the size of the buffer BEFORE inserting any 153 | // new lines. While EdLineSizeLimit is about checking the 154 | // size of the line we are about to insert. 155 | if len(ed.Buffer) >= EdLineCountLimit { 156 | env.SendMessage(fmt.Sprintf("%s Your message exceeded line count limit (You may have %d lines maximum)", env.AtAuthor(), EdLineCountLimit)) 157 | return 158 | } 159 | if utf8.RuneCountInString(command) > EdLineSizeLimit { 160 | env.SendMessage(fmt.Sprintf("%s Your message exceeded line size limit (Your lines may have %d characters maximum)", env.AtAuthor(), EdLineSizeLimit)) 161 | return 162 | } 163 | if _, ok := ed.LineAt(ed.Cursor); ok { 164 | ed.Cursor += 1 165 | ed.Buffer = slices.Insert(ed.Buffer, ed.Cursor, command) 166 | } else if len(ed.Buffer) == 0 { 167 | ed.Cursor = 0 168 | ed.Buffer = append(ed.Buffer, command); 169 | } else { 170 | ed.Huh(env) 171 | } 172 | } 173 | default: 174 | log.Printf("Invalid mode of Ed State: %#v\n", ed) 175 | env.SendMessage(fmt.Sprintf("%s something went wrong with the state of your Ed. I've tried to correct it. Try again and ask %s to check the logs if the problem persists.", env.AtAuthor(), env.AtAdmin())); 176 | ed.Mode = EdCommandMode 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /cmd/gatekeeper/gatekeeper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | "log" 8 | "time" 9 | "math/rand" 10 | "github.com/tsoding/gatekeeper/internal" 11 | ) 12 | 13 | func main() { 14 | rand.Seed(time.Now().UnixNano()) 15 | 16 | // PostgreSQL ////////////////////////////// 17 | db := internal.StartPostgreSQL() 18 | if db == nil { 19 | log.Println("Starting without PostgreSQL. Commands that require it won't work.") 20 | } else { 21 | defer db.Close() 22 | } 23 | 24 | // Discord ////////////////////////////// 25 | dg, err := startDiscord(db) 26 | if err != nil { 27 | log.Println("Could not open Discord connection:", err); 28 | } else { 29 | defer dg.Close(); 30 | } 31 | 32 | // MPV ////////////////////////////// 33 | mpvMsgs, ok := startMpvControl(); 34 | if !ok { 35 | log.Println("Could not start the MPV Control"); 36 | } 37 | 38 | // Twitch ////////////////////////////// 39 | tw, ok := startTwitch(db, mpvMsgs); 40 | if !ok { 41 | log.Println("Could not open Twitch connection"); 42 | } else { 43 | defer tw.Close() 44 | } 45 | 46 | // Wait here until CTRL-C or other term signal is received. 47 | log.Println("Bot is now running. Press CTRL-C to exit.") 48 | sc := make(chan os.Signal, 1) 49 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) 50 | <-sc 51 | } 52 | -------------------------------------------------------------------------------- /cmd/gatekeeper/irc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "io" 6 | "fmt" 7 | "regexp" 8 | ) 9 | 10 | type IrcCmdName string 11 | const ( 12 | IrcCmdPass IrcCmdName = "PASS" 13 | IrcCmdNick = "NICK" 14 | IrcCmdJoin = "JOIN" 15 | IrcCmdPrivmsg = "PRIVMSG" 16 | IrcCmdPing = "PING" 17 | IrcCmdPong = "PONG" 18 | IrcCmd001 = "001" 19 | ) 20 | 21 | type IrcMsg struct { 22 | Prefix string 23 | Name IrcCmdName 24 | Args []string 25 | } 26 | 27 | // TODO: IrcMsg serialization should probably check for max IRC message length 28 | func (msg *IrcMsg) String() (result string, ok bool) { 29 | var sb strings.Builder 30 | 31 | if len(msg.Prefix) > 0 { 32 | if !VerifyPrefix(msg.Prefix) { 33 | return 34 | } 35 | 36 | sb.WriteString(":") 37 | sb.WriteString(msg.Prefix) 38 | sb.WriteString(" ") 39 | } 40 | 41 | if !VerifyCmdName(string(msg.Name)) { 42 | return 43 | } 44 | sb.WriteString(string(msg.Name)) 45 | 46 | n := len(msg.Args) 47 | for i := 0 ; i < n-1 ; i++ { 48 | if !VerifyMiddle(msg.Args[i]) { 49 | return 50 | } 51 | sb.WriteString(" ") 52 | sb.WriteString(msg.Args[i]) 53 | } 54 | if n > 0 { 55 | if !VerifyTrailing(msg.Args[n-1]) { 56 | return 57 | } 58 | sb.WriteString(" :") 59 | sb.WriteString(msg.Args[n-1]) 60 | } 61 | result = sb.String() 62 | ok = true 63 | return 64 | } 65 | 66 | func VerifyPrefix(prefix string) bool { 67 | // I don't know the exact format of prefix (RFC 1459 redirects to 68 | // RFC 952 which I'm too lazy to read), but here I simply assume 69 | // it may not contain NUL, SPACE, CR and LF, because that makes my 70 | // life easier. 71 | // 72 | // If it turns out that they may contain those, it's easy to fix. 73 | return !strings.ContainsAny(prefix, "\x00 \r\n") 74 | } 75 | 76 | var CmdNameRegexp = regexp.MustCompile("^([0-9]{3}|[a-zA-Z]+)$") 77 | 78 | func VerifyCmdName(name string) bool { 79 | return CmdNameRegexp.MatchString(name) 80 | } 81 | 82 | func VerifyMiddle(middle string) bool { 83 | // From RFC 1459 84 | // ::= 86 | if len(middle) == 0 { 87 | return false 88 | } 89 | if strings.HasPrefix(middle, ":") { 90 | return false 91 | } 92 | if strings.ContainsAny(middle, " \x00\r\n") { 93 | return false 94 | } 95 | return true 96 | } 97 | 98 | var TrailingForbidden = "\x00\r\n"; 99 | 100 | func FilterTrailingForbidden(s string) string { 101 | result := []byte{} 102 | for _, x := range []byte(s) { 103 | if strings.IndexByte(TrailingForbidden, x) < 0 { 104 | result = append(result, x) 105 | } 106 | } 107 | return string(result) 108 | } 109 | 110 | func VerifyTrailing(trailing string) bool { 111 | // From RFC 1459 112 | // ::= 114 | return !strings.ContainsAny(trailing, TrailingForbidden) 115 | } 116 | 117 | func ParseIrcMsg(source string) (msg IrcMsg, ok bool) { 118 | if strings.HasPrefix(source, ":") { 119 | split := strings.SplitN(source, " ", 2) 120 | if len(split) < 2 { 121 | return 122 | } 123 | msg.Prefix = strings.TrimPrefix(split[0], ":") 124 | if !VerifyPrefix(msg.Prefix) { 125 | return 126 | } 127 | source = split[1] 128 | } 129 | 130 | split := strings.SplitN(source, " ", 2) 131 | if len(split) < 2 { 132 | return 133 | } 134 | if !VerifyCmdName(split[0]) { 135 | return 136 | } 137 | msg.Name = IrcCmdName(split[0]) 138 | source = split[1] 139 | 140 | Loop: 141 | for len(source) > 0 { 142 | if strings.HasPrefix(source, ":") { 143 | trailing := strings.TrimPrefix(source, ":") 144 | if !VerifyTrailing(trailing) { 145 | return 146 | } 147 | msg.Args = append(msg.Args, trailing) 148 | break 149 | } else { 150 | split = strings.SplitN(source, " ", 2) 151 | switch len(split) { 152 | case 1: 153 | middle := split[0] 154 | if !VerifyMiddle(middle) { 155 | return 156 | } 157 | msg.Args = append(msg.Args, middle) 158 | break Loop 159 | case 2: 160 | middle := split[0] 161 | if !VerifyMiddle(middle) { 162 | return 163 | } 164 | msg.Args = append(msg.Args, middle) 165 | source = split[1] 166 | default: 167 | return // must be unreachable 168 | } 169 | } 170 | } 171 | 172 | ok = true 173 | return 174 | } 175 | 176 | func (msg IrcMsg) Send(writer io.Writer) error { 177 | msgString, ok := msg.String() 178 | if !ok { 179 | return fmt.Errorf("Could not serialize IRC message %#v", msg) 180 | } 181 | msgBytes := []byte(msgString+"\r\n") 182 | n, err := writer.Write(msgBytes) 183 | if err != nil { 184 | return fmt.Errorf("Could not send command %s: %w", msg.Name, err) 185 | } 186 | if n != len(msgBytes) { 187 | return fmt.Errorf("Command %s was not fully sent", msg.Name) 188 | } 189 | return nil 190 | } 191 | 192 | func (msg IrcMsg) Nick() string { 193 | // From RFC 1459 194 | // ::= | [ '!' ] [ '@' ] 195 | nick := strings.SplitN(msg.Prefix, "!", 2)[0] 196 | nick = strings.SplitN(nick, "@", 2)[0] 197 | return nick 198 | } 199 | -------------------------------------------------------------------------------- /cmd/gatekeeper/minesweeper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "hash/fnv" 5 | "strings" 6 | "fmt" 7 | "math/rand" 8 | ) 9 | 10 | const ( 11 | FieldRows = 9 12 | FieldCols = 9 13 | MinesCount = 13 14 | MaxMinesAttempts = FieldRows * FieldCols 15 | SeedSyllMaxLen = 5 16 | MineEmoji = "💥" 17 | ) 18 | 19 | var ( 20 | EmptyCellEmojis = [9]string{"🟦", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣"} 21 | SeedCons = []string{"b", "c", "d", "f", "g", "j", "k", "l", "m", "n", "p", "q", "s", "t", "v", "x", "z", "h", "r", "w", "y"} 22 | SeedVow = []string{"a", "e", "i", "o", "u"} 23 | ) 24 | 25 | type MinesweeperField struct { 26 | cells [FieldRows][FieldCols]bool 27 | } 28 | 29 | func (field MinesweeperField) countNbors(row0 int, col0 int) (count int) { 30 | for drow := -1; drow <= 1; drow += 1 { 31 | for dcol := -1; dcol <= 1; dcol += 1 { 32 | if drow != 0 || dcol != 0 { 33 | row := drow + row0; 34 | col := dcol + col0; 35 | if 0 <= row && row < FieldRows && 0 <= col && col < FieldCols { 36 | if (field.cells[row][col]) { 37 | count += 1 38 | } 39 | } 40 | } 41 | } 42 | } 43 | return 44 | } 45 | 46 | func findFirstCell(field MinesweeperField) (row int, col int, found bool) { 47 | for row = 0; row < FieldRows; row += 1 { 48 | for col = 0; col < FieldRows; col += 1 { 49 | if !field.cells[row][col] && field.countNbors(row, col) == 0 { 50 | found = true 51 | return 52 | } 53 | } 54 | } 55 | return 56 | } 57 | 58 | func emojiOfCell(field MinesweeperField, row int, col int) string { 59 | if field.cells[row][col] { 60 | return MineEmoji 61 | } 62 | 63 | return EmptyCellEmojis[field.countNbors(row, col)] 64 | } 65 | 66 | func renderMinesweeperFieldForDiscord(field MinesweeperField, seed string) string { 67 | firstRow, firstCol, foundFirst := findFirstCell(field) 68 | var sb strings.Builder 69 | fmt.Fprintf(&sb, "👉 %s\n", seed) 70 | for row := 0; row < FieldRows; row += 1 { 71 | for col := 0; col < FieldCols; col += 1 { 72 | if foundFirst && row == firstRow && col == firstCol { 73 | fmt.Fprintf(&sb, "%s", emojiOfCell(field, row, col)) 74 | } else { 75 | fmt.Fprintf(&sb, "||%s||", emojiOfCell(field, row, col)) 76 | } 77 | } 78 | sb.WriteString("\n") 79 | } 80 | return sb.String() 81 | } 82 | 83 | func renderOpenMinesweeperFieldForDiscord(field MinesweeperField, seed string) string { 84 | var sb strings.Builder 85 | fmt.Fprintf(&sb, "open 👉 %s\n", seed) 86 | sb.WriteString("||") 87 | for row := 0; row < FieldRows; row += 1 { 88 | for col := 0; col < FieldCols; col += 1 { 89 | fmt.Fprintf(&sb, "%s", emojiOfCell(field, row, col)) 90 | } 91 | sb.WriteString("\n") 92 | } 93 | sb.WriteString("||") 94 | return sb.String() 95 | } 96 | 97 | func randomMinesweeperField(r *rand.Rand) (field MinesweeperField) { 98 | for i := 0; i < MinesCount; i += 1 { 99 | row := r.Intn(FieldRows) 100 | col := r.Intn(FieldCols) 101 | for j := 0; j < MaxMinesAttempts && field.cells[row][col]; j += 1 { 102 | row = r.Intn(FieldRows) 103 | col = r.Intn(FieldCols) 104 | } 105 | field.cells[row][col] = true 106 | } 107 | return 108 | } 109 | 110 | func randomMinesweeperSeed() string { 111 | var sb strings.Builder 112 | for i := 0; i < SeedSyllMaxLen; i += 1 { 113 | sb.WriteString(SeedCons[rand.Intn(len(SeedCons))]) 114 | sb.WriteString(SeedVow[rand.Intn(len(SeedVow))]) 115 | } 116 | return sb.String() 117 | } 118 | 119 | func seedAsSource(seed string) rand.Source { 120 | h := fnv.New64a() 121 | h.Write([]byte(seed)) 122 | return rand.NewSource(int64(h.Sum64())) 123 | } 124 | -------------------------------------------------------------------------------- /cmd/gatekeeper/mpv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "log" 6 | "net" 7 | _"fmt" 8 | _"time" 9 | "encoding/json" 10 | "strings" 11 | "database/sql" 12 | ) 13 | 14 | type MpvSong struct { 15 | title string 16 | artist string 17 | } 18 | 19 | func LogMpvSong(db *sql.DB, song MpvSong) { 20 | _, err := db.Exec("INSERT INTO Song_Log (artist, title) VALUES ($1, $2)", song.artist, song.title); 21 | if err != nil { 22 | log.Println("ERROR: LogMpvSong: could not insert element %#v: %s", song, err); 23 | return 24 | } 25 | } 26 | 27 | func LastSongPlayed(db *sql.DB) *MpvSong { 28 | row := db.QueryRow("select artist, title from Song_Log order by startedAt desc limit 1") 29 | var artist string 30 | var title string 31 | err := row.Scan(&artist, &title) 32 | if err == sql.ErrNoRows { 33 | return nil 34 | } 35 | if err != nil { 36 | log.Printf("MPV: Could not query last played song: %s", err) 37 | return nil 38 | } 39 | return &MpvSong{ 40 | artist: artist, 41 | title: title, 42 | } 43 | } 44 | 45 | type Object = map[string]interface{} 46 | type Array = []interface{} 47 | 48 | func startMpvControlThread(conn net.Conn, mpvIpcAddress string, msgs chan MpvSong) { 49 | defer conn.Close() 50 | 51 | var root interface{} 52 | decoder := json.NewDecoder(conn) 53 | 54 | for { 55 | err := decoder.Decode(&root) 56 | if err != nil { 57 | log.Printf("MPV: Could not read from %s: %s\n", mpvIpcAddress, err); 58 | return; 59 | } 60 | cursor := root.(Object); 61 | if cursor["event"] != nil { 62 | if cursor["event"] == "file-loaded" { 63 | _, err := conn.Write([]byte("{ \"command\": [\"get_property\", \"metadata\"] }\n")); 64 | if err != nil { 65 | log.Printf("MPV: could not send command: %s\n", err); 66 | return 67 | } 68 | } 69 | } else if cursor["data"] != nil { 70 | cursor := cursor["data"].(Object); 71 | title := "UNKNOWN" 72 | artist := "UNKNOWN" 73 | for k, v := range cursor { 74 | switch strings.ToLower(k) { 75 | case "title": title = v.(string); 76 | case "artist": artist = v.(string); 77 | } 78 | } 79 | msgs <- MpvSong{ 80 | title: title, 81 | artist: artist, 82 | } 83 | log.Printf("MPV: %#v", cursor); 84 | } else { 85 | log.Printf("MPV: unknown message: %v\n", root); 86 | } 87 | } 88 | } 89 | 90 | func startMpvControl() (chan MpvSong, bool) { 91 | msgs := make(chan MpvSong); 92 | 93 | mpvIpcAddress := os.Getenv("GATEKEEPER_MPV_IPC_ADDRESS"); 94 | if mpvIpcAddress == "" { 95 | log.Println("MPV: no GATEKEEPER_MPV_IPC_ADDRESS is provided."); 96 | return msgs, false 97 | } 98 | 99 | l, err := net.Listen("tcp", mpvIpcAddress); 100 | if err != nil { 101 | log.Printf("MPV: could not listen to %s: %s", mpvIpcAddress, err); 102 | return msgs, false 103 | } 104 | 105 | log.Printf("MPV: listening to %s", mpvIpcAddress); 106 | 107 | go func() { 108 | for { 109 | conn, err := l.Accept(); 110 | if err != nil { 111 | log.Printf("MPV: failed to accept connection on %s: %s", mpvIpcAddress, err); 112 | return; 113 | } 114 | 115 | log.Printf("MPV: %s connected", conn.RemoteAddr()); 116 | startMpvControlThread(conn, mpvIpcAddress, msgs); 117 | } 118 | }(); 119 | 120 | return msgs, true 121 | } 122 | -------------------------------------------------------------------------------- /cmd/gatekeeper/twitch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "log" 6 | "fmt" 7 | "os" 8 | "crypto/tls" 9 | "time" 10 | "database/sql" 11 | _"github.com/tsoding/gatekeeper/internal" 12 | ) 13 | 14 | const ( 15 | BotAdminTwitchHandle = "Tsoding" 16 | ) 17 | 18 | // https://dev.twitch.tv/docs/irc#connecting-to-the-twitch-irc-server 19 | const ( 20 | TwitchIrcAddress = "irc.chat.twitch.tv:6697" 21 | TwitchIrcChannel = "#tsoding" 22 | ) 23 | 24 | type TwitchConnState int 25 | const ( 26 | TwitchConnect TwitchConnState = iota 27 | TwitchLogin 28 | TwitchJoin 29 | TwitchChat 30 | ) 31 | 32 | type TwitchEnvironment struct { 33 | AuthorHandle string 34 | Conn *tls.Conn 35 | Channel string 36 | } 37 | 38 | func (env *TwitchEnvironment) AsDiscord() *DiscordEnvironment { 39 | return nil 40 | } 41 | 42 | func (env *TwitchEnvironment) AtAdmin() string { 43 | return "@"+BotAdminTwitchHandle 44 | } 45 | 46 | func (env *TwitchEnvironment) AuthorUserId() string { 47 | return "twitch#"+env.AuthorHandle 48 | } 49 | 50 | func (env *TwitchEnvironment) AtAuthor() string { 51 | if len(env.AuthorHandle) > 0 { 52 | return "@"+env.AuthorHandle 53 | } 54 | // The author could be empty if the environment was created not by 55 | // a command. For instance, MPV message handler 56 | return "" 57 | } 58 | 59 | func (env *TwitchEnvironment) IsAuthorAdmin() bool { 60 | return strings.ToUpper(env.AuthorHandle) == strings.ToUpper(BotAdminTwitchHandle) 61 | } 62 | 63 | func (env *TwitchEnvironment) SendMessage(message string) { 64 | message = ". "+FilterTrailingForbidden(message); 65 | msg := IrcMsg{Name: IrcCmdPrivmsg, Args: []string{env.Channel, message}} 66 | err := msg.Send(env.Conn) 67 | if err != nil { 68 | log.Println("Error sending Twitch message \"%s\" for channel %s: %s", message, env.Channel, err) 69 | } 70 | } 71 | 72 | type TwitchConn struct { 73 | State TwitchConnState 74 | Reconnected int 75 | Nick string 76 | Pass string 77 | Conn *tls.Conn 78 | Quit chan int 79 | Incoming chan IrcMsg 80 | IncomingQuit chan int 81 | Mpv chan MpvSong 82 | } 83 | 84 | func (conn *TwitchConn) Close() { 85 | // TODO: How to make this block until it's handled? 86 | conn.Quit <- 69 87 | } 88 | 89 | func twitchIncomingLoop(twitchConn *TwitchConn) { 90 | reply := make([]byte, 2048) 91 | for { 92 | n, err := twitchConn.Conn.Read(reply) 93 | if err != nil { 94 | log.Println("Could not read the reply:", err) 95 | break 96 | } 97 | 98 | lines := strings.Split(string(reply[0:n]), "\n") 99 | for _, line := range lines { 100 | line = strings.TrimSuffix(line, "\r") 101 | if len(line) == 0 { 102 | continue 103 | } 104 | msg, ok := ParseIrcMsg(line) 105 | if !ok { 106 | // TODO: we should probably restart the connection if parsing commands failed too many times 107 | log.Printf("Failed to parse command: |%s| %d\n", line, len(line)) 108 | continue 109 | } 110 | twitchConn.Incoming <- msg 111 | } 112 | } 113 | 114 | twitchConn.IncomingQuit <- 69 115 | } 116 | 117 | // `granum` stands for `Grammatical Number`: https://en.wikipedia.org/wiki/Grammatical_number 118 | func granum(amount int, singular string, plural string) string { 119 | if amount == 1 { 120 | return fmt.Sprintf("1 %s", singular) 121 | } 122 | return fmt.Sprintf("%d %s", amount, plural) 123 | } 124 | 125 | func startTwitch(db *sql.DB, mpv chan MpvSong) (*TwitchConn, bool) { 126 | twitchConn := TwitchConn{ 127 | Quit: make(chan int), 128 | Incoming: make(chan IrcMsg), 129 | IncomingQuit: make(chan int), 130 | Mpv: mpv, 131 | } 132 | 133 | twitchConn.Nick = os.Getenv("GATEKEEPER_TWITCH_IRC_NICK"); 134 | if twitchConn.Nick == "" { 135 | log.Println("No GATEKEEPER_TWITCH_IRC_NICK envar is provided.") 136 | return nil, false 137 | } 138 | 139 | twitchConn.Pass = os.Getenv("GATEKEEPER_TWITCH_IRC_PASS"); 140 | if twitchConn.Pass == "" { 141 | log.Println("No GATEKEEPER_TWITCH_IRC_PASS envar is provided.") 142 | return nil, false 143 | } 144 | 145 | go func() { 146 | for { 147 | switch twitchConn.State { 148 | case TwitchConnect: 149 | // TODO: Can't ^C when the bot keeps reconnecting 150 | // Can be reproduced by turning off the Internet 151 | if twitchConn.Conn != nil { 152 | twitchConn.Conn.Close() 153 | twitchConn.Conn = nil 154 | } 155 | 156 | if twitchConn.Reconnected > 0 { 157 | seconds := 1 << (twitchConn.Reconnected - 1) 158 | log.Printf("Waiting %s before reconnecting Twitch IRC server\n", granum(seconds, "second", "seconds")) 159 | time.Sleep(time.Duration(seconds) * time.Second) 160 | } 161 | twitchConn.Reconnected += 1; 162 | 163 | conn, err := tls.Dial("tcp", TwitchIrcAddress, nil) 164 | if err != nil { 165 | log.Println("Failed to connect to Twitch IRC server:", err) 166 | continue 167 | } 168 | twitchConn.Conn = conn 169 | twitchConn.State = TwitchLogin 170 | case TwitchLogin: 171 | err := IrcMsg{Name: IrcCmdPass, Args: []string{"oauth:"+twitchConn.Pass}}.Send(twitchConn.Conn) 172 | if err != nil { 173 | log.Println(err) 174 | } 175 | err = IrcMsg{Name: IrcCmdNick, Args: []string{twitchConn.Nick}}.Send(twitchConn.Conn) 176 | if err != nil { 177 | log.Println(err) 178 | } 179 | twitchConn.State = TwitchJoin 180 | // TODO: check for authentication failures 181 | // Reconnection is pointless. Abandon Twitch service at all. 182 | go twitchIncomingLoop(&twitchConn) 183 | case TwitchJoin: 184 | select { 185 | case <-twitchConn.IncomingQuit: 186 | twitchConn.State = TwitchConnect 187 | continue 188 | case msg := <-twitchConn.Incoming: 189 | switch msg.Name { 190 | case IrcCmd001: 191 | // > 001 is a welcome event, so we join channels there 192 | // Source: https://github.com/go-irc/irc#example 193 | err := IrcMsg{Name: IrcCmdJoin, Args: []string{TwitchIrcChannel}}.Send(twitchConn.Conn) 194 | if err != nil { 195 | log.Println(err) 196 | } 197 | twitchConn.State = TwitchChat 198 | continue 199 | } 200 | } 201 | case TwitchChat: 202 | select { 203 | case <-twitchConn.Quit: 204 | log.Println("Twitch: closing connection...") 205 | twitchConn.Conn.Close() 206 | return 207 | case <-twitchConn.IncomingQuit: 208 | twitchConn.State = TwitchConnect 209 | continue 210 | case song := <-twitchConn.Mpv: 211 | tw := TwitchEnvironment{ 212 | AuthorHandle: "", 213 | Conn: twitchConn.Conn, 214 | Channel: TwitchIrcChannel, 215 | } 216 | LogMpvSong(db, song) 217 | tw.SendMessage(fmt.Sprintf("🎶 🎵 Currently Playing: \"%s\" by %s 🎵 🎶", song.title, song.artist)); 218 | case msg := <-twitchConn.Incoming: 219 | switch msg.Name { 220 | // TODO: Handle RECONNECT command 221 | // https://dev.twitch.tv/docs/irc/commands#reconnect 222 | case IrcCmdPing: 223 | // Reset the Reconnected counter only after 224 | // some time. I think the Twitch's Keep-Alive 225 | // PING is a good time to reset it. 226 | twitchConn.Reconnected = 0 227 | err := IrcMsg{Name: IrcCmdPong, Args: msg.Args}.Send(twitchConn.Conn) 228 | if err != nil { 229 | log.Println(err) 230 | continue 231 | } 232 | case IrcCmdPrivmsg: 233 | // TODO: this should be probably verified at parsing 234 | // Each IrcCmdName should have an associated arity with it that is verified at parse/serialize. 235 | if len(msg.Args) != 2 { 236 | log.Printf("Twitch: unexpected amount of args of PRIVMSG. Expected 2, but got %d\n", len(msg.Args)) 237 | continue 238 | } 239 | 240 | command, ok := parseCommand(msg.Args[1]) 241 | if !ok { 242 | // // TODO: consider feeding Twitch logs to the carrotson model 243 | // if db != nil { 244 | // internal.FeedMessageToCarrotson(db, msg.Args[1]) 245 | // } 246 | continue 247 | } 248 | 249 | env := &TwitchEnvironment{ 250 | AuthorHandle: msg.Nick(), 251 | Conn: twitchConn.Conn, 252 | Channel: TwitchIrcChannel, 253 | } 254 | 255 | EvalCommand(db, command, env); 256 | } 257 | } 258 | default: panic("unreachable") 259 | } 260 | } 261 | }() 262 | 263 | return &twitchConn, true 264 | } 265 | -------------------------------------------------------------------------------- /cmd/mpv-client/.gitignore: -------------------------------------------------------------------------------- 1 | mpv-client -------------------------------------------------------------------------------- /cmd/mpv-client/flag.h: -------------------------------------------------------------------------------- 1 | // flag.h -- v1.0.0 -- command-line flag parsing 2 | // 3 | // Inspired by Go's flag module: https://pkg.go.dev/flag 4 | // 5 | #ifndef FLAG_H_ 6 | #define FLAG_H_ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // TODO: add support for -flag=x syntax 20 | // TODO: *_var function variants 21 | // void flag_bool_var(bool *var, const char *name, bool def, const char *desc); 22 | // void flag_bool_uint64(uint64_t *var, const char *name, bool def, const char *desc); 23 | // etc. 24 | // WARNING! *_var functions may break the flag_name() functionality 25 | 26 | char *flag_name(void *val); 27 | bool *flag_bool(const char *name, bool def, const char *desc); 28 | uint64_t *flag_uint64(const char *name, uint64_t def, const char *desc); 29 | size_t *flag_size(const char *name, uint64_t def, const char *desc); 30 | char **flag_str(const char *name, const char *def, const char *desc); 31 | bool flag_parse(int argc, char **argv); 32 | int flag_rest_argc(void); 33 | char **flag_rest_argv(void); 34 | void flag_print_error(FILE *stream); 35 | void flag_print_options(FILE *stream); 36 | 37 | #endif // FLAG_H_ 38 | 39 | ////////////////////////////// 40 | 41 | #ifdef FLAG_IMPLEMENTATION 42 | 43 | typedef enum { 44 | FLAG_BOOL = 0, 45 | FLAG_UINT64, 46 | FLAG_SIZE, 47 | FLAG_STR, 48 | COUNT_FLAG_TYPES, 49 | } Flag_Type; 50 | 51 | static_assert(COUNT_FLAG_TYPES == 4, "Exhaustive Flag_Value definition"); 52 | typedef union { 53 | char *as_str; 54 | uint64_t as_uint64; 55 | bool as_bool; 56 | size_t as_size; 57 | } Flag_Value; 58 | 59 | typedef enum { 60 | FLAG_NO_ERROR = 0, 61 | FLAG_ERROR_UNKNOWN, 62 | FLAG_ERROR_NO_VALUE, 63 | FLAG_ERROR_INVALID_NUMBER, 64 | FLAG_ERROR_INTEGER_OVERFLOW, 65 | FLAG_ERROR_INVALID_SIZE_SUFFIX, 66 | COUNT_FLAG_ERRORS, 67 | } Flag_Error; 68 | 69 | typedef struct { 70 | Flag_Type type; 71 | char *name; 72 | char *desc; 73 | Flag_Value val; 74 | Flag_Value def; 75 | } Flag; 76 | 77 | #ifndef FLAGS_CAP 78 | #define FLAGS_CAP 256 79 | #endif 80 | 81 | typedef struct { 82 | Flag flags[FLAGS_CAP]; 83 | size_t flags_count; 84 | 85 | Flag_Error flag_error; 86 | char *flag_error_name; 87 | 88 | const char *program_name; 89 | 90 | int rest_argc; 91 | char **rest_argv; 92 | } Flag_Context; 93 | 94 | static Flag_Context flag_global_context; 95 | 96 | Flag *flag_new(Flag_Type type, const char *name, const char *desc) 97 | { 98 | Flag_Context *c = &flag_global_context; 99 | 100 | assert(c->flags_count < FLAGS_CAP); 101 | Flag *flag = &c->flags[c->flags_count++]; 102 | memset(flag, 0, sizeof(*flag)); 103 | flag->type = type; 104 | // NOTE: I won't touch them I promise Kappa 105 | flag->name = (char*) name; 106 | flag->desc = (char*) desc; 107 | return flag; 108 | } 109 | 110 | char *flag_name(void *val) 111 | { 112 | Flag *flag = (Flag*) ((char*) val - offsetof(Flag, val)); 113 | return flag->name; 114 | } 115 | 116 | bool *flag_bool(const char *name, bool def, const char *desc) 117 | { 118 | Flag *flag = flag_new(FLAG_BOOL, name, desc); 119 | flag->def.as_bool = def; 120 | flag->val.as_bool = def; 121 | return &flag->val.as_bool; 122 | } 123 | 124 | uint64_t *flag_uint64(const char *name, uint64_t def, const char *desc) 125 | { 126 | Flag *flag = flag_new(FLAG_UINT64, name, desc); 127 | flag->val.as_uint64 = def; 128 | flag->def.as_uint64 = def; 129 | return &flag->val.as_uint64; 130 | } 131 | 132 | size_t *flag_size(const char *name, uint64_t def, const char *desc) 133 | { 134 | Flag *flag = flag_new(FLAG_SIZE, name, desc); 135 | flag->val.as_size = def; 136 | flag->def.as_size = def; 137 | return &flag->val.as_size; 138 | } 139 | 140 | char **flag_str(const char *name, const char *def, const char *desc) 141 | { 142 | Flag *flag = flag_new(FLAG_STR, name, desc); 143 | flag->val.as_str = (char*) def; 144 | flag->def.as_str = (char*) def; 145 | return &flag->val.as_str; 146 | } 147 | 148 | static char *flag_shift_args(int *argc, char ***argv) 149 | { 150 | assert(*argc > 0); 151 | char *result = **argv; 152 | *argv += 1; 153 | *argc -= 1; 154 | return result; 155 | } 156 | 157 | int flag_rest_argc(void) 158 | { 159 | return flag_global_context.rest_argc; 160 | } 161 | 162 | char **flag_rest_argv(void) 163 | { 164 | return flag_global_context.rest_argv; 165 | } 166 | 167 | const char *flag_program_name(void) 168 | { 169 | return flag_global_context.program_name; 170 | } 171 | 172 | bool flag_parse(int argc, char **argv) 173 | { 174 | Flag_Context *c = &flag_global_context; 175 | 176 | if (c->program_name == NULL) { 177 | c->program_name = flag_shift_args(&argc, &argv); 178 | } 179 | 180 | while (argc > 0) { 181 | char *flag = flag_shift_args(&argc, &argv); 182 | 183 | if (*flag != '-') { 184 | // NOTE: pushing flag back into args 185 | c->rest_argc = argc + 1; 186 | c->rest_argv = argv - 1; 187 | return true; 188 | } 189 | 190 | if (strcmp(flag, "--") == 0) { 191 | // NOTE: but if it's the terminator we don't need to push it back 192 | c->rest_argc = argc; 193 | c->rest_argv = argv; 194 | return true; 195 | } 196 | 197 | // NOTE: remove the dash 198 | flag += 1; 199 | 200 | bool found = false; 201 | for (size_t i = 0; i < c->flags_count; ++i) { 202 | if (strcmp(c->flags[i].name, flag) == 0) { 203 | static_assert(COUNT_FLAG_TYPES == 4, "Exhaustive flag type parsing"); 204 | switch (c->flags[i].type) { 205 | case FLAG_BOOL: { 206 | c->flags[i].val.as_bool = true; 207 | } 208 | break; 209 | 210 | case FLAG_STR: { 211 | if (argc == 0) { 212 | c->flag_error = FLAG_ERROR_NO_VALUE; 213 | c->flag_error_name = flag; 214 | return false; 215 | } 216 | char *arg = flag_shift_args(&argc, &argv); 217 | c->flags[i].val.as_str = arg; 218 | } 219 | break; 220 | 221 | case FLAG_UINT64: { 222 | if (argc == 0) { 223 | c->flag_error = FLAG_ERROR_NO_VALUE; 224 | c->flag_error_name = flag; 225 | return false; 226 | } 227 | char *arg = flag_shift_args(&argc, &argv); 228 | 229 | static_assert(sizeof(unsigned long long int) == sizeof(uint64_t), "The original author designed this for x86_64 machine with the compiler that expects unsigned long long int and uint64_t to be the same thing, so they could use strtoull() function to parse it. Please adjust this code for your case and maybe even send the patch to upstream to make it work on a wider range of environments."); 230 | char *endptr; 231 | // TODO: replace strtoull with a custom solution 232 | // That way we can get rid of the dependency on errno and static_assert 233 | unsigned long long int result = strtoull(arg, &endptr, 10); 234 | 235 | if (*endptr != '\0') { 236 | c->flag_error = FLAG_ERROR_INVALID_NUMBER; 237 | c->flag_error_name = flag; 238 | return false; 239 | } 240 | 241 | if (result == ULLONG_MAX && errno == ERANGE) { 242 | c->flag_error = FLAG_ERROR_INTEGER_OVERFLOW; 243 | c->flag_error_name = flag; 244 | return false; 245 | } 246 | 247 | c->flags[i].val.as_uint64 = result; 248 | } 249 | break; 250 | 251 | case FLAG_SIZE: { 252 | if (argc == 0) { 253 | c->flag_error = FLAG_ERROR_NO_VALUE; 254 | c->flag_error_name = flag; 255 | return false; 256 | } 257 | char *arg = flag_shift_args(&argc, &argv); 258 | 259 | static_assert(sizeof(unsigned long long int) == sizeof(size_t), "The original author designed this for x86_64 machine with the compiler that expects unsigned long long int and size_t to be the same thing, so they could use strtoull() function to parse it. Please adjust this code for your case and maybe even send the patch to upstream to make it work on a wider range of environments."); 260 | char *endptr; 261 | // TODO: replace strtoull with a custom solution 262 | // That way we can get rid of the dependency on errno and static_assert 263 | unsigned long long int result = strtoull(arg, &endptr, 10); 264 | 265 | // TODO: handle more multiplicative suffixes like in dd(1). From the dd(1) man page: 266 | // > N and BYTES may be followed by the following 267 | // > multiplicative suffixes: c =1, w =2, b =512, kB =1000, K 268 | // > =1024, MB =1000*1000, M =1024*1024, xM =M, GB 269 | // > =1000*1000*1000, G =1024*1024*1024, and so on for T, P, 270 | // > E, Z, Y. 271 | if (strcmp(endptr, "K") == 0) { 272 | result *= 1024; 273 | } else if (strcmp(endptr, "M") == 0) { 274 | result *= 1024*1024; 275 | } else if (strcmp(endptr, "G") == 0) { 276 | result *= 1024*1024*1024; 277 | } else if (strcmp(endptr, "") != 0) { 278 | c->flag_error = FLAG_ERROR_INVALID_SIZE_SUFFIX; 279 | c->flag_error_name = flag; 280 | // TODO: capability to report what exactly is the wrong suffix 281 | return false; 282 | } 283 | 284 | if (result == ULLONG_MAX && errno == ERANGE) { 285 | c->flag_error = FLAG_ERROR_INTEGER_OVERFLOW; 286 | c->flag_error_name = flag; 287 | return false; 288 | } 289 | 290 | c->flags[i].val.as_size = result; 291 | } 292 | break; 293 | 294 | case COUNT_FLAG_TYPES: 295 | default: { 296 | assert(0 && "unreachable"); 297 | exit(69); 298 | } 299 | } 300 | 301 | found = true; 302 | } 303 | } 304 | 305 | if (!found) { 306 | c->flag_error = FLAG_ERROR_UNKNOWN; 307 | c->flag_error_name = flag; 308 | return false; 309 | } 310 | } 311 | 312 | c->rest_argc = argc; 313 | c->rest_argv = argv; 314 | return true; 315 | } 316 | 317 | void flag_print_options(FILE *stream) 318 | { 319 | Flag_Context *c = &flag_global_context; 320 | for (size_t i = 0; i < c->flags_count; ++i) { 321 | Flag *flag = &c->flags[i]; 322 | 323 | fprintf(stream, " -%s\n", flag->name); 324 | fprintf(stream, " %s\n", flag->desc); 325 | static_assert(COUNT_FLAG_TYPES == 4, "Exhaustive flag type defaults printing"); 326 | switch (c->flags[i].type) { 327 | case FLAG_BOOL: 328 | if (flag->def.as_bool) { 329 | fprintf(stream, " Default: %s\n", flag->def.as_bool ? "true" : "false"); 330 | } 331 | break; 332 | case FLAG_UINT64: 333 | fprintf(stream, " Default: %" PRIu64 "\n", flag->def.as_uint64); 334 | break; 335 | case FLAG_SIZE: 336 | fprintf(stream, " Default: %zu\n", flag->def.as_size); 337 | break; 338 | case FLAG_STR: 339 | if (flag->def.as_str) { 340 | fprintf(stream, " Default: %s\n", flag->def.as_str); 341 | } 342 | break; 343 | default: 344 | assert(0 && "unreachable"); 345 | exit(69); 346 | } 347 | } 348 | } 349 | 350 | void flag_print_error(FILE *stream) 351 | { 352 | Flag_Context *c = &flag_global_context; 353 | static_assert(COUNT_FLAG_ERRORS == 6, "Exhaustive flag error printing"); 354 | switch (c->flag_error) { 355 | case FLAG_NO_ERROR: 356 | // NOTE: don't call flag_print_error() if flag_parse() didn't return false, okay? ._. 357 | fprintf(stream, "Operation Failed Successfully! Please tell the developer of this software that they don't know what they are doing! :)"); 358 | break; 359 | case FLAG_ERROR_UNKNOWN: 360 | fprintf(stream, "ERROR: -%s: unknown flag\n", c->flag_error_name); 361 | break; 362 | case FLAG_ERROR_NO_VALUE: 363 | fprintf(stream, "ERROR: -%s: no value provided\n", c->flag_error_name); 364 | break; 365 | case FLAG_ERROR_INVALID_NUMBER: 366 | fprintf(stream, "ERROR: -%s: invalid number\n", c->flag_error_name); 367 | break; 368 | case FLAG_ERROR_INTEGER_OVERFLOW: 369 | fprintf(stream, "ERROR: -%s: integer overflow\n", c->flag_error_name); 370 | break; 371 | case FLAG_ERROR_INVALID_SIZE_SUFFIX: 372 | fprintf(stream, "ERROR: -%s: invalid size suffix\n", c->flag_error_name); 373 | break; 374 | case COUNT_FLAG_ERRORS: 375 | default: 376 | assert(0 && "unreachable"); 377 | exit(69); 378 | } 379 | } 380 | 381 | #endif // FLAG_IMPLEMENTATION 382 | 383 | /* 384 | Revision history: 385 | 386 | 1.0.0 (2025-03-03) Initial release 387 | Save program_name in the context 388 | 389 | */ 390 | 391 | // Copyright 2021 Alexey Kutepov 392 | // 393 | // Permission is hereby granted, free of charge, to any person obtaining a copy 394 | // of this software and associated documentation files (the "Software"), to 395 | // deal in the Software without restriction, including without limitation the 396 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 397 | // sell copies of the Software, and to permit persons to whom the Software is 398 | // furnished to do so, subject to the following conditions: 399 | // 400 | // The above copyright notice and this permission notice shall be included in 401 | // all copies or substantial portions of the Software. 402 | // 403 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 404 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 405 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 406 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 407 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 408 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 409 | // IN THE SOFTWARE. 410 | -------------------------------------------------------------------------------- /cmd/mpv-client/mpv-client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define NOB_IMPLEMENTATION 5 | #define NOB_STRIP_PREFIX 6 | #include "nob.h" 7 | #define FLAG_IMPLEMENTATION 8 | #include "flag.h" 9 | 10 | void usage(void) 11 | { 12 | fprintf(stderr, "Usage: %s [OPTIONS]\n", flag_program_name()); 13 | fprintf(stderr, "OPTIONS:\n"); 14 | flag_print_options(stderr); 15 | } 16 | 17 | int main(int argc, char **argv) 18 | { 19 | char **address = flag_str("a", "127.0.0.1", "Address of the server"); 20 | size_t *port = flag_size("p", 8080, "Port of the server"); 21 | bool *help = flag_bool("help", false, "Print this help message"); 22 | 23 | if (!flag_parse(argc, argv)) { 24 | usage(); 25 | flag_print_error(stderr); 26 | return 1; 27 | } 28 | 29 | if (*help) { 30 | usage(); 31 | return 0; 32 | } 33 | 34 | if (*address == NULL) { 35 | usage(); 36 | fprintf(stderr, "ERROR: no value provided -%s\n", flag_name(address)); 37 | return 1; 38 | } 39 | 40 | int client = socket(AF_INET, SOCK_STREAM, 0); 41 | if (client < 0) { 42 | fprintf(stderr, "ERROR: could not create socket: %s\n", strerror(errno)); 43 | return 1; 44 | } 45 | 46 | // Set server address 47 | struct sockaddr_in server_address = {0}; 48 | server_address.sin_family = AF_INET; 49 | server_address.sin_port = htons(*port); 50 | inet_pton(AF_INET, *address, &server_address.sin_addr); 51 | 52 | if (connect(client, (const struct sockaddr*)&server_address, sizeof(server_address)) < 0) { 53 | fprintf(stderr, "ERROR: could not connect to %s:%zu: %s\n", *address, *port, strerror(errno)); 54 | return 1; 55 | } 56 | 57 | // NOTE: This leaks a bit of memory in the child process. 58 | // But do we actually care? It's a one off leak anyway... 59 | Cmd cmd_null = {0}; 60 | cmd_append(&cmd_null, "mpv", temp_sprintf("--input-ipc-client=fd://%d", client)); 61 | da_append_many(&cmd_null, flag_rest_argv(), flag_rest_argc()); 62 | cmd_append(&cmd_null, NULL); 63 | 64 | if (execvp(cmd_null.items[0], (char * const*) cmd_null.items) < 0) { 65 | nob_log(ERROR, "Could not exec child process for %s: %s", cmd_null.items[0], strerror(errno)); 66 | return 1; 67 | } 68 | 69 | UNREACHABLE("mpv-client"); 70 | } 71 | -------------------------------------------------------------------------------- /cmd/mpv-client/nob.h: -------------------------------------------------------------------------------- 1 | /* nob - v1.20.2 - Public Domain - https://github.com/tsoding/nob.h 2 | 3 | This library is the next generation of the [NoBuild](https://github.com/tsoding/nobuild) idea. 4 | 5 | # Quick Example 6 | 7 | ```c 8 | // nob.c 9 | #define NOB_IMPLEMENTATION 10 | #include "nob.h" 11 | 12 | int main(int argc, char **argv) 13 | { 14 | NOB_GO_REBUILD_URSELF(argc, argv); 15 | Nob_Cmd cmd = {0}; 16 | nob_cmd_append(&cmd, "cc", "-Wall", "-Wextra", "-o", "main", "main.c"); 17 | if (!nob_cmd_run_sync(cmd)) return 1; 18 | return 0; 19 | } 20 | ``` 21 | 22 | ```console 23 | $ cc -o nob nob.c 24 | $ ./nob 25 | ``` 26 | 27 | The `nob` automatically rebuilds itself if `nob.c` is modified thanks to 28 | the `NOB_GO_REBUILD_URSELF` macro (don't forget to check out how it works below) 29 | 30 | # The Zoo of `nob_cmd_run_*` Functions 31 | 32 | `Nob_Cmd` is just a dynamic array of strings which represents a command and its arguments. 33 | First you append the arguments 34 | 35 | ```c 36 | Nob_Cmd cmd = {0}; 37 | nob_cmd_append(&cmd, "cc", "-Wall", "-Wextra", "-o", "main", "main.c"); 38 | ``` 39 | 40 | Then you run it 41 | 42 | ```c 43 | if (!nob_cmd_run_sync(cmd)) return 1; 44 | ``` 45 | 46 | `*_sync` at the end indicates that the function blocks until the command finishes executing 47 | and returns `true` on success and `false` on failure. You can run the command asynchronously 48 | but you have to explitictly wait for it afterwards: 49 | 50 | ```c 51 | Nob_Proc p = nob_cmd_run_async(cmd); 52 | if (p == NOB_INVALID_PROC) return 1; 53 | if (!nob_proc_wait(p)) return 1; 54 | ``` 55 | 56 | One of the problems with running commands like that is that `Nob_Cmd` still contains the arguments 57 | from the previously run command. If you want to reuse the same `Nob_Cmd` you have to not forget to reset 58 | it 59 | 60 | ```c 61 | Nob_Cmd cmd = {0}; 62 | 63 | nob_cmd_append(&cmd, "cc", "-Wall", "-Wextra", "-o", "main", "main.c"); 64 | if (!nob_cmd_run_sync(cmd)) return 1; 65 | cmd.count = 0; 66 | 67 | nob_cmd_append(&cmd, "./main", "foo", "bar", "baz"); 68 | if (!nob_cmd_run_sync(cmd)) return 1; 69 | cmd.count = 0; 70 | ``` 71 | 72 | Which is a bit error prone. To make it a bit easier we have `nob_cmd_run_sync_and_reset()` which 73 | accepts `Nob_Cmd` by reference and resets it for you: 74 | 75 | ```c 76 | Nob_Cmd cmd = {0}; 77 | 78 | nob_cmd_append(&cmd, "cc", "-Wall", "-Wextra", "-o", "main", "main.c"); 79 | if (!nob_cmd_run_sync_and_reset(&cmd)) return 1; 80 | 81 | nob_cmd_append(&cmd, "./main", "foo", "bar", "baz"); 82 | if (!nob_cmd_run_sync_and_reset(&cmd)) return 1; 83 | ``` 84 | 85 | There is of course also `nob_cmd_run_async_and_reset()` to maintain the pattern. 86 | 87 | The stdin, stdout and stderr of any command can be redirected by using `Nob_Cmd_Redirect` structure 88 | along with `nob_cmd_run_sync_redirect()` or `nob_cmd_run_async_redirect()` 89 | 90 | ```c 91 | // Opening all the necessary files 92 | Nob_Fd fdin = nob_fd_open_for_read("input.txt"); 93 | if (fdin == NOB_INVALID_FD) return 1; 94 | Nob_Fd fdout = nob_fd_open_for_write("output.txt"); 95 | if (fdout == NOB_INVALID_FD) return 1; 96 | Nob_Fd fderr = nob_fd_open_for_write("error.txt"); 97 | if (fderr == NOB_INVALID_FD) return 1; 98 | 99 | // Preparing the command 100 | Nob_Cmd cmd = {0}; 101 | nob_cmd_append(&cmd, "./main", "foo", "bar", "baz"); 102 | 103 | // Running the command synchronously redirecting the standard streams 104 | bool ok = nob_cmd_run_sync_redirect(cmd, (Nob_Cmd_Redirect) { 105 | .fdin = fdin, 106 | .fdout = fdout, 107 | .fderr = fderr, 108 | }); 109 | if (!ok) return 1; 110 | 111 | // Closing all the files 112 | nob_fd_close(fdin); 113 | nob_fd_close(fdout); 114 | nob_fd_close(fderr); 115 | 116 | // Reseting the command 117 | cmd.count = 0; 118 | ``` 119 | 120 | And of course if you find closing the files and reseting the command annoying we have 121 | `nob_cmd_run_sync_redirect_and_reset()` and `nob_cmd_run_async_redirect_and_reset()` 122 | which do all of that for you automatically. 123 | 124 | All the Zoo of `nob_cmd_run_*` functions follows the same pattern: sync/async, 125 | redirect/no redirect, and_reset/no and_reset. They always come in that order. 126 | 127 | # Stripping off `nob_` Prefixes 128 | 129 | Since Pure C does not have any namespaces we prefix each name of the API with the `nob_` to avoid any 130 | potential conflicts with any other names in your code. But sometimes it is very annoying and makes 131 | the code noisy. If you know that none of the names from nob.h conflict with anything in your code 132 | you can enable NOB_STRIP_PREFIX macro and just drop all the prefixes: 133 | 134 | ```c 135 | // nob.c 136 | #define NOB_IMPLEMENTATION 137 | #define NOB_STRIP_PREFIX 138 | #include "nob.h" 139 | 140 | int main(int argc, char **argv) 141 | { 142 | NOB_GO_REBUILD_URSELF(argc, argv); 143 | Cmd cmd = {0}; 144 | cmd_append(&cmd, "cc", "-Wall", "-Wextra", "-o", "main", "main.c"); 145 | if (!cmd_run_sync(cmd)) return 1; 146 | return 0; 147 | } 148 | ``` 149 | 150 | Not all the names have strippable prefixes. All the redefinable names like `NOB_GO_REBUILD_URSELF` 151 | for instance will retain their prefix even if NOB_STRIP_PREFIX is enabled. Notable exception is the 152 | nob_log() function. Stripping away the prefix results in log() which was historically always referring 153 | to the natural logarithmic function that is already defined in math.h. So there is no reason to strip 154 | off the prefix for nob_log(). 155 | 156 | The prefixes are stripped off only on the level of preprocessor. The names of the functions in the 157 | compiled object file will still retain the `nob_` prefix. Keep that in mind when you FFI with nob.h 158 | from other languages (for whatever reason). 159 | 160 | If only few specific names create conflicts for you, you can just #undef those names after the 161 | `#include ` since they are macros anyway. 162 | */ 163 | 164 | #ifndef NOB_H_ 165 | #define NOB_H_ 166 | 167 | #ifndef NOB_ASSERT 168 | #include 169 | #define NOB_ASSERT assert 170 | #endif /* NOB_ASSERT */ 171 | 172 | #ifndef NOB_REALLOC 173 | #include 174 | #define NOB_REALLOC realloc 175 | #endif /* NOB_REALLOC */ 176 | 177 | #ifndef NOB_FREE 178 | #include 179 | #define NOB_FREE free 180 | #endif /* NOB_FREE */ 181 | 182 | #include 183 | #include 184 | #include 185 | #include 186 | #include 187 | #include 188 | #include 189 | #include 190 | 191 | #ifdef _WIN32 192 | # define WIN32_LEAN_AND_MEAN 193 | # define _WINUSER_ 194 | # define _WINGDI_ 195 | # define _IMM_ 196 | # define _WINCON_ 197 | # include 198 | # include 199 | # include 200 | #else 201 | # include 202 | # include 203 | # include 204 | # include 205 | # include 206 | #endif 207 | 208 | #ifdef _WIN32 209 | # define NOB_LINE_END "\r\n" 210 | #else 211 | # define NOB_LINE_END "\n" 212 | #endif 213 | 214 | #if defined(__GNUC__) || defined(__clang__) 215 | // https://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html 216 | #define NOB_PRINTF_FORMAT(STRING_INDEX, FIRST_TO_CHECK) __attribute__ ((format (printf, STRING_INDEX, FIRST_TO_CHECK))) 217 | #else 218 | // TODO: implement NOB_PRINTF_FORMAT for MSVC 219 | #define NOB_PRINTF_FORMAT(STRING_INDEX, FIRST_TO_CHECK) 220 | #endif 221 | 222 | #define NOB_UNUSED(value) (void)(value) 223 | #define NOB_TODO(message) do { fprintf(stderr, "%s:%d: TODO: %s\n", __FILE__, __LINE__, message); abort(); } while(0) 224 | #define NOB_UNREACHABLE(message) do { fprintf(stderr, "%s:%d: UNREACHABLE: %s\n", __FILE__, __LINE__, message); abort(); } while(0) 225 | 226 | #define NOB_ARRAY_LEN(array) (sizeof(array)/sizeof(array[0])) 227 | #define NOB_ARRAY_GET(array, index) \ 228 | (NOB_ASSERT((size_t)index < NOB_ARRAY_LEN(array)), array[(size_t)index]) 229 | 230 | typedef enum { 231 | NOB_INFO, 232 | NOB_WARNING, 233 | NOB_ERROR, 234 | NOB_NO_LOGS, 235 | } Nob_Log_Level; 236 | 237 | // Any messages with the level below nob_minimal_log_level are going to be suppressed. 238 | extern Nob_Log_Level nob_minimal_log_level; 239 | 240 | void nob_log(Nob_Log_Level level, const char *fmt, ...) NOB_PRINTF_FORMAT(2, 3); 241 | 242 | // It is an equivalent of shift command from bash. It basically pops an element from 243 | // the beginning of a sized array. 244 | #define nob_shift(xs, xs_sz) (NOB_ASSERT((xs_sz) > 0), (xs_sz)--, *(xs)++) 245 | // NOTE: nob_shift_args() is an alias for an old variant of nob_shift that only worked with 246 | // the command line arguments passed to the main() function. nob_shift() is more generic. 247 | // So nob_shift_args() is semi-deprecated, but I don't see much reason to urgently 248 | // remove it. This alias does not hurt anybody. 249 | #define nob_shift_args(argc, argv) nob_shift(*argv, *argc) 250 | 251 | typedef struct { 252 | const char **items; 253 | size_t count; 254 | size_t capacity; 255 | } Nob_File_Paths; 256 | 257 | typedef enum { 258 | NOB_FILE_REGULAR = 0, 259 | NOB_FILE_DIRECTORY, 260 | NOB_FILE_SYMLINK, 261 | NOB_FILE_OTHER, 262 | } Nob_File_Type; 263 | 264 | bool nob_mkdir_if_not_exists(const char *path); 265 | bool nob_copy_file(const char *src_path, const char *dst_path); 266 | bool nob_copy_directory_recursively(const char *src_path, const char *dst_path); 267 | bool nob_read_entire_dir(const char *parent, Nob_File_Paths *children); 268 | bool nob_write_entire_file(const char *path, const void *data, size_t size); 269 | Nob_File_Type nob_get_file_type(const char *path); 270 | bool nob_delete_file(const char *path); 271 | 272 | #define nob_return_defer(value) do { result = (value); goto defer; } while(0) 273 | 274 | // Initial capacity of a dynamic array 275 | #ifndef NOB_DA_INIT_CAP 276 | #define NOB_DA_INIT_CAP 256 277 | #endif 278 | 279 | #define nob_da_reserve(da, expected_capacity) \ 280 | do { \ 281 | if ((expected_capacity) > (da)->capacity) { \ 282 | if ((da)->capacity == 0) { \ 283 | (da)->capacity = NOB_DA_INIT_CAP; \ 284 | } \ 285 | while ((expected_capacity) > (da)->capacity) { \ 286 | (da)->capacity *= 2; \ 287 | } \ 288 | (da)->items = NOB_REALLOC((da)->items, (da)->capacity * sizeof(*(da)->items)); \ 289 | NOB_ASSERT((da)->items != NULL && "Buy more RAM lol"); \ 290 | } \ 291 | } while (0) 292 | 293 | // Append an item to a dynamic array 294 | #define nob_da_append(da, item) \ 295 | do { \ 296 | nob_da_reserve((da), (da)->count + 1); \ 297 | (da)->items[(da)->count++] = (item); \ 298 | } while (0) 299 | 300 | #define nob_da_free(da) NOB_FREE((da).items) 301 | 302 | // Append several items to a dynamic array 303 | #define nob_da_append_many(da, new_items, new_items_count) \ 304 | do { \ 305 | nob_da_reserve((da), (da)->count + (new_items_count)); \ 306 | memcpy((da)->items + (da)->count, (new_items), (new_items_count)*sizeof(*(da)->items)); \ 307 | (da)->count += (new_items_count); \ 308 | } while (0) 309 | 310 | #define nob_da_resize(da, new_size) \ 311 | do { \ 312 | nob_da_reserve((da), new_size); \ 313 | (da)->count = (new_size); \ 314 | } while (0) 315 | 316 | #define nob_da_last(da) (da)->items[(NOB_ASSERT((da)->count > 0), (da)->count-1)] 317 | #define nob_da_remove_unordered(da, i) \ 318 | do { \ 319 | size_t j = (i); \ 320 | NOB_ASSERT(j < (da)->count); \ 321 | (da)->items[j] = (da)->items[--(da)->count]; \ 322 | } while(0) 323 | 324 | // Foreach over Dynamic Arrays. Example: 325 | // ```c 326 | // typedef struct { 327 | // int *items; 328 | // size_t count; 329 | // size_t capacity; 330 | // } Numbers; 331 | // 332 | // Numbers xs = {0}; 333 | // 334 | // nob_da_append(&xs, 69); 335 | // nob_da_append(&xs, 420); 336 | // nob_da_append(&xs, 1337); 337 | // 338 | // nob_da_foreach(int, x, &xs) { 339 | // // `x` here is a pointer to the current element. You can get its index by taking a difference 340 | // // between `x` and the start of the array which is `x.items`. 341 | // size_t index = x - xs.items; 342 | // nob_log(INFO, "%zu: %d", index, *x); 343 | // } 344 | // ``` 345 | #define nob_da_foreach(Type, it, da) for (Type *it = (da)->items; it < (da)->items + (da)->count; ++it) 346 | 347 | typedef struct { 348 | char *items; 349 | size_t count; 350 | size_t capacity; 351 | } Nob_String_Builder; 352 | 353 | bool nob_read_entire_file(const char *path, Nob_String_Builder *sb); 354 | int nob_sb_appendf(Nob_String_Builder *sb, const char *fmt, ...) NOB_PRINTF_FORMAT(2, 3); 355 | 356 | // Append a sized buffer to a string builder 357 | #define nob_sb_append_buf(sb, buf, size) nob_da_append_many(sb, buf, size) 358 | 359 | // Append a NULL-terminated string to a string builder 360 | #define nob_sb_append_cstr(sb, cstr) \ 361 | do { \ 362 | const char *s = (cstr); \ 363 | size_t n = strlen(s); \ 364 | nob_da_append_many(sb, s, n); \ 365 | } while (0) 366 | 367 | // Append a single NULL character at the end of a string builder. So then you can 368 | // use it a NULL-terminated C string 369 | #define nob_sb_append_null(sb) nob_da_append_many(sb, "", 1) 370 | 371 | // Free the memory allocated by a string builder 372 | #define nob_sb_free(sb) NOB_FREE((sb).items) 373 | 374 | // Process handle 375 | #ifdef _WIN32 376 | typedef HANDLE Nob_Proc; 377 | #define NOB_INVALID_PROC INVALID_HANDLE_VALUE 378 | typedef HANDLE Nob_Fd; 379 | #define NOB_INVALID_FD INVALID_HANDLE_VALUE 380 | #else 381 | typedef int Nob_Proc; 382 | #define NOB_INVALID_PROC (-1) 383 | typedef int Nob_Fd; 384 | #define NOB_INVALID_FD (-1) 385 | #endif // _WIN32 386 | 387 | Nob_Fd nob_fd_open_for_read(const char *path); 388 | Nob_Fd nob_fd_open_for_write(const char *path); 389 | void nob_fd_close(Nob_Fd fd); 390 | 391 | typedef struct { 392 | Nob_Proc *items; 393 | size_t count; 394 | size_t capacity; 395 | } Nob_Procs; 396 | 397 | // Wait until the process has finished 398 | bool nob_proc_wait(Nob_Proc proc); 399 | // Wait until all the processes have finished 400 | bool nob_procs_wait(Nob_Procs procs); 401 | // Wait until all the processes have finished and empty the procs array 402 | bool nob_procs_wait_and_reset(Nob_Procs *procs); 403 | // Append a new process to procs array and if procs.count reaches max_procs_count call nob_procs_wait_and_reset() on it 404 | bool nob_procs_append_with_flush(Nob_Procs *procs, Nob_Proc proc, size_t max_procs_count); 405 | 406 | // A command - the main workhorse of Nob. Nob is all about building commands and running them 407 | typedef struct { 408 | const char **items; 409 | size_t count; 410 | size_t capacity; 411 | } Nob_Cmd; 412 | 413 | // Example: 414 | // ```c 415 | // Nob_Fd fdin = nob_fd_open_for_read("input.txt"); 416 | // if (fdin == NOB_INVALID_FD) fail(); 417 | // Nob_Fd fdout = nob_fd_open_for_write("output.txt"); 418 | // if (fdout == NOB_INVALID_FD) fail(); 419 | // Nob_Cmd cmd = {0}; 420 | // nob_cmd_append(&cmd, "cat"); 421 | // if (!nob_cmd_run_sync_redirect_and_reset(&cmd, (Nob_Cmd_Redirect) { 422 | // .fdin = &fdin, 423 | // .fdout = &fdout 424 | // })) fail(); 425 | // ``` 426 | typedef struct { 427 | Nob_Fd *fdin; 428 | Nob_Fd *fdout; 429 | Nob_Fd *fderr; 430 | } Nob_Cmd_Redirect; 431 | 432 | // Render a string representation of a command into a string builder. Keep in mind the the 433 | // string builder is not NULL-terminated by default. Use nob_sb_append_null if you plan to 434 | // use it as a C string. 435 | void nob_cmd_render(Nob_Cmd cmd, Nob_String_Builder *render); 436 | 437 | // TODO: implement C++ support for nob.h 438 | #define nob_cmd_append(cmd, ...) \ 439 | nob_da_append_many(cmd, \ 440 | ((const char*[]){__VA_ARGS__}), \ 441 | (sizeof((const char*[]){__VA_ARGS__})/sizeof(const char*))) 442 | 443 | #define nob_cmd_extend(cmd, other_cmd) \ 444 | nob_da_append_many(cmd, (other_cmd)->items, (other_cmd)->count) 445 | 446 | // Free all the memory allocated by command arguments 447 | #define nob_cmd_free(cmd) NOB_FREE(cmd.items) 448 | 449 | // Run command asynchronously 450 | #define nob_cmd_run_async(cmd) nob_cmd_run_async_redirect(cmd, (Nob_Cmd_Redirect) {0}) 451 | // NOTE: nob_cmd_run_async_and_reset() is just like nob_cmd_run_async() except it also resets cmd.count to 0 452 | // so the Nob_Cmd instance can be seamlessly used several times in a row 453 | Nob_Proc nob_cmd_run_async_and_reset(Nob_Cmd *cmd); 454 | // Run redirected command asynchronously 455 | Nob_Proc nob_cmd_run_async_redirect(Nob_Cmd cmd, Nob_Cmd_Redirect redirect); 456 | // Run redirected command asynchronously and set cmd.count to 0 and close all the opened files 457 | Nob_Proc nob_cmd_run_async_redirect_and_reset(Nob_Cmd *cmd, Nob_Cmd_Redirect redirect); 458 | 459 | // Run command synchronously 460 | bool nob_cmd_run_sync(Nob_Cmd cmd); 461 | // NOTE: nob_cmd_run_sync_and_reset() is just like nob_cmd_run_sync() except it also resets cmd.count to 0 462 | // so the Nob_Cmd instance can be seamlessly used several times in a row 463 | bool nob_cmd_run_sync_and_reset(Nob_Cmd *cmd); 464 | // Run redirected command synchronously 465 | bool nob_cmd_run_sync_redirect(Nob_Cmd cmd, Nob_Cmd_Redirect redirect); 466 | // Run redirected command synchronously and set cmd.count to 0 and close all the opened files 467 | bool nob_cmd_run_sync_redirect_and_reset(Nob_Cmd *cmd, Nob_Cmd_Redirect redirect); 468 | 469 | #ifndef NOB_TEMP_CAPACITY 470 | #define NOB_TEMP_CAPACITY (8*1024*1024) 471 | #endif // NOB_TEMP_CAPACITY 472 | char *nob_temp_strdup(const char *cstr); 473 | void *nob_temp_alloc(size_t size); 474 | char *nob_temp_sprintf(const char *format, ...) NOB_PRINTF_FORMAT(1, 2); 475 | void nob_temp_reset(void); 476 | size_t nob_temp_save(void); 477 | void nob_temp_rewind(size_t checkpoint); 478 | 479 | // Given any path returns the last part of that path. 480 | // "/path/to/a/file.c" -> "file.c"; "/path/to/a/directory" -> "directory" 481 | const char *nob_path_name(const char *path); 482 | bool nob_rename(const char *old_path, const char *new_path); 483 | int nob_needs_rebuild(const char *output_path, const char **input_paths, size_t input_paths_count); 484 | int nob_needs_rebuild1(const char *output_path, const char *input_path); 485 | int nob_file_exists(const char *file_path); 486 | const char *nob_get_current_dir_temp(void); 487 | bool nob_set_current_dir(const char *path); 488 | 489 | // TODO: we should probably document somewhere all the compiler we support 490 | 491 | // The nob_cc_* macros try to abstract away the specific compiler. 492 | // They are verify basic and not particularly flexible, but you can redefine them if you need to 493 | // or not use them at all and create your own abstraction on top of Nob_Cmd. 494 | 495 | #ifndef nob_cc 496 | # if _WIN32 497 | # if defined(__GNUC__) 498 | # define nob_cc(cmd) nob_cmd_append(cmd, "cc") 499 | # elif defined(__clang__) 500 | # define nob_cc(cmd) nob_cmd_append(cmd, "clang") 501 | # elif defined(_MSC_VER) 502 | # define nob_cc(cmd) nob_cmd_append(cmd, "cl.exe") 503 | # endif 504 | # else 505 | # define nob_cc(cmd) nob_cmd_append(cmd, "cc") 506 | # endif 507 | #endif // nob_cc 508 | 509 | #ifndef nob_cc_flags 510 | # if defined(_MSC_VER) 511 | # define nob_cc_flags(...) // TODO: Add some cool recommended flags for MSVC (I don't really know any) 512 | # else 513 | # define nob_cc_flags(cmd) nob_cmd_append(cmd, "-Wall", "-Wextra") 514 | # endif 515 | #endif // nob_cc_output 516 | 517 | #ifndef nob_cc_output 518 | # if defined(_MSC_VER) 519 | # define nob_cc_output(cmd, output_path) nob_cmd_append(cmd, nob_temp_sprintf("/Fe:%s", (output_path))) 520 | # else 521 | # define nob_cc_output(cmd, output_path) nob_cmd_append(cmd, "-o", (output_path)) 522 | # endif 523 | #endif // nob_cc_output 524 | 525 | #ifndef nob_cc_inputs 526 | # define nob_cc_inputs(cmd, ...) nob_cmd_append(cmd, __VA_ARGS__) 527 | #endif // nob_cc_inputs 528 | 529 | // TODO: add MinGW support for Go Rebuild Urself™ Technology and all the nob_cc_* macros above 530 | // Musializer contributors came up with a pretty interesting idea of an optional prefix macro which could be useful for 531 | // MinGW support: 532 | // https://github.com/tsoding/musializer/blob/b7578cc76b9ecb573d239acc9ccf5a04d3aba2c9/src_build/nob_win64_mingw.c#L3-L9 533 | // TODO: Maybe instead NOB_REBUILD_URSELF macro, the Go Rebuild Urself™ Technology should use the 534 | // user defined nob_cc_* macros instead? 535 | #ifndef NOB_REBUILD_URSELF 536 | # if defined(_WIN32) 537 | # if defined(__GNUC__) 538 | # define NOB_REBUILD_URSELF(binary_path, source_path) "gcc", "-o", binary_path, source_path 539 | # elif defined(__clang__) 540 | # define NOB_REBUILD_URSELF(binary_path, source_path) "clang", "-o", binary_path, source_path 541 | # elif defined(_MSC_VER) 542 | # define NOB_REBUILD_URSELF(binary_path, source_path) "cl.exe", nob_temp_sprintf("/Fe:%s", (binary_path)), source_path 543 | # endif 544 | # else 545 | # define NOB_REBUILD_URSELF(binary_path, source_path) "cc", "-o", binary_path, source_path 546 | # endif 547 | #endif 548 | 549 | // Go Rebuild Urself™ Technology 550 | // 551 | // How to use it: 552 | // int main(int argc, char** argv) { 553 | // NOB_GO_REBUILD_URSELF(argc, argv); 554 | // // actual work 555 | // return 0; 556 | // } 557 | // 558 | // After your added this macro every time you run ./nob it will detect 559 | // that you modified its original source code and will try to rebuild itself 560 | // before doing any actual work. So you only need to bootstrap your build system 561 | // once. 562 | // 563 | // The modification is detected by comparing the last modified times of the executable 564 | // and its source code. The same way the make utility usually does it. 565 | // 566 | // The rebuilding is done by using the NOB_REBUILD_URSELF macro which you can redefine 567 | // if you need a special way of bootstraping your build system. (which I personally 568 | // do not recommend since the whole idea of NoBuild is to keep the process of bootstrapping 569 | // as simple as possible and doing all of the actual work inside of ./nob) 570 | // 571 | void nob__go_rebuild_urself(int argc, char **argv, const char *source_path, ...); 572 | #define NOB_GO_REBUILD_URSELF(argc, argv) nob__go_rebuild_urself(argc, argv, __FILE__, NULL) 573 | // Sometimes your nob.c includes additional files, so you want the Go Rebuild Urself™ Technology to check 574 | // if they also were modified and rebuild nob.c accordingly. For that we have NOB_GO_REBUILD_URSELF_PLUS(): 575 | // ```c 576 | // #define NOB_IMPLEMENTATION 577 | // #include "nob.h" 578 | // 579 | // #include "foo.c" 580 | // #include "bar.c" 581 | // 582 | // int main(int argc, char **argv) 583 | // { 584 | // NOB_GO_REBUILD_URSELF_PLUS(argc, argv, "foo.c", "bar.c"); 585 | // // ... 586 | // return 0; 587 | // } 588 | #define NOB_GO_REBUILD_URSELF_PLUS(argc, argv, ...) nob__go_rebuild_urself(argc, argv, __FILE__, __VA_ARGS__, NULL); 589 | 590 | typedef struct { 591 | size_t count; 592 | const char *data; 593 | } Nob_String_View; 594 | 595 | const char *nob_temp_sv_to_cstr(Nob_String_View sv); 596 | 597 | Nob_String_View nob_sv_chop_by_delim(Nob_String_View *sv, char delim); 598 | Nob_String_View nob_sv_chop_left(Nob_String_View *sv, size_t n); 599 | Nob_String_View nob_sv_trim(Nob_String_View sv); 600 | Nob_String_View nob_sv_trim_left(Nob_String_View sv); 601 | Nob_String_View nob_sv_trim_right(Nob_String_View sv); 602 | bool nob_sv_eq(Nob_String_View a, Nob_String_View b); 603 | bool nob_sv_end_with(Nob_String_View sv, const char *cstr); 604 | bool nob_sv_starts_with(Nob_String_View sv, Nob_String_View expected_prefix); 605 | Nob_String_View nob_sv_from_cstr(const char *cstr); 606 | Nob_String_View nob_sv_from_parts(const char *data, size_t count); 607 | // nob_sb_to_sv() enables you to just view Nob_String_Builder as Nob_String_View 608 | #define nob_sb_to_sv(sb) nob_sv_from_parts((sb).items, (sb).count) 609 | 610 | // printf macros for String_View 611 | #ifndef SV_Fmt 612 | #define SV_Fmt "%.*s" 613 | #endif // SV_Fmt 614 | #ifndef SV_Arg 615 | #define SV_Arg(sv) (int) (sv).count, (sv).data 616 | #endif // SV_Arg 617 | // USAGE: 618 | // String_View name = ...; 619 | // printf("Name: "SV_Fmt"\n", SV_Arg(name)); 620 | 621 | 622 | // minirent.h HEADER BEGIN //////////////////////////////////////// 623 | // Copyright 2021 Alexey Kutepov 624 | // 625 | // Permission is hereby granted, free of charge, to any person obtaining 626 | // a copy of this software and associated documentation files (the 627 | // "Software"), to deal in the Software without restriction, including 628 | // without limitation the rights to use, copy, modify, merge, publish, 629 | // distribute, sublicense, and/or sell copies of the Software, and to 630 | // permit persons to whom the Software is furnished to do so, subject to 631 | // the following conditions: 632 | // 633 | // The above copyright notice and this permission notice shall be 634 | // included in all copies or substantial portions of the Software. 635 | // 636 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 637 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 638 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 639 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 640 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 641 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 642 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 643 | // 644 | // ============================================================ 645 | // 646 | // minirent — 0.0.1 — A subset of dirent interface for Windows. 647 | // 648 | // https://github.com/tsoding/minirent 649 | // 650 | // ============================================================ 651 | // 652 | // ChangeLog (https://semver.org/ is implied) 653 | // 654 | // 0.0.2 Automatically include dirent.h on non-Windows 655 | // platforms 656 | // 0.0.1 First Official Release 657 | 658 | #ifndef _WIN32 659 | #include 660 | #else // _WIN32 661 | 662 | #define WIN32_LEAN_AND_MEAN 663 | #include "windows.h" 664 | 665 | struct dirent 666 | { 667 | char d_name[MAX_PATH+1]; 668 | }; 669 | 670 | typedef struct DIR DIR; 671 | 672 | static DIR *opendir(const char *dirpath); 673 | static struct dirent *readdir(DIR *dirp); 674 | static int closedir(DIR *dirp); 675 | 676 | #endif // _WIN32 677 | // minirent.h HEADER END //////////////////////////////////////// 678 | 679 | #ifdef _WIN32 680 | 681 | char *nob_win32_error_message(DWORD err); 682 | 683 | #endif // _WIN32 684 | 685 | #endif // NOB_H_ 686 | 687 | #ifdef NOB_IMPLEMENTATION 688 | 689 | // Any messages with the level below nob_minimal_log_level are going to be suppressed. 690 | Nob_Log_Level nob_minimal_log_level = NOB_INFO; 691 | 692 | #ifdef _WIN32 693 | 694 | // Base on https://stackoverflow.com/a/75644008 695 | // > .NET Core uses 4096 * sizeof(WCHAR) buffer on stack for FormatMessageW call. And...thats it. 696 | // > 697 | // > https://github.com/dotnet/runtime/blob/3b63eb1346f1ddbc921374a5108d025662fb5ffd/src/coreclr/utilcode/posterror.cpp#L264-L265 698 | #ifndef NOB_WIN32_ERR_MSG_SIZE 699 | #define NOB_WIN32_ERR_MSG_SIZE (4 * 1024) 700 | #endif // NOB_WIN32_ERR_MSG_SIZE 701 | 702 | char *nob_win32_error_message(DWORD err) { 703 | static char win32ErrMsg[NOB_WIN32_ERR_MSG_SIZE] = {0}; 704 | DWORD errMsgSize = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, err, LANG_USER_DEFAULT, win32ErrMsg, 705 | NOB_WIN32_ERR_MSG_SIZE, NULL); 706 | 707 | if (errMsgSize == 0) { 708 | if (GetLastError() != ERROR_MR_MID_NOT_FOUND) { 709 | if (sprintf(win32ErrMsg, "Could not get error message for 0x%lX", err) > 0) { 710 | return (char *)&win32ErrMsg; 711 | } else { 712 | return NULL; 713 | } 714 | } else { 715 | if (sprintf(win32ErrMsg, "Invalid Windows Error code (0x%lX)", err) > 0) { 716 | return (char *)&win32ErrMsg; 717 | } else { 718 | return NULL; 719 | } 720 | } 721 | } 722 | 723 | while (errMsgSize > 1 && isspace(win32ErrMsg[errMsgSize - 1])) { 724 | win32ErrMsg[--errMsgSize] = '\0'; 725 | } 726 | 727 | return win32ErrMsg; 728 | } 729 | 730 | #endif // _WIN32 731 | 732 | // The implementation idea is stolen from https://github.com/zhiayang/nabs 733 | void nob__go_rebuild_urself(int argc, char **argv, const char *source_path, ...) 734 | { 735 | const char *binary_path = nob_shift(argv, argc); 736 | #ifdef _WIN32 737 | // On Windows executables almost always invoked without extension, so 738 | // it's ./nob, not ./nob.exe. For renaming the extension is a must. 739 | if (!nob_sv_end_with(nob_sv_from_cstr(binary_path), ".exe")) { 740 | binary_path = nob_temp_sprintf("%s.exe", binary_path); 741 | } 742 | #endif 743 | 744 | Nob_File_Paths source_paths = {0}; 745 | nob_da_append(&source_paths, source_path); 746 | va_list args; 747 | va_start(args, source_path); 748 | for (;;) { 749 | const char *path = va_arg(args, const char*); 750 | if (path == NULL) break; 751 | nob_da_append(&source_paths, path); 752 | } 753 | va_end(args); 754 | 755 | int rebuild_is_needed = nob_needs_rebuild(binary_path, source_paths.items, source_paths.count); 756 | if (rebuild_is_needed < 0) exit(1); // error 757 | if (!rebuild_is_needed) { // no rebuild is needed 758 | NOB_FREE(source_paths.items); 759 | return; 760 | } 761 | 762 | Nob_Cmd cmd = {0}; 763 | 764 | const char *old_binary_path = nob_temp_sprintf("%s.old", binary_path); 765 | 766 | if (!nob_rename(binary_path, old_binary_path)) exit(1); 767 | nob_cmd_append(&cmd, NOB_REBUILD_URSELF(binary_path, source_path)); 768 | if (!nob_cmd_run_sync_and_reset(&cmd)) { 769 | nob_rename(old_binary_path, binary_path); 770 | exit(1); 771 | } 772 | #ifdef NOB_EXPERIMENTAL_DELETE_OLD 773 | // TODO: this is an experimental behavior behind a compilation flag. 774 | // Once it is confirmed that it does not cause much problems on both POSIX and Windows 775 | // we may turn it on by default. 776 | nob_delete_file(old_binary_path); 777 | #endif // NOB_EXPERIMENTAL_DELETE_OLD 778 | 779 | nob_cmd_append(&cmd, binary_path); 780 | nob_da_append_many(&cmd, argv, argc); 781 | if (!nob_cmd_run_sync_and_reset(&cmd)) exit(1); 782 | exit(0); 783 | } 784 | 785 | static size_t nob_temp_size = 0; 786 | static char nob_temp[NOB_TEMP_CAPACITY] = {0}; 787 | 788 | bool nob_mkdir_if_not_exists(const char *path) 789 | { 790 | #ifdef _WIN32 791 | int result = mkdir(path); 792 | #else 793 | int result = mkdir(path, 0755); 794 | #endif 795 | if (result < 0) { 796 | if (errno == EEXIST) { 797 | nob_log(NOB_INFO, "directory `%s` already exists", path); 798 | return true; 799 | } 800 | nob_log(NOB_ERROR, "could not create directory `%s`: %s", path, strerror(errno)); 801 | return false; 802 | } 803 | 804 | nob_log(NOB_INFO, "created directory `%s`", path); 805 | return true; 806 | } 807 | 808 | bool nob_copy_file(const char *src_path, const char *dst_path) 809 | { 810 | nob_log(NOB_INFO, "copying %s -> %s", src_path, dst_path); 811 | #ifdef _WIN32 812 | if (!CopyFile(src_path, dst_path, FALSE)) { 813 | nob_log(NOB_ERROR, "Could not copy file: %s", nob_win32_error_message(GetLastError())); 814 | return false; 815 | } 816 | return true; 817 | #else 818 | int src_fd = -1; 819 | int dst_fd = -1; 820 | size_t buf_size = 32*1024; 821 | char *buf = NOB_REALLOC(NULL, buf_size); 822 | NOB_ASSERT(buf != NULL && "Buy more RAM lol!!"); 823 | bool result = true; 824 | 825 | src_fd = open(src_path, O_RDONLY); 826 | if (src_fd < 0) { 827 | nob_log(NOB_ERROR, "Could not open file %s: %s", src_path, strerror(errno)); 828 | nob_return_defer(false); 829 | } 830 | 831 | struct stat src_stat; 832 | if (fstat(src_fd, &src_stat) < 0) { 833 | nob_log(NOB_ERROR, "Could not get mode of file %s: %s", src_path, strerror(errno)); 834 | nob_return_defer(false); 835 | } 836 | 837 | dst_fd = open(dst_path, O_CREAT | O_TRUNC | O_WRONLY, src_stat.st_mode); 838 | if (dst_fd < 0) { 839 | nob_log(NOB_ERROR, "Could not create file %s: %s", dst_path, strerror(errno)); 840 | nob_return_defer(false); 841 | } 842 | 843 | for (;;) { 844 | ssize_t n = read(src_fd, buf, buf_size); 845 | if (n == 0) break; 846 | if (n < 0) { 847 | nob_log(NOB_ERROR, "Could not read from file %s: %s", src_path, strerror(errno)); 848 | nob_return_defer(false); 849 | } 850 | char *buf2 = buf; 851 | while (n > 0) { 852 | ssize_t m = write(dst_fd, buf2, n); 853 | if (m < 0) { 854 | nob_log(NOB_ERROR, "Could not write to file %s: %s", dst_path, strerror(errno)); 855 | nob_return_defer(false); 856 | } 857 | n -= m; 858 | buf2 += m; 859 | } 860 | } 861 | 862 | defer: 863 | NOB_FREE(buf); 864 | close(src_fd); 865 | close(dst_fd); 866 | return result; 867 | #endif 868 | } 869 | 870 | void nob_cmd_render(Nob_Cmd cmd, Nob_String_Builder *render) 871 | { 872 | for (size_t i = 0; i < cmd.count; ++i) { 873 | const char *arg = cmd.items[i]; 874 | if (arg == NULL) break; 875 | if (i > 0) nob_sb_append_cstr(render, " "); 876 | if (!strchr(arg, ' ')) { 877 | nob_sb_append_cstr(render, arg); 878 | } else { 879 | nob_da_append(render, '\''); 880 | nob_sb_append_cstr(render, arg); 881 | nob_da_append(render, '\''); 882 | } 883 | } 884 | } 885 | 886 | Nob_Proc nob_cmd_run_async_redirect(Nob_Cmd cmd, Nob_Cmd_Redirect redirect) 887 | { 888 | if (cmd.count < 1) { 889 | nob_log(NOB_ERROR, "Could not run empty command"); 890 | return NOB_INVALID_PROC; 891 | } 892 | 893 | Nob_String_Builder sb = {0}; 894 | nob_cmd_render(cmd, &sb); 895 | nob_sb_append_null(&sb); 896 | nob_log(NOB_INFO, "CMD: %s", sb.items); 897 | nob_sb_free(sb); 898 | memset(&sb, 0, sizeof(sb)); 899 | 900 | #ifdef _WIN32 901 | // https://docs.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output 902 | 903 | STARTUPINFO siStartInfo; 904 | ZeroMemory(&siStartInfo, sizeof(siStartInfo)); 905 | siStartInfo.cb = sizeof(STARTUPINFO); 906 | // NOTE: theoretically setting NULL to std handles should not be a problem 907 | // https://docs.microsoft.com/en-us/windows/console/getstdhandle?redirectedfrom=MSDN#attachdetach-behavior 908 | // TODO: check for errors in GetStdHandle 909 | siStartInfo.hStdError = redirect.fderr ? *redirect.fderr : GetStdHandle(STD_ERROR_HANDLE); 910 | siStartInfo.hStdOutput = redirect.fdout ? *redirect.fdout : GetStdHandle(STD_OUTPUT_HANDLE); 911 | siStartInfo.hStdInput = redirect.fdin ? *redirect.fdin : GetStdHandle(STD_INPUT_HANDLE); 912 | siStartInfo.dwFlags |= STARTF_USESTDHANDLES; 913 | 914 | PROCESS_INFORMATION piProcInfo; 915 | ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION)); 916 | 917 | // TODO: use a more reliable rendering of the command instead of cmd_render 918 | // cmd_render is for logging primarily 919 | nob_cmd_render(cmd, &sb); 920 | nob_sb_append_null(&sb); 921 | BOOL bSuccess = CreateProcessA(NULL, sb.items, NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &piProcInfo); 922 | nob_sb_free(sb); 923 | 924 | if (!bSuccess) { 925 | nob_log(NOB_ERROR, "Could not create child process for %s: %s", cmd.items[0], nob_win32_error_message(GetLastError())); 926 | return NOB_INVALID_PROC; 927 | } 928 | 929 | CloseHandle(piProcInfo.hThread); 930 | 931 | return piProcInfo.hProcess; 932 | #else 933 | pid_t cpid = fork(); 934 | if (cpid < 0) { 935 | nob_log(NOB_ERROR, "Could not fork child process: %s", strerror(errno)); 936 | return NOB_INVALID_PROC; 937 | } 938 | 939 | if (cpid == 0) { 940 | if (redirect.fdin) { 941 | if (dup2(*redirect.fdin, STDIN_FILENO) < 0) { 942 | nob_log(NOB_ERROR, "Could not setup stdin for child process: %s", strerror(errno)); 943 | exit(1); 944 | } 945 | } 946 | 947 | if (redirect.fdout) { 948 | if (dup2(*redirect.fdout, STDOUT_FILENO) < 0) { 949 | nob_log(NOB_ERROR, "Could not setup stdout for child process: %s", strerror(errno)); 950 | exit(1); 951 | } 952 | } 953 | 954 | if (redirect.fderr) { 955 | if (dup2(*redirect.fderr, STDERR_FILENO) < 0) { 956 | nob_log(NOB_ERROR, "Could not setup stderr for child process: %s", strerror(errno)); 957 | exit(1); 958 | } 959 | } 960 | 961 | // NOTE: This leaks a bit of memory in the child process. 962 | // But do we actually care? It's a one off leak anyway... 963 | Nob_Cmd cmd_null = {0}; 964 | nob_da_append_many(&cmd_null, cmd.items, cmd.count); 965 | nob_cmd_append(&cmd_null, NULL); 966 | 967 | if (execvp(cmd.items[0], (char * const*) cmd_null.items) < 0) { 968 | nob_log(NOB_ERROR, "Could not exec child process for %s: %s", cmd.items[0], strerror(errno)); 969 | exit(1); 970 | } 971 | NOB_UNREACHABLE("nob_cmd_run_async_redirect"); 972 | } 973 | 974 | return cpid; 975 | #endif 976 | } 977 | 978 | Nob_Proc nob_cmd_run_async_and_reset(Nob_Cmd *cmd) 979 | { 980 | Nob_Proc proc = nob_cmd_run_async(*cmd); 981 | cmd->count = 0; 982 | return proc; 983 | } 984 | 985 | Nob_Proc nob_cmd_run_async_redirect_and_reset(Nob_Cmd *cmd, Nob_Cmd_Redirect redirect) 986 | { 987 | Nob_Proc proc = nob_cmd_run_async_redirect(*cmd, redirect); 988 | cmd->count = 0; 989 | if (redirect.fdin) { 990 | nob_fd_close(*redirect.fdin); 991 | *redirect.fdin = NOB_INVALID_FD; 992 | } 993 | if (redirect.fdout) { 994 | nob_fd_close(*redirect.fdout); 995 | *redirect.fdout = NOB_INVALID_FD; 996 | } 997 | if (redirect.fderr) { 998 | nob_fd_close(*redirect.fderr); 999 | *redirect.fderr = NOB_INVALID_FD; 1000 | } 1001 | return proc; 1002 | } 1003 | 1004 | Nob_Fd nob_fd_open_for_read(const char *path) 1005 | { 1006 | #ifndef _WIN32 1007 | Nob_Fd result = open(path, O_RDONLY); 1008 | if (result < 0) { 1009 | nob_log(NOB_ERROR, "Could not open file %s: %s", path, strerror(errno)); 1010 | return NOB_INVALID_FD; 1011 | } 1012 | return result; 1013 | #else 1014 | // https://docs.microsoft.com/en-us/windows/win32/fileio/opening-a-file-for-reading-or-writing 1015 | SECURITY_ATTRIBUTES saAttr = {0}; 1016 | saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); 1017 | saAttr.bInheritHandle = TRUE; 1018 | 1019 | Nob_Fd result = CreateFile( 1020 | path, 1021 | GENERIC_READ, 1022 | 0, 1023 | &saAttr, 1024 | OPEN_EXISTING, 1025 | FILE_ATTRIBUTE_READONLY, 1026 | NULL); 1027 | 1028 | if (result == INVALID_HANDLE_VALUE) { 1029 | nob_log(NOB_ERROR, "Could not open file %s: %s", path, nob_win32_error_message(GetLastError())); 1030 | return NOB_INVALID_FD; 1031 | } 1032 | 1033 | return result; 1034 | #endif // _WIN32 1035 | } 1036 | 1037 | Nob_Fd nob_fd_open_for_write(const char *path) 1038 | { 1039 | #ifndef _WIN32 1040 | Nob_Fd result = open(path, 1041 | O_WRONLY | O_CREAT | O_TRUNC, 1042 | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); 1043 | if (result < 0) { 1044 | nob_log(NOB_ERROR, "could not open file %s: %s", path, strerror(errno)); 1045 | return NOB_INVALID_FD; 1046 | } 1047 | return result; 1048 | #else 1049 | SECURITY_ATTRIBUTES saAttr = {0}; 1050 | saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); 1051 | saAttr.bInheritHandle = TRUE; 1052 | 1053 | Nob_Fd result = CreateFile( 1054 | path, // name of the write 1055 | GENERIC_WRITE, // open for writing 1056 | 0, // do not share 1057 | &saAttr, // default security 1058 | CREATE_ALWAYS, // create always 1059 | FILE_ATTRIBUTE_NORMAL, // normal file 1060 | NULL // no attr. template 1061 | ); 1062 | 1063 | if (result == INVALID_HANDLE_VALUE) { 1064 | nob_log(NOB_ERROR, "Could not open file %s: %s", path, nob_win32_error_message(GetLastError())); 1065 | return NOB_INVALID_FD; 1066 | } 1067 | 1068 | return result; 1069 | #endif // _WIN32 1070 | } 1071 | 1072 | void nob_fd_close(Nob_Fd fd) 1073 | { 1074 | #ifdef _WIN32 1075 | CloseHandle(fd); 1076 | #else 1077 | close(fd); 1078 | #endif // _WIN32 1079 | } 1080 | 1081 | bool nob_procs_wait(Nob_Procs procs) 1082 | { 1083 | bool success = true; 1084 | for (size_t i = 0; i < procs.count; ++i) { 1085 | success = nob_proc_wait(procs.items[i]) && success; 1086 | } 1087 | return success; 1088 | } 1089 | 1090 | bool nob_procs_wait_and_reset(Nob_Procs *procs) 1091 | { 1092 | bool success = nob_procs_wait(*procs); 1093 | procs->count = 0; 1094 | return success; 1095 | } 1096 | 1097 | bool nob_proc_wait(Nob_Proc proc) 1098 | { 1099 | if (proc == NOB_INVALID_PROC) return false; 1100 | 1101 | #ifdef _WIN32 1102 | DWORD result = WaitForSingleObject( 1103 | proc, // HANDLE hHandle, 1104 | INFINITE // DWORD dwMilliseconds 1105 | ); 1106 | 1107 | if (result == WAIT_FAILED) { 1108 | nob_log(NOB_ERROR, "could not wait on child process: %s", nob_win32_error_message(GetLastError())); 1109 | return false; 1110 | } 1111 | 1112 | DWORD exit_status; 1113 | if (!GetExitCodeProcess(proc, &exit_status)) { 1114 | nob_log(NOB_ERROR, "could not get process exit code: %s", nob_win32_error_message(GetLastError())); 1115 | return false; 1116 | } 1117 | 1118 | if (exit_status != 0) { 1119 | nob_log(NOB_ERROR, "command exited with exit code %lu", exit_status); 1120 | return false; 1121 | } 1122 | 1123 | CloseHandle(proc); 1124 | 1125 | return true; 1126 | #else 1127 | for (;;) { 1128 | int wstatus = 0; 1129 | if (waitpid(proc, &wstatus, 0) < 0) { 1130 | nob_log(NOB_ERROR, "could not wait on command (pid %d): %s", proc, strerror(errno)); 1131 | return false; 1132 | } 1133 | 1134 | if (WIFEXITED(wstatus)) { 1135 | int exit_status = WEXITSTATUS(wstatus); 1136 | if (exit_status != 0) { 1137 | nob_log(NOB_ERROR, "command exited with exit code %d", exit_status); 1138 | return false; 1139 | } 1140 | 1141 | break; 1142 | } 1143 | 1144 | if (WIFSIGNALED(wstatus)) { 1145 | nob_log(NOB_ERROR, "command process was terminated by signal %d", WTERMSIG(wstatus)); 1146 | return false; 1147 | } 1148 | } 1149 | 1150 | return true; 1151 | #endif 1152 | } 1153 | 1154 | bool nob_procs_append_with_flush(Nob_Procs *procs, Nob_Proc proc, size_t max_procs_count) 1155 | { 1156 | nob_da_append(procs, proc); 1157 | 1158 | if (procs->count >= max_procs_count) { 1159 | if (!nob_procs_wait_and_reset(procs)) return false; 1160 | } 1161 | 1162 | return true; 1163 | } 1164 | 1165 | bool nob_cmd_run_sync_redirect(Nob_Cmd cmd, Nob_Cmd_Redirect redirect) 1166 | { 1167 | Nob_Proc p = nob_cmd_run_async_redirect(cmd, redirect); 1168 | if (p == NOB_INVALID_PROC) return false; 1169 | return nob_proc_wait(p); 1170 | } 1171 | 1172 | bool nob_cmd_run_sync(Nob_Cmd cmd) 1173 | { 1174 | Nob_Proc p = nob_cmd_run_async(cmd); 1175 | if (p == NOB_INVALID_PROC) return false; 1176 | return nob_proc_wait(p); 1177 | } 1178 | 1179 | bool nob_cmd_run_sync_and_reset(Nob_Cmd *cmd) 1180 | { 1181 | bool p = nob_cmd_run_sync(*cmd); 1182 | cmd->count = 0; 1183 | return p; 1184 | } 1185 | 1186 | bool nob_cmd_run_sync_redirect_and_reset(Nob_Cmd *cmd, Nob_Cmd_Redirect redirect) 1187 | { 1188 | bool p = nob_cmd_run_sync_redirect(*cmd, redirect); 1189 | cmd->count = 0; 1190 | if (redirect.fdin) { 1191 | nob_fd_close(*redirect.fdin); 1192 | *redirect.fdin = NOB_INVALID_FD; 1193 | } 1194 | if (redirect.fdout) { 1195 | nob_fd_close(*redirect.fdout); 1196 | *redirect.fdout = NOB_INVALID_FD; 1197 | } 1198 | if (redirect.fderr) { 1199 | nob_fd_close(*redirect.fderr); 1200 | *redirect.fderr = NOB_INVALID_FD; 1201 | } 1202 | return p; 1203 | } 1204 | 1205 | void nob_log(Nob_Log_Level level, const char *fmt, ...) 1206 | { 1207 | if (level < nob_minimal_log_level) return; 1208 | 1209 | switch (level) { 1210 | case NOB_INFO: 1211 | fprintf(stderr, "[INFO] "); 1212 | break; 1213 | case NOB_WARNING: 1214 | fprintf(stderr, "[WARNING] "); 1215 | break; 1216 | case NOB_ERROR: 1217 | fprintf(stderr, "[ERROR] "); 1218 | break; 1219 | case NOB_NO_LOGS: return; 1220 | default: 1221 | NOB_UNREACHABLE("nob_log"); 1222 | } 1223 | 1224 | va_list args; 1225 | va_start(args, fmt); 1226 | vfprintf(stderr, fmt, args); 1227 | va_end(args); 1228 | fprintf(stderr, "\n"); 1229 | } 1230 | 1231 | bool nob_read_entire_dir(const char *parent, Nob_File_Paths *children) 1232 | { 1233 | bool result = true; 1234 | DIR *dir = NULL; 1235 | 1236 | dir = opendir(parent); 1237 | if (dir == NULL) { 1238 | #ifdef _WIN32 1239 | nob_log(NOB_ERROR, "Could not open directory %s: %s", parent, nob_win32_error_message(GetLastError())); 1240 | #else 1241 | nob_log(NOB_ERROR, "Could not open directory %s: %s", parent, strerror(errno)); 1242 | #endif // _WIN32 1243 | nob_return_defer(false); 1244 | } 1245 | 1246 | errno = 0; 1247 | struct dirent *ent = readdir(dir); 1248 | while (ent != NULL) { 1249 | nob_da_append(children, nob_temp_strdup(ent->d_name)); 1250 | ent = readdir(dir); 1251 | } 1252 | 1253 | if (errno != 0) { 1254 | #ifdef _WIN32 1255 | nob_log(NOB_ERROR, "Could not read directory %s: %s", parent, nob_win32_error_message(GetLastError())); 1256 | #else 1257 | nob_log(NOB_ERROR, "Could not read directory %s: %s", parent, strerror(errno)); 1258 | #endif // _WIN32 1259 | nob_return_defer(false); 1260 | } 1261 | 1262 | defer: 1263 | if (dir) closedir(dir); 1264 | return result; 1265 | } 1266 | 1267 | bool nob_write_entire_file(const char *path, const void *data, size_t size) 1268 | { 1269 | bool result = true; 1270 | 1271 | FILE *f = fopen(path, "wb"); 1272 | if (f == NULL) { 1273 | nob_log(NOB_ERROR, "Could not open file %s for writing: %s\n", path, strerror(errno)); 1274 | nob_return_defer(false); 1275 | } 1276 | 1277 | // len 1278 | // v 1279 | // aaaaaaaaaa 1280 | // ^ 1281 | // data 1282 | 1283 | const char *buf = data; 1284 | while (size > 0) { 1285 | size_t n = fwrite(buf, 1, size, f); 1286 | if (ferror(f)) { 1287 | nob_log(NOB_ERROR, "Could not write into file %s: %s\n", path, strerror(errno)); 1288 | nob_return_defer(false); 1289 | } 1290 | size -= n; 1291 | buf += n; 1292 | } 1293 | 1294 | defer: 1295 | if (f) fclose(f); 1296 | return result; 1297 | } 1298 | 1299 | Nob_File_Type nob_get_file_type(const char *path) 1300 | { 1301 | #ifdef _WIN32 1302 | DWORD attr = GetFileAttributesA(path); 1303 | if (attr == INVALID_FILE_ATTRIBUTES) { 1304 | nob_log(NOB_ERROR, "Could not get file attributes of %s: %s", path, nob_win32_error_message(GetLastError())); 1305 | return -1; 1306 | } 1307 | 1308 | if (attr & FILE_ATTRIBUTE_DIRECTORY) return NOB_FILE_DIRECTORY; 1309 | // TODO: detect symlinks on Windows (whatever that means on Windows anyway) 1310 | return NOB_FILE_REGULAR; 1311 | #else // _WIN32 1312 | struct stat statbuf; 1313 | if (stat(path, &statbuf) < 0) { 1314 | nob_log(NOB_ERROR, "Could not get stat of %s: %s", path, strerror(errno)); 1315 | return -1; 1316 | } 1317 | 1318 | if (S_ISREG(statbuf.st_mode)) return NOB_FILE_REGULAR; 1319 | if (S_ISDIR(statbuf.st_mode)) return NOB_FILE_DIRECTORY; 1320 | if (S_ISLNK(statbuf.st_mode)) return NOB_FILE_SYMLINK; 1321 | return NOB_FILE_OTHER; 1322 | #endif // _WIN32 1323 | } 1324 | 1325 | bool nob_delete_file(const char *path) 1326 | { 1327 | nob_log(NOB_INFO, "deleting %s", path); 1328 | #ifdef _WIN32 1329 | if (!DeleteFileA(path)) { 1330 | nob_log(NOB_ERROR, "Could not delete file %s: %s", path, nob_win32_error_message(GetLastError())); 1331 | return false; 1332 | } 1333 | return true; 1334 | #else 1335 | if (remove(path) < 0) { 1336 | nob_log(NOB_ERROR, "Could not delete file %s: %s", path, strerror(errno)); 1337 | return false; 1338 | } 1339 | return true; 1340 | #endif // _WIN32 1341 | } 1342 | 1343 | bool nob_copy_directory_recursively(const char *src_path, const char *dst_path) 1344 | { 1345 | bool result = true; 1346 | Nob_File_Paths children = {0}; 1347 | Nob_String_Builder src_sb = {0}; 1348 | Nob_String_Builder dst_sb = {0}; 1349 | size_t temp_checkpoint = nob_temp_save(); 1350 | 1351 | Nob_File_Type type = nob_get_file_type(src_path); 1352 | if (type < 0) return false; 1353 | 1354 | switch (type) { 1355 | case NOB_FILE_DIRECTORY: { 1356 | if (!nob_mkdir_if_not_exists(dst_path)) nob_return_defer(false); 1357 | if (!nob_read_entire_dir(src_path, &children)) nob_return_defer(false); 1358 | 1359 | for (size_t i = 0; i < children.count; ++i) { 1360 | if (strcmp(children.items[i], ".") == 0) continue; 1361 | if (strcmp(children.items[i], "..") == 0) continue; 1362 | 1363 | src_sb.count = 0; 1364 | nob_sb_append_cstr(&src_sb, src_path); 1365 | nob_sb_append_cstr(&src_sb, "/"); 1366 | nob_sb_append_cstr(&src_sb, children.items[i]); 1367 | nob_sb_append_null(&src_sb); 1368 | 1369 | dst_sb.count = 0; 1370 | nob_sb_append_cstr(&dst_sb, dst_path); 1371 | nob_sb_append_cstr(&dst_sb, "/"); 1372 | nob_sb_append_cstr(&dst_sb, children.items[i]); 1373 | nob_sb_append_null(&dst_sb); 1374 | 1375 | if (!nob_copy_directory_recursively(src_sb.items, dst_sb.items)) { 1376 | nob_return_defer(false); 1377 | } 1378 | } 1379 | } break; 1380 | 1381 | case NOB_FILE_REGULAR: { 1382 | if (!nob_copy_file(src_path, dst_path)) { 1383 | nob_return_defer(false); 1384 | } 1385 | } break; 1386 | 1387 | case NOB_FILE_SYMLINK: { 1388 | nob_log(NOB_WARNING, "TODO: Copying symlinks is not supported yet"); 1389 | } break; 1390 | 1391 | case NOB_FILE_OTHER: { 1392 | nob_log(NOB_ERROR, "Unsupported type of file %s", src_path); 1393 | nob_return_defer(false); 1394 | } break; 1395 | 1396 | default: NOB_UNREACHABLE("nob_copy_directory_recursively"); 1397 | } 1398 | 1399 | defer: 1400 | nob_temp_rewind(temp_checkpoint); 1401 | nob_da_free(src_sb); 1402 | nob_da_free(dst_sb); 1403 | nob_da_free(children); 1404 | return result; 1405 | } 1406 | 1407 | char *nob_temp_strdup(const char *cstr) 1408 | { 1409 | size_t n = strlen(cstr); 1410 | char *result = nob_temp_alloc(n + 1); 1411 | NOB_ASSERT(result != NULL && "Increase NOB_TEMP_CAPACITY"); 1412 | memcpy(result, cstr, n); 1413 | result[n] = '\0'; 1414 | return result; 1415 | } 1416 | 1417 | void *nob_temp_alloc(size_t size) 1418 | { 1419 | if (nob_temp_size + size > NOB_TEMP_CAPACITY) return NULL; 1420 | void *result = &nob_temp[nob_temp_size]; 1421 | nob_temp_size += size; 1422 | return result; 1423 | } 1424 | 1425 | char *nob_temp_sprintf(const char *format, ...) 1426 | { 1427 | va_list args; 1428 | va_start(args, format); 1429 | int n = vsnprintf(NULL, 0, format, args); 1430 | va_end(args); 1431 | 1432 | NOB_ASSERT(n >= 0); 1433 | char *result = nob_temp_alloc(n + 1); 1434 | NOB_ASSERT(result != NULL && "Extend the size of the temporary allocator"); 1435 | // TODO: use proper arenas for the temporary allocator; 1436 | va_start(args, format); 1437 | vsnprintf(result, n + 1, format, args); 1438 | va_end(args); 1439 | 1440 | return result; 1441 | } 1442 | 1443 | void nob_temp_reset(void) 1444 | { 1445 | nob_temp_size = 0; 1446 | } 1447 | 1448 | size_t nob_temp_save(void) 1449 | { 1450 | return nob_temp_size; 1451 | } 1452 | 1453 | void nob_temp_rewind(size_t checkpoint) 1454 | { 1455 | nob_temp_size = checkpoint; 1456 | } 1457 | 1458 | const char *nob_temp_sv_to_cstr(Nob_String_View sv) 1459 | { 1460 | char *result = nob_temp_alloc(sv.count + 1); 1461 | NOB_ASSERT(result != NULL && "Extend the size of the temporary allocator"); 1462 | memcpy(result, sv.data, sv.count); 1463 | result[sv.count] = '\0'; 1464 | return result; 1465 | } 1466 | 1467 | int nob_needs_rebuild(const char *output_path, const char **input_paths, size_t input_paths_count) 1468 | { 1469 | #ifdef _WIN32 1470 | BOOL bSuccess; 1471 | 1472 | HANDLE output_path_fd = CreateFile(output_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); 1473 | if (output_path_fd == INVALID_HANDLE_VALUE) { 1474 | // NOTE: if output does not exist it 100% must be rebuilt 1475 | if (GetLastError() == ERROR_FILE_NOT_FOUND) return 1; 1476 | nob_log(NOB_ERROR, "Could not open file %s: %s", output_path, nob_win32_error_message(GetLastError())); 1477 | return -1; 1478 | } 1479 | FILETIME output_path_time; 1480 | bSuccess = GetFileTime(output_path_fd, NULL, NULL, &output_path_time); 1481 | CloseHandle(output_path_fd); 1482 | if (!bSuccess) { 1483 | nob_log(NOB_ERROR, "Could not get time of %s: %s", output_path, nob_win32_error_message(GetLastError())); 1484 | return -1; 1485 | } 1486 | 1487 | for (size_t i = 0; i < input_paths_count; ++i) { 1488 | const char *input_path = input_paths[i]; 1489 | HANDLE input_path_fd = CreateFile(input_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); 1490 | if (input_path_fd == INVALID_HANDLE_VALUE) { 1491 | // NOTE: non-existing input is an error cause it is needed for building in the first place 1492 | nob_log(NOB_ERROR, "Could not open file %s: %s", input_path, nob_win32_error_message(GetLastError())); 1493 | return -1; 1494 | } 1495 | FILETIME input_path_time; 1496 | bSuccess = GetFileTime(input_path_fd, NULL, NULL, &input_path_time); 1497 | CloseHandle(input_path_fd); 1498 | if (!bSuccess) { 1499 | nob_log(NOB_ERROR, "Could not get time of %s: %s", input_path, nob_win32_error_message(GetLastError())); 1500 | return -1; 1501 | } 1502 | 1503 | // NOTE: if even a single input_path is fresher than output_path that's 100% rebuild 1504 | if (CompareFileTime(&input_path_time, &output_path_time) == 1) return 1; 1505 | } 1506 | 1507 | return 0; 1508 | #else 1509 | struct stat statbuf = {0}; 1510 | 1511 | if (stat(output_path, &statbuf) < 0) { 1512 | // NOTE: if output does not exist it 100% must be rebuilt 1513 | if (errno == ENOENT) return 1; 1514 | nob_log(NOB_ERROR, "could not stat %s: %s", output_path, strerror(errno)); 1515 | return -1; 1516 | } 1517 | int output_path_time = statbuf.st_mtime; 1518 | 1519 | for (size_t i = 0; i < input_paths_count; ++i) { 1520 | const char *input_path = input_paths[i]; 1521 | if (stat(input_path, &statbuf) < 0) { 1522 | // NOTE: non-existing input is an error cause it is needed for building in the first place 1523 | nob_log(NOB_ERROR, "could not stat %s: %s", input_path, strerror(errno)); 1524 | return -1; 1525 | } 1526 | int input_path_time = statbuf.st_mtime; 1527 | // NOTE: if even a single input_path is fresher than output_path that's 100% rebuild 1528 | if (input_path_time > output_path_time) return 1; 1529 | } 1530 | 1531 | return 0; 1532 | #endif 1533 | } 1534 | 1535 | int nob_needs_rebuild1(const char *output_path, const char *input_path) 1536 | { 1537 | return nob_needs_rebuild(output_path, &input_path, 1); 1538 | } 1539 | 1540 | const char *nob_path_name(const char *path) 1541 | { 1542 | #ifdef _WIN32 1543 | const char *p1 = strrchr(path, '/'); 1544 | const char *p2 = strrchr(path, '\\'); 1545 | const char *p = (p1 > p2)? p1 : p2; // NULL is ignored if the other search is successful 1546 | return p ? p + 1 : path; 1547 | #else 1548 | const char *p = strrchr(path, '/'); 1549 | return p ? p + 1 : path; 1550 | #endif // _WIN32 1551 | } 1552 | 1553 | bool nob_rename(const char *old_path, const char *new_path) 1554 | { 1555 | nob_log(NOB_INFO, "renaming %s -> %s", old_path, new_path); 1556 | #ifdef _WIN32 1557 | if (!MoveFileEx(old_path, new_path, MOVEFILE_REPLACE_EXISTING)) { 1558 | nob_log(NOB_ERROR, "could not rename %s to %s: %s", old_path, new_path, nob_win32_error_message(GetLastError())); 1559 | return false; 1560 | } 1561 | #else 1562 | if (rename(old_path, new_path) < 0) { 1563 | nob_log(NOB_ERROR, "could not rename %s to %s: %s", old_path, new_path, strerror(errno)); 1564 | return false; 1565 | } 1566 | #endif // _WIN32 1567 | return true; 1568 | } 1569 | 1570 | bool nob_read_entire_file(const char *path, Nob_String_Builder *sb) 1571 | { 1572 | bool result = true; 1573 | 1574 | FILE *f = fopen(path, "rb"); 1575 | if (f == NULL) nob_return_defer(false); 1576 | if (fseek(f, 0, SEEK_END) < 0) nob_return_defer(false); 1577 | #ifndef _WIN32 1578 | long m = ftell(f); 1579 | #else 1580 | long long m = _ftelli64(f); 1581 | #endif 1582 | if (m < 0) nob_return_defer(false); 1583 | if (fseek(f, 0, SEEK_SET) < 0) nob_return_defer(false); 1584 | 1585 | size_t new_count = sb->count + m; 1586 | if (new_count > sb->capacity) { 1587 | sb->items = NOB_REALLOC(sb->items, new_count); 1588 | NOB_ASSERT(sb->items != NULL && "Buy more RAM lool!!"); 1589 | sb->capacity = new_count; 1590 | } 1591 | 1592 | fread(sb->items + sb->count, m, 1, f); 1593 | if (ferror(f)) { 1594 | // TODO: Afaik, ferror does not set errno. So the error reporting in defer is not correct in this case. 1595 | nob_return_defer(false); 1596 | } 1597 | sb->count = new_count; 1598 | 1599 | defer: 1600 | if (!result) nob_log(NOB_ERROR, "Could not read file %s: %s", path, strerror(errno)); 1601 | if (f) fclose(f); 1602 | return result; 1603 | } 1604 | 1605 | int nob_sb_appendf(Nob_String_Builder *sb, const char *fmt, ...) 1606 | { 1607 | va_list args; 1608 | 1609 | va_start(args, fmt); 1610 | int n = vsnprintf(NULL, 0, fmt, args); 1611 | va_end(args); 1612 | 1613 | // NOTE: the new_capacity needs to be +1 because of the null terminator. 1614 | // However, further below we increase sb->count by n, not n + 1. 1615 | // This is because we don't want the sb to include the null terminator. The user can always sb_append_null() if they want it 1616 | nob_da_reserve(sb, sb->count + n + 1); 1617 | char *dest = sb->items + sb->count; 1618 | va_start(args, fmt); 1619 | vsnprintf(dest, n+1, fmt, args); 1620 | va_end(args); 1621 | 1622 | sb->count += n; 1623 | 1624 | return n; 1625 | } 1626 | 1627 | Nob_String_View nob_sv_chop_by_delim(Nob_String_View *sv, char delim) 1628 | { 1629 | size_t i = 0; 1630 | while (i < sv->count && sv->data[i] != delim) { 1631 | i += 1; 1632 | } 1633 | 1634 | Nob_String_View result = nob_sv_from_parts(sv->data, i); 1635 | 1636 | if (i < sv->count) { 1637 | sv->count -= i + 1; 1638 | sv->data += i + 1; 1639 | } else { 1640 | sv->count -= i; 1641 | sv->data += i; 1642 | } 1643 | 1644 | return result; 1645 | } 1646 | 1647 | Nob_String_View nob_sv_chop_left(Nob_String_View *sv, size_t n) 1648 | { 1649 | if (n > sv->count) { 1650 | n = sv->count; 1651 | } 1652 | 1653 | Nob_String_View result = nob_sv_from_parts(sv->data, n); 1654 | 1655 | sv->data += n; 1656 | sv->count -= n; 1657 | 1658 | return result; 1659 | } 1660 | 1661 | Nob_String_View nob_sv_from_parts(const char *data, size_t count) 1662 | { 1663 | Nob_String_View sv; 1664 | sv.count = count; 1665 | sv.data = data; 1666 | return sv; 1667 | } 1668 | 1669 | Nob_String_View nob_sv_trim_left(Nob_String_View sv) 1670 | { 1671 | size_t i = 0; 1672 | while (i < sv.count && isspace(sv.data[i])) { 1673 | i += 1; 1674 | } 1675 | 1676 | return nob_sv_from_parts(sv.data + i, sv.count - i); 1677 | } 1678 | 1679 | Nob_String_View nob_sv_trim_right(Nob_String_View sv) 1680 | { 1681 | size_t i = 0; 1682 | while (i < sv.count && isspace(sv.data[sv.count - 1 - i])) { 1683 | i += 1; 1684 | } 1685 | 1686 | return nob_sv_from_parts(sv.data, sv.count - i); 1687 | } 1688 | 1689 | Nob_String_View nob_sv_trim(Nob_String_View sv) 1690 | { 1691 | return nob_sv_trim_right(nob_sv_trim_left(sv)); 1692 | } 1693 | 1694 | Nob_String_View nob_sv_from_cstr(const char *cstr) 1695 | { 1696 | return nob_sv_from_parts(cstr, strlen(cstr)); 1697 | } 1698 | 1699 | bool nob_sv_eq(Nob_String_View a, Nob_String_View b) 1700 | { 1701 | if (a.count != b.count) { 1702 | return false; 1703 | } else { 1704 | return memcmp(a.data, b.data, a.count) == 0; 1705 | } 1706 | } 1707 | 1708 | bool nob_sv_end_with(Nob_String_View sv, const char *cstr) 1709 | { 1710 | size_t cstr_count = strlen(cstr); 1711 | if (sv.count >= cstr_count) { 1712 | size_t ending_start = sv.count - cstr_count; 1713 | Nob_String_View sv_ending = nob_sv_from_parts(sv.data + ending_start, cstr_count); 1714 | return nob_sv_eq(sv_ending, nob_sv_from_cstr(cstr)); 1715 | } 1716 | return false; 1717 | } 1718 | 1719 | 1720 | bool nob_sv_starts_with(Nob_String_View sv, Nob_String_View expected_prefix) 1721 | { 1722 | if (expected_prefix.count <= sv.count) { 1723 | Nob_String_View actual_prefix = nob_sv_from_parts(sv.data, expected_prefix.count); 1724 | return nob_sv_eq(expected_prefix, actual_prefix); 1725 | } 1726 | 1727 | return false; 1728 | } 1729 | 1730 | // RETURNS: 1731 | // 0 - file does not exists 1732 | // 1 - file exists 1733 | // -1 - error while checking if file exists. The error is logged 1734 | int nob_file_exists(const char *file_path) 1735 | { 1736 | #if _WIN32 1737 | // TODO: distinguish between "does not exists" and other errors 1738 | DWORD dwAttrib = GetFileAttributesA(file_path); 1739 | return dwAttrib != INVALID_FILE_ATTRIBUTES; 1740 | #else 1741 | struct stat statbuf; 1742 | if (stat(file_path, &statbuf) < 0) { 1743 | if (errno == ENOENT) return 0; 1744 | nob_log(NOB_ERROR, "Could not check if file %s exists: %s", file_path, strerror(errno)); 1745 | return -1; 1746 | } 1747 | return 1; 1748 | #endif 1749 | } 1750 | 1751 | const char *nob_get_current_dir_temp(void) 1752 | { 1753 | #ifdef _WIN32 1754 | DWORD nBufferLength = GetCurrentDirectory(0, NULL); 1755 | if (nBufferLength == 0) { 1756 | nob_log(NOB_ERROR, "could not get current directory: %s", nob_win32_error_message(GetLastError())); 1757 | return NULL; 1758 | } 1759 | 1760 | char *buffer = (char*) nob_temp_alloc(nBufferLength); 1761 | if (GetCurrentDirectory(nBufferLength, buffer) == 0) { 1762 | nob_log(NOB_ERROR, "could not get current directory: %s", nob_win32_error_message(GetLastError())); 1763 | return NULL; 1764 | } 1765 | 1766 | return buffer; 1767 | #else 1768 | char *buffer = (char*) nob_temp_alloc(PATH_MAX); 1769 | if (getcwd(buffer, PATH_MAX) == NULL) { 1770 | nob_log(NOB_ERROR, "could not get current directory: %s", strerror(errno)); 1771 | return NULL; 1772 | } 1773 | 1774 | return buffer; 1775 | #endif // _WIN32 1776 | } 1777 | 1778 | bool nob_set_current_dir(const char *path) 1779 | { 1780 | #ifdef _WIN32 1781 | if (!SetCurrentDirectory(path)) { 1782 | nob_log(NOB_ERROR, "could not set current directory to %s: %s", path, nob_win32_error_message(GetLastError())); 1783 | return false; 1784 | } 1785 | return true; 1786 | #else 1787 | if (chdir(path) < 0) { 1788 | nob_log(NOB_ERROR, "could not set current directory to %s: %s", path, strerror(errno)); 1789 | return false; 1790 | } 1791 | return true; 1792 | #endif // _WIN32 1793 | } 1794 | 1795 | // minirent.h SOURCE BEGIN //////////////////////////////////////// 1796 | #ifdef _WIN32 1797 | struct DIR 1798 | { 1799 | HANDLE hFind; 1800 | WIN32_FIND_DATA data; 1801 | struct dirent *dirent; 1802 | }; 1803 | 1804 | DIR *opendir(const char *dirpath) 1805 | { 1806 | NOB_ASSERT(dirpath); 1807 | 1808 | char buffer[MAX_PATH]; 1809 | snprintf(buffer, MAX_PATH, "%s\\*", dirpath); 1810 | 1811 | DIR *dir = (DIR*)NOB_REALLOC(NULL, sizeof(DIR)); 1812 | memset(dir, 0, sizeof(DIR)); 1813 | 1814 | dir->hFind = FindFirstFile(buffer, &dir->data); 1815 | if (dir->hFind == INVALID_HANDLE_VALUE) { 1816 | // TODO: opendir should set errno accordingly on FindFirstFile fail 1817 | // https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror 1818 | errno = ENOSYS; 1819 | goto fail; 1820 | } 1821 | 1822 | return dir; 1823 | 1824 | fail: 1825 | if (dir) { 1826 | NOB_FREE(dir); 1827 | } 1828 | 1829 | return NULL; 1830 | } 1831 | 1832 | struct dirent *readdir(DIR *dirp) 1833 | { 1834 | NOB_ASSERT(dirp); 1835 | 1836 | if (dirp->dirent == NULL) { 1837 | dirp->dirent = (struct dirent*)NOB_REALLOC(NULL, sizeof(struct dirent)); 1838 | memset(dirp->dirent, 0, sizeof(struct dirent)); 1839 | } else { 1840 | if(!FindNextFile(dirp->hFind, &dirp->data)) { 1841 | if (GetLastError() != ERROR_NO_MORE_FILES) { 1842 | // TODO: readdir should set errno accordingly on FindNextFile fail 1843 | // https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror 1844 | errno = ENOSYS; 1845 | } 1846 | 1847 | return NULL; 1848 | } 1849 | } 1850 | 1851 | memset(dirp->dirent->d_name, 0, sizeof(dirp->dirent->d_name)); 1852 | 1853 | strncpy( 1854 | dirp->dirent->d_name, 1855 | dirp->data.cFileName, 1856 | sizeof(dirp->dirent->d_name) - 1); 1857 | 1858 | return dirp->dirent; 1859 | } 1860 | 1861 | int closedir(DIR *dirp) 1862 | { 1863 | NOB_ASSERT(dirp); 1864 | 1865 | if(!FindClose(dirp->hFind)) { 1866 | // TODO: closedir should set errno accordingly on FindClose fail 1867 | // https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror 1868 | errno = ENOSYS; 1869 | return -1; 1870 | } 1871 | 1872 | if (dirp->dirent) { 1873 | NOB_FREE(dirp->dirent); 1874 | } 1875 | NOB_FREE(dirp); 1876 | 1877 | return 0; 1878 | } 1879 | #endif // _WIN32 1880 | // minirent.h SOURCE END //////////////////////////////////////// 1881 | 1882 | #endif // NOB_IMPLEMENTATION 1883 | 1884 | #ifndef NOB_STRIP_PREFIX_GUARD_ 1885 | #define NOB_STRIP_PREFIX_GUARD_ 1886 | // NOTE: The name stripping should be part of the header so it's not accidentally included 1887 | // several times. At the same time, it should be at the end of the file so to not create any 1888 | // potential conflicts in the NOB_IMPLEMENTATION. The header obviously cannot be at the end 1889 | // of the file because NOB_IMPLEMENTATION needs the forward declarations from there. So the 1890 | // solution is to split the header into two parts where the name stripping part is at the 1891 | // end of the file after the NOB_IMPLEMENTATION. 1892 | #ifdef NOB_STRIP_PREFIX 1893 | #define TODO NOB_TODO 1894 | #define UNREACHABLE NOB_UNREACHABLE 1895 | #define UNUSED NOB_UNUSED 1896 | #define ARRAY_LEN NOB_ARRAY_LEN 1897 | #define ARRAY_GET NOB_ARRAY_GET 1898 | #define INFO NOB_INFO 1899 | #define WARNING NOB_WARNING 1900 | #define ERROR NOB_ERROR 1901 | #define NO_LOGS NOB_NO_LOGS 1902 | #define Log_Level Nob_Log_Level 1903 | #define minimal_log_level nob_minimal_log_level 1904 | // NOTE: Name log is already defined in math.h and historically always was the natural logarithmic function. 1905 | // So there should be no reason to strip the `nob_` prefix in this specific case. 1906 | // #define log nob_log 1907 | #define shift nob_shift 1908 | #define shift_args nob_shift_args 1909 | #define File_Paths Nob_File_Paths 1910 | #define FILE_REGULAR NOB_FILE_REGULAR 1911 | #define FILE_DIRECTORY NOB_FILE_DIRECTORY 1912 | #define FILE_SYMLINK NOB_FILE_SYMLINK 1913 | #define FILE_OTHER NOB_FILE_OTHER 1914 | #define File_Type Nob_File_Type 1915 | #define mkdir_if_not_exists nob_mkdir_if_not_exists 1916 | #define copy_file nob_copy_file 1917 | #define copy_directory_recursively nob_copy_directory_recursively 1918 | #define read_entire_dir nob_read_entire_dir 1919 | #define write_entire_file nob_write_entire_file 1920 | #define get_file_type nob_get_file_type 1921 | #define delete_file nob_delete_file 1922 | #define return_defer nob_return_defer 1923 | #define da_append nob_da_append 1924 | #define da_free nob_da_free 1925 | #define da_append_many nob_da_append_many 1926 | #define da_resize nob_da_resize 1927 | #define da_reserve nob_da_reserve 1928 | #define da_last nob_da_last 1929 | #define da_remove_unordered nob_da_remove_unordered 1930 | #define da_foreach nob_da_foreach 1931 | #define String_Builder Nob_String_Builder 1932 | #define read_entire_file nob_read_entire_file 1933 | #define sb_appendf nob_sb_appendf 1934 | #define sb_append_buf nob_sb_append_buf 1935 | #define sb_append_cstr nob_sb_append_cstr 1936 | #define sb_append_null nob_sb_append_null 1937 | #define sb_free nob_sb_free 1938 | #define Proc Nob_Proc 1939 | #define INVALID_PROC NOB_INVALID_PROC 1940 | #define Fd Nob_Fd 1941 | #define INVALID_FD NOB_INVALID_FD 1942 | #define fd_open_for_read nob_fd_open_for_read 1943 | #define fd_open_for_write nob_fd_open_for_write 1944 | #define fd_close nob_fd_close 1945 | #define Procs Nob_Procs 1946 | #define proc_wait nob_proc_wait 1947 | #define procs_wait nob_procs_wait 1948 | #define procs_wait_and_reset nob_procs_wait_and_reset 1949 | #define procs_append_with_flush nob_procs_append_with_flush 1950 | #define Cmd Nob_Cmd 1951 | #define Cmd_Redirect Nob_Cmd_Redirect 1952 | #define cmd_render nob_cmd_render 1953 | #define cmd_append nob_cmd_append 1954 | #define cmd_extend nob_cmd_extend 1955 | #define cmd_free nob_cmd_free 1956 | #define cmd_run_async nob_cmd_run_async 1957 | #define cmd_run_async_and_reset nob_cmd_run_async_and_reset 1958 | #define cmd_run_async_redirect nob_cmd_run_async_redirect 1959 | #define cmd_run_async_redirect_and_reset nob_cmd_run_async_redirect_and_reset 1960 | #define cmd_run_sync nob_cmd_run_sync 1961 | #define cmd_run_sync_and_reset nob_cmd_run_sync_and_reset 1962 | #define cmd_run_sync_redirect nob_cmd_run_sync_redirect 1963 | #define cmd_run_sync_redirect_and_reset nob_cmd_run_sync_redirect_and_reset 1964 | #define temp_strdup nob_temp_strdup 1965 | #define temp_alloc nob_temp_alloc 1966 | #define temp_sprintf nob_temp_sprintf 1967 | #define temp_reset nob_temp_reset 1968 | #define temp_save nob_temp_save 1969 | #define temp_rewind nob_temp_rewind 1970 | #define path_name nob_path_name 1971 | #define rename nob_rename 1972 | #define needs_rebuild nob_needs_rebuild 1973 | #define needs_rebuild1 nob_needs_rebuild1 1974 | #define file_exists nob_file_exists 1975 | #define get_current_dir_temp nob_get_current_dir_temp 1976 | #define set_current_dir nob_set_current_dir 1977 | #define String_View Nob_String_View 1978 | #define temp_sv_to_cstr nob_temp_sv_to_cstr 1979 | #define sv_chop_by_delim nob_sv_chop_by_delim 1980 | #define sv_chop_left nob_sv_chop_left 1981 | #define sv_trim nob_sv_trim 1982 | #define sv_trim_left nob_sv_trim_left 1983 | #define sv_trim_right nob_sv_trim_right 1984 | #define sv_eq nob_sv_eq 1985 | #define sv_starts_with nob_sv_starts_with 1986 | #define sv_end_with nob_sv_end_with 1987 | #define sv_from_cstr nob_sv_from_cstr 1988 | #define sv_from_parts nob_sv_from_parts 1989 | #define sb_to_sv nob_sb_to_sv 1990 | #define win32_error_message nob_win32_error_message 1991 | #endif // NOB_STRIP_PREFIX 1992 | #endif // NOB_STRIP_PREFIX_GUARD_ 1993 | 1994 | /* 1995 | Revision history: 1996 | 1997 | 1.20.2 (2025-04-24) Report the program name that failed to start up in nob_cmd_run_async_redirect() (By @rexim) 1998 | 1.20.1 (2025-04-16) Use vsnprintf() in nob_sb_appendf() instead of vsprintf() (By @LainLayer) 1999 | 1.20.0 (2025-04-16) Introduce nob_cc(), nob_cc_flags(), nob_cc_inputs(), nob_cc_output() macros (By @rexim) 2000 | 1.19.0 (2025-03-25) Add nob_procs_append_with_flush() (By @rexim and @anion155) 2001 | 1.18.0 (2025-03-24) Add nob_da_foreach() (By @rexim) 2002 | Allow file sizes greater than 2GB to be read on windows (By @satchelfrost and @KillerxDBr) 2003 | Fix nob_fd_open_for_write behaviour on windows so it truncates the opened files (By @twixuss) 2004 | 1.17.0 (2025-03-16) Factor out nob_da_reserve() (By @rexim) 2005 | Add nob_sb_appendf() (By @angelcaru) 2006 | 1.16.1 (2025-03-16) Make nob_da_resize() exponentially grow capacity similar to no_da_append_many() 2007 | 1.16.0 (2025-03-16) Introduce NOB_PRINTF_FORMAT 2008 | 1.15.1 (2025-03-16) Make nob.h compilable in gcc/clang with -std=c99 on POSIX. This includes: 2009 | not using strsignal() 2010 | using S_IS* stat macros instead of S_IF* flags 2011 | 1.15.0 (2025-03-03) Add nob_sv_chop_left() 2012 | 1.14.1 (2025-03-02) Add NOB_EXPERIMENTAL_DELETE_OLD flag that enables deletion of nob.old in Go Rebuild Urself™ Technology 2013 | 1.14.0 (2025-02-17) Add nob_da_last() 2014 | Add nob_da_remove_unordered() 2015 | 1.13.1 (2025-02-17) Fix segfault in nob_delete_file() (By @SileNce5k) 2016 | 1.13.0 (2025-02-11) Add nob_da_resize() (By @satchelfrost) 2017 | 1.12.0 (2025-02-04) Add nob_delete_file() 2018 | Add nob_sv_start_with() 2019 | 1.11.0 (2025-02-04) Add NOB_GO_REBUILD_URSELF_PLUS() (By @rexim) 2020 | 1.10.0 (2025-02-04) Make NOB_ASSERT, NOB_REALLOC, and NOB_FREE redefinable (By @OleksiiBulba) 2021 | 1.9.1 (2025-02-04) Fix signature of nob_get_current_dir_temp() (By @julianstoerig) 2022 | 1.9.0 (2024-11-06) Add Nob_Cmd_Redirect mechanism (By @rexim) 2023 | Add nob_path_name() (By @0dminnimda) 2024 | 1.8.0 (2024-11-03) Add nob_cmd_extend() (By @0dminnimda) 2025 | 1.7.0 (2024-11-03) Add nob_win32_error_message and NOB_WIN32_ERR_MSG_SIZE (By @KillerxDBr) 2026 | 1.6.0 (2024-10-27) Add nob_cmd_run_sync_and_reset() 2027 | Add nob_sb_to_sv() 2028 | Add nob_procs_wait_and_reset() 2029 | 1.5.1 (2024-10-25) Include limits.h for Linux musl libc (by @pgalkin) 2030 | 1.5.0 (2024-10-23) Add nob_get_current_dir_temp() 2031 | Add nob_set_current_dir() 2032 | 1.4.0 (2024-10-21) Fix UX issues with NOB_GO_REBUILD_URSELF on Windows when you call nob without the .exe extension (By @pgalkin) 2033 | Add nob_sv_end_with (By @pgalkin) 2034 | 1.3.2 (2024-10-21) Fix unreachable error in nob_log on passing NOB_NO_LOGS 2035 | 1.3.1 (2024-10-21) Fix redeclaration error for minimal_log_level (By @KillerxDBr) 2036 | 1.3.0 (2024-10-17) Add NOB_UNREACHABLE 2037 | 1.2.2 (2024-10-16) Fix compilation of nob_cmd_run_sync_and_reset on Windows (By @KillerxDBr) 2038 | 1.2.1 (2024-10-16) Add a separate include guard for NOB_STRIP_PREFIX. 2039 | 1.2.0 (2024-10-15) Make NOB_DA_INIT_CAP redefinable 2040 | Add NOB_STRIP_PREFIX which strips off nob_* prefix from all the user facing names 2041 | Add NOB_UNUSED macro 2042 | Add NOB_TODO macro 2043 | Add nob_sv_trim_left and nob_sv_trim_right declarations to the header part 2044 | 1.1.1 (2024-10-15) Remove forward declaration for is_path1_modified_after_path2 2045 | 1.1.0 (2024-10-15) nob_minimal_log_level 2046 | nob_cmd_run_sync_and_reset 2047 | 1.0.0 (2024-10-15) first release based on https://github.com/tsoding/musializer/blob/4ac7cce9874bc19e02d8c160c8c6229de8919401/nob.h 2048 | */ 2049 | 2050 | /* 2051 | Version Conventions: 2052 | 2053 | We are following https://semver.org/ so the version has a format MAJOR.MINOR.PATCH: 2054 | - Modifying comments does not update the version. 2055 | - PATCH is incremented in case of a bug fix or refactoring without touching the API. 2056 | - MINOR is incremented when new functions and/or types are added in a way that does 2057 | not break any existing user code. We want to do this in the majority of the situation. 2058 | If we want to delete a certain function or type in favor of another one we should 2059 | just add the new function/type and deprecate the old one in a backward compatible way 2060 | and let them co-exist for a while. 2061 | - MAJOR update should be just a periodic cleanup of the deprecated functions and types 2062 | without really modifying any existing functionality. 2063 | 2064 | Naming Conventions: 2065 | 2066 | - All the user facing names should be prefixed with `nob_` or `NOB_` depending on the case. 2067 | - The prefixes of non-redefinable names should be strippable with NOB_STRIP_PREFIX (unless 2068 | explicitly stated otherwise like in case of nob_log). 2069 | - Internal functions should be prefixed with `nob__` (double underscore). 2070 | */ 2071 | 2072 | /* 2073 | ------------------------------------------------------------------------------ 2074 | This software is available under 2 licenses -- choose whichever you prefer. 2075 | ------------------------------------------------------------------------------ 2076 | ALTERNATIVE A - MIT License 2077 | Copyright (c) 2024 Alexey Kutepov 2078 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2079 | this software and associated documentation files (the "Software"), to deal in 2080 | the Software without restriction, including without limitation the rights to 2081 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 2082 | of the Software, and to permit persons to whom the Software is furnished to do 2083 | so, subject to the following conditions: 2084 | The above copyright notice and this permission notice shall be included in all 2085 | copies or substantial portions of the Software. 2086 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 2087 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 2088 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 2089 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 2090 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 2091 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 2092 | SOFTWARE. 2093 | ------------------------------------------------------------------------------ 2094 | ALTERNATIVE B - Public Domain (www.unlicense.org) 2095 | This is free and unencumbered software released into the public domain. 2096 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 2097 | software, either in source code form or as a compiled binary, for any purpose, 2098 | commercial or non-commercial, and by any means. 2099 | In jurisdictions that recognize copyright laws, the author or authors of this 2100 | software dedicate any and all copyright interest in the software to the public 2101 | domain. We make this dedication for the benefit of the public at large and to 2102 | the detriment of our heirs and successors. We intend this dedication to be an 2103 | overt act of relinquishment in perpetuity of all present and future rights to 2104 | this software under copyright law. 2105 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 2106 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 2107 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 2108 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 2109 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 2110 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 2111 | ------------------------------------------------------------------------------ 2112 | */ 2113 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tsoding/gatekeeper 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bwmarrin/discordgo v0.26.1 7 | github.com/lib/pq v1.10.7 8 | github.com/tsoding/smig v0.0.0-20221103133817-c10d61146815 9 | ) 10 | 11 | require ( 12 | github.com/gorilla/websocket v1.4.2 // indirect 13 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect 14 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE= 2 | github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 3 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 4 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 6 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 7 | github.com/tsoding/smig v0.0.0-20221103133817-c10d61146815 h1:8MO+TO3bGQH7ShvQxiNVfmdFC2WQtctJkVgM4br671c= 8 | github.com/tsoding/smig v0.0.0-20221103133817-c10d61146815/go.mod h1:qDHEhUKRupwGNUf1HF0pwQDhZeXF/013NwX/uRFqjlw= 9 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= 10 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 11 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 12 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 13 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 15 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 16 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 17 | -------------------------------------------------------------------------------- /internal/carrotson.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "log" 7 | "math" 8 | ) 9 | 10 | const ContextSize = 8 11 | 12 | type Path struct { 13 | context []rune 14 | follows rune 15 | } 16 | 17 | func splitMessageIntoPaths(message []rune) (branches []Path) { 18 | for i := -ContextSize; i+ContextSize < len(message); i += 1 { 19 | j := i 20 | if j < 0 { 21 | j = 0 22 | } 23 | branches = append(branches, Path{ 24 | context: message[j : i+ContextSize], 25 | follows: message[i+ContextSize], 26 | }) 27 | } 28 | return 29 | } 30 | 31 | type Branch struct { 32 | Context []rune 33 | Follows rune 34 | Frequency int64 35 | } 36 | 37 | var ( 38 | EmptyFollowsError = errors.New("Empty follows of a Carrotson branch") 39 | ) 40 | 41 | func QueryRandomBranchFromUnfinishedContext(db *sql.DB, context []rune) (*Branch, error) { 42 | row := db.QueryRow("SELECT context, follows, frequency FROM Carrotson_Branches WHERE starts_with(context, $1) AND frequency > 0 ORDER BY random() LIMIT 1", string(context)) 43 | var fullContext string 44 | var follows string 45 | var frequency int64 46 | err := row.Scan(&fullContext, &follows, &frequency) 47 | if err == sql.ErrNoRows { 48 | return nil, nil 49 | } 50 | if err != nil { 51 | return nil, err 52 | } 53 | if len(follows) == 0 { 54 | return nil, EmptyFollowsError 55 | } 56 | return &Branch{ 57 | Context: []rune(fullContext), 58 | Follows: []rune(follows)[0], 59 | Frequency: frequency, 60 | }, nil 61 | } 62 | 63 | func QueryRandomBranchFromContext(db *sql.DB, context []rune, t float64) (*Branch, error) { 64 | row := db.QueryRow("select follows, frequency from (select * from carrotson_branches where context = $1 AND frequency > 0 order by frequency desc limit CEIL((select count(*) from carrotson_branches where context = $1 AND frequency > 0)*1.0*$2)) as c order by random() limit 1", string(context), t) 65 | var follows string 66 | var frequency int64 67 | err := row.Scan(&follows, &frequency) 68 | if err == sql.ErrNoRows { 69 | return nil, nil 70 | } 71 | if err != nil { 72 | return nil, err 73 | } 74 | if len(follows) == 0 { 75 | return nil, EmptyFollowsError 76 | } 77 | return &Branch{ 78 | Context: context, 79 | Follows: []rune(follows)[0], 80 | Frequency: frequency, 81 | }, nil 82 | } 83 | 84 | func QueryBranchesFromContext(db *sql.DB, context []rune) ([]Branch, error) { 85 | rows, err := db.Query("SELECT follows, frequency FROM Carrotson_Branches WHERE context = $1 AND frequency > 0", string(context)) 86 | if err != nil { 87 | return nil, err 88 | } 89 | branches := []Branch{} 90 | for rows.Next() { 91 | branch := Branch{} 92 | var follows string 93 | err = rows.Scan(&follows, &branch.Frequency) 94 | if err != nil { 95 | return nil, err 96 | } 97 | if len(follows) == 0 { 98 | return nil, EmptyFollowsError 99 | } 100 | branch.Follows = []rune(follows)[0] 101 | branches = append(branches, branch) 102 | } 103 | return branches, nil 104 | } 105 | 106 | func ContextOfMessage(message []rune) []rune { 107 | i := len(message) - ContextSize 108 | if i < 0 { 109 | i = 0 110 | } 111 | return message[i:len(message)] 112 | } 113 | 114 | func CarrotsonGenerate(db *sql.DB, prefix string, limit int) (string, error) { 115 | var err error = nil 116 | var branch *Branch 117 | message := []rune(prefix) 118 | t := float64(len(message)) / float64(limit) 119 | if len(message) >= ContextSize || len(message) == 0 { 120 | branch, err = QueryRandomBranchFromContext(db, ContextOfMessage(message), (math.Cos(t*math.Pi*1.5)+1.0)/2.0) 121 | } else { 122 | branch, err = QueryRandomBranchFromUnfinishedContext(db, ContextOfMessage(message)) 123 | if err == nil && branch != nil { 124 | message = branch.Context 125 | } 126 | } 127 | for err == nil && branch != nil && len(message) < limit { 128 | message = append(message, branch.Follows) 129 | t = float64(len(message)) / float64(limit) 130 | branch, err = QueryRandomBranchFromContext(db, ContextOfMessage(message), (math.Cos(t*math.Pi*1.5)+1.0)/2.0) 131 | } 132 | return string(message), err 133 | } 134 | 135 | func FeedMessageToCarrotson(db *sql.DB, message string) { 136 | tx, err := db.Begin() 137 | if err != nil { 138 | log.Println("ERROR: feedMessageToCarrotson: could not start transaction:", err) 139 | return 140 | } 141 | for _, path := range splitMessageIntoPaths([]rune(message)) { 142 | _, err := tx.Exec("INSERT INTO Carrotson_Branches (context, follows, frequency) VALUES ($1, $2, 1) ON CONFLICT (context, follows) DO UPDATE SET frequency = Carrotson_Branches.frequency + 1;", string(path.context), string([]rune{path.follows})) 143 | if err != nil { 144 | log.Println("ERROR: feedMessageToCarrotson: could not insert element", string(path.context), string([]rune{path.follows}), ":", err) 145 | err := tx.Rollback() 146 | if err != nil { 147 | log.Println("ERROR: feedMessageToCarrotson: could not rollback transaction after failure:", err) 148 | } 149 | return 150 | } 151 | } 152 | err = tx.Commit() 153 | if err != nil { 154 | log.Println("ERROR: feedMessageToCarrotson: could not commit transaction:", err) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /internal/postgres.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "database/sql" 7 | "github.com/tsoding/smig" 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | func migratePostgres(db *sql.DB) bool { 12 | log.Println("Checking if there are any migrations to apply") 13 | tx, err := db.Begin() 14 | if err != nil { 15 | log.Println("Error starting the migration transaction:", err) 16 | return false 17 | } 18 | 19 | err = smig.MigratePG(tx, "./sql/") 20 | if err != nil { 21 | log.Println("Error during the migration:", err) 22 | 23 | err = tx.Rollback() 24 | if err != nil { 25 | log.Println("Error rolling back the migration transaction:", err) 26 | } 27 | 28 | return false 29 | } 30 | 31 | err = tx.Commit() 32 | if err != nil { 33 | log.Println("Error during committing the transaction:", err) 34 | return false 35 | } 36 | 37 | log.Println("All the migrations are applied") 38 | return true 39 | } 40 | 41 | func StartPostgreSQL() *sql.DB { 42 | pgsqlConnection, found := os.LookupEnv("GATEKEEPER_PGSQL_CONNECTION") 43 | if !found { 44 | log.Println("Could not find GATEKEEPER_PGSQL_CONNECTION variable") 45 | return nil 46 | } 47 | 48 | db, err := sql.Open("postgres", pgsqlConnection) 49 | if err != nil { 50 | log.Println("Could not open PostgreSQL connection:", err) 51 | return nil 52 | } 53 | 54 | ok := migratePostgres(db) 55 | if !ok { 56 | err := db.Close() 57 | if err != nil { 58 | log.Println("Error while closing PostgreSQL connection due to failed migration:", err) 59 | } 60 | return nil 61 | } 62 | 63 | return db 64 | } 65 | -------------------------------------------------------------------------------- /logo/gatekeeper-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/Gatekeeper/faf43d28278b575b58c6e3cb5bdc20f32de461e6/logo/gatekeeper-256.png -------------------------------------------------------------------------------- /logo/gatekeeper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/Gatekeeper/faf43d28278b575b58c6e3cb5bdc20f32de461e6/logo/gatekeeper.png -------------------------------------------------------------------------------- /logo/gatekeeper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 368 | -------------------------------------------------------------------------------- /sql/01-init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE TrustLog ( 2 | trusterId varchar(32), 3 | trusteeId varchar(32), 4 | trustedAt timestamp DEFAULT now() 5 | ) 6 | -------------------------------------------------------------------------------- /sql/02-carrotson.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Carrotson_Branches ( 2 | context varchar(8), 3 | follows char, 4 | frequency bigint, 5 | UNIQUE(context, follows) 6 | ); 7 | -------------------------------------------------------------------------------- /sql/03-bex.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Commands ( 2 | name varchar(64), 3 | bex varchar(1024), 4 | UNIQUE(name) 5 | ); 6 | -------------------------------------------------------------------------------- /sql/04-discord-log.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Discord_Log( 2 | message_id varchar(32), 3 | user_id varchar(32), 4 | user_name varchar(32), 5 | posted_at timestamp DEFAULT now(), 6 | text varchar(2000) 7 | ); 8 | -------------------------------------------------------------------------------- /sql/05-ed-state.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Ed_State( 2 | -- NOTE: user_id is a string that uniquely identifies the user across environments. 3 | -- On Twitch it's the nickname (which is between 4-25 characters + "twitch#" prefix). 4 | -- On Discord it's the user id (which is a Snowflake ID which is a 64bit integer the maximium 5 | -- value of which in decimal is 20 characters long + "discord#" prefix). 6 | -- So we set the size of the user ID as 32 just in case to accomodate both of the ids. 7 | user_id varchar(32), 8 | -- NOTE: the size of the buffer is based on EdLineCountLimit and EdLineSizeLimit constants. 9 | -- The formula is 2*EdLineCountLimit*EdLineSizeLimit (the 2 is to accomodate the newlines) 10 | -- If the constants are modified, this size should be adjusted as well. 11 | buffer varchar(1000), 12 | cur int, 13 | mode int, 14 | UNIQUE(user_id) 15 | ); 16 | -------------------------------------------------------------------------------- /sql/06-command-count.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE Commands ADD COLUMN count bigint DEFAULT 0; 2 | -------------------------------------------------------------------------------- /sql/07-song-log.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Song_Log( 2 | artist varchar(256), 3 | title varchar(256), 4 | startedAt timestamp DEFAULT now() 5 | ) 6 | -------------------------------------------------------------------------------- /tools/inflitrate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ^ 3 | # We only support bash 4 | 5 | set -e 6 | 7 | export PGVER=17.2 8 | export GOVER=1.23.4 9 | export GATEKEEPER_PREFIX="$HOME/Gatekeeper" 10 | export PGDATA="$GATEKEEPER_PREFIX/data/db" # NOTE(rexim): Tells PostgreSQL where the database is 11 | export PATH="$GATEKEEPER_PREFIX/pkg/go/bin:$GATEKEEPER_PREFIX/pkg/postgresql-$PGVER/bin/:$PATH" 12 | export GATEKEEPER_COMMANDS=() 13 | 14 | GATEKEEPER_COMMANDS+=(setup-everything) 15 | setup-everything() { 16 | echo "##################################################################" 17 | echo "# WARNING! This script is a part of an on going effort to create #" 18 | echo "# Nyr-style (See https://github.com/Nyr/wireguard-install) #" 19 | echo "# setup script for Gatekeeper and it's not finished yet. #" 20 | echo "# It is not recommended to use it right now. #" 21 | echo "##################################################################" 22 | echo "" 23 | read -p "Press Enter to continue or ^C to cancel..." 24 | 25 | # NOTE(rexim): Rough Layout of $GATEKEEPER_PREFIX 26 | # 27 | # `-$GATEKEEPER_PREFIX/ 28 | # | 29 | # `-src/ # Source code 30 | # | `-postgres/... 31 | # | `-gatekeeper/... 32 | # `-pkg/ # Binaries build from the Source code 33 | # | `-postgres/... 34 | # | `-go/... 35 | # `-data/ # Applications data 36 | # `-db/... 37 | mkdir -vp "$GATEKEEPER_PREFIX/src" 38 | mkdir -vp "$GATEKEEPER_PREFIX/pkg" 39 | mkdir -vp "$GATEKEEPER_PREFIX/data" 40 | 41 | setup-deps 42 | setup-postgres 43 | setup-go 44 | setup-gatekeeper 45 | 46 | echo "To enter the inflitrated environment do" 47 | echo "" 48 | echo " source $GATEKEEPER_PREFIX/inflitrate.sh" 49 | echo "" 50 | echo "in your bash" 51 | } 52 | 53 | # TODO(rexim): do not try to call sudo if all the necessary dependencies are already installed 54 | GATEKEEPER_COMMANDS+=(setup-deps) 55 | setup-deps() { 56 | . /etc/os-release 57 | # TODO(rexim): test on different distros via Docker 58 | case $ID in 59 | "void") 60 | # TODO(rexim): update xbps 61 | sudo xbps-install -y git pkg-config bison flex readline readline-devel 62 | ;; 63 | "debian") 64 | # TODO(rexim): update apt 65 | su -c "apt install git pkg-config gcc libicu-dev bison flex libreadline-dev zlib1g-dev" 66 | ;; 67 | *) 68 | echo "------------------------------------------------------------" 69 | echo "$NAME currently is not supported." 70 | echo "------------------------------------------------------------" 71 | exit 1 72 | esac 73 | } 74 | 75 | GATEKEEPER_COMMANDS+=(setup-postgres) 76 | setup-postgres() { 77 | if [ ! -e "$GATEKEEPER_PREFIX/pkg/postgresql-$PGVER/" ]; then 78 | if [ ! -e "$GATEKEEPER_PREFIX/src/postgresql-$PGVER/" ]; then 79 | cd "$GATEKEEPER_PREFIX/src" 80 | wget https://ftp.postgresql.org/pub/source/v$PGVER/postgresql-$PGVER.tar.gz 81 | tar fvx postgresql-$PGVER.tar.gz 82 | else 83 | echo "$GATEKEEPER_PREFIX/src/postgresql-$PGVER/ already exists" 84 | fi 85 | 86 | cd "$GATEKEEPER_PREFIX/src/postgresql-$PGVER/" 87 | # TODO(rexim): Do we need to build postgres with ssl support? 88 | # Doesn't feel like we do cause this script implies that we are running bot and db 89 | # on the same machine and the db only listens to local connections. But who knows? 90 | # Maybe this script will support multiple machines setup in the future. But even 91 | # in the case of multiple machine setup it is easier to just running everything 92 | # inside of a VPN and listen only to the local VPN connections. 93 | ./configure --prefix="$GATEKEEPER_PREFIX/pkg/postgresql-$PGVER/" 94 | make -j$(nproc) 95 | make install 96 | else 97 | echo "$GATEKEEPER_PREFIX/pkg/postgresql-$PGVER/ already exists" 98 | fi 99 | 100 | mkdir -vp "$GATEKEEPER_PREFIX/data/logs" 101 | 102 | if [ ! -e "$PGDATA" ]; then 103 | initdb -U postgres 104 | db-start # TODO(rexim): if there is already running stock Postgres on the machine this step will fail 105 | createuser gatekeeper -U postgres 106 | createdb gatekeeper -U postgres -O gatekeeper 107 | db-stop 108 | else 109 | echo "$PGDATA already exists" 110 | fi 111 | } 112 | 113 | GATEKEEPER_COMMANDS+=(setup-go) 114 | setup-go() { 115 | if [ -e "$GATEKEEPER_PREFIX/pkg/go/" ]; then 116 | echo "$GATEKEEPER_PREFIX/pkg/go/ already exists" 117 | return 118 | fi 119 | 120 | cd "$GATEKEEPER_PREFIX/pkg" 121 | wget https://go.dev/dl/go$GOVER.linux-amd64.tar.gz 122 | tar fvx go$GOVER.linux-amd64.tar.gz 123 | } 124 | 125 | GATEKEEPER_COMMANDS+=(setup-gatekeeper) 126 | setup-gatekeeper() { 127 | if [ ! -e "$GATEKEEPER_PREFIX/src/gatekeeper" ]; then 128 | cd "$GATEKEEPER_PREFIX/src" 129 | 130 | # TODO(rexim): iirc Go has its own sort of standardized layout of installing packages. 131 | # It has something to do with $GOPATH and $GOROOT or whatever (I'm not a Go dev, I don't know) 132 | # Maybe we can utilize this mechanism here somehow. 133 | 134 | # TODO(rexim): constant interrupt questions like this do not allow to just start inflitrating and walk away 135 | # We should probably introduce some sort of preconfigure step were you can set your preferences 136 | read -p 'Clone Gatekeeper source code from the SSH url? [y/n] ' yn 137 | while true; do 138 | case $yn in 139 | [Yy]*) 140 | git clone "git@github.com:tsoding/gatekeeper.git" 141 | break 142 | ;; 143 | [Nn]*) 144 | git clone "https://github.com/tsoding/gatekeeper" 145 | break 146 | ;; 147 | esac 148 | done 149 | else 150 | echo "$GATEKEEPER_PREFIX/src/gatekeeper already exists" 151 | fi 152 | 153 | if [ ! -e "$GATEKEEPER_PREFIX/data/secret" ]; then 154 | echo "Generating $GATEKEEPER_PREFIX/data/secret" 155 | # TODO(rexim): walk the user throw the entire process of acquiring them for both Twitch and Discord. 156 | cat > "$GATEKEEPER_PREFIX/data/secret" </dev/null); then 238 | # TODO(rexim): check if we are already inflitrated and maybe say something about that? 239 | # I don't really know if it makes sense to even care about user doing `env` while within `env`... 240 | set +e # Do not enable exit-on-error in the user facing environment 241 | # TODO(rexim): print the available commands and just a general help message 242 | cmd-list 243 | PS1="[inflitrated] $PS1" 244 | elif [ -z "$@" ]; then 245 | setup-everything 246 | else 247 | $@ 248 | fi 249 | 250 | # TODO(rexim): subcommand to make/restore backups 251 | # TODO(rexim): how would you autostart the whole system with this kind of setup? 252 | --------------------------------------------------------------------------------