initial commit
This commit is contained in:
		
						commit
						3af2afbae8
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| hubbub | ||||
| config.toml | ||||
							
								
								
									
										24
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| Dayenu Public License | ||||
| 
 | ||||
| Copyright (c) ~nbsp | ||||
| All rights reserved. | ||||
| 
 | ||||
| Redistribution and use in source and binary forms, with or without | ||||
| modification, are permitted provided that the following conditions are met: | ||||
| 
 | ||||
| 1. If They had used the software, but did not make any changes to it — dayenu! | ||||
| 
 | ||||
| 2. If They had made changes to the software, but did not redistribute the source | ||||
|    code — dayenu! | ||||
| 
 | ||||
| 3. If They had redistributed the source code, but did not retain the above | ||||
|    copyright notice — dayenu! | ||||
| 
 | ||||
| 4. If They had retained the copyright notice, but not this list of license | ||||
|    conditions — dayenu! | ||||
| 
 | ||||
| 5. If They had retained this list of license conditions, but did not license the | ||||
|    derivative work under the same license — dayenu! | ||||
| 
 | ||||
| DAY-DAY-YENU, DAY-DAY-YENU, DAY-DAY-YENU, DAYENU DAYENU, DAYENU— | ||||
| DAY-DAY-YENU, DAY-DAY-YENU, DAY-DAY-YENU, DAYENU DAYENU! | ||||
							
								
								
									
										56
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| # hubbub | ||||
| 
 | ||||
| hubbub is a frightening idea: Voice-over-IRC-over-SSH. i saw [voirc] by asie | ||||
| before making this, but hubbub is different in a couple of ways: | ||||
| 
 | ||||
| - it doesn't plop you in a big alternate screen | ||||
| - it does not support text | ||||
| - it has deafen and mute indicators | ||||
| - crucially, it is built for tilde town, so it tunnels through ssh. | ||||
| 
 | ||||
| [voirc]: https://github.com/asiekierka/voirc | ||||
| 
 | ||||
| the code is about as horrifying as the idea itself. if something breaks when it | ||||
| reasonably shouldn't, let me know (or fix it and then let me know!) | ||||
| 
 | ||||
| ## todo | ||||
| 
 | ||||
| - [x] joining a channel | ||||
| - [x] handling everyone's state | ||||
| - [ ] any of the actual audio stuff | ||||
| 
 | ||||
| ## building | ||||
| hubbub requires go 1.23 to build. it might run on earlier versions, but i think | ||||
| `slices` mainlined in 1.23 and i can't be bothered to check. run `go build` and | ||||
| copy the binary wherever. | ||||
| 
 | ||||
| ## usage | ||||
| hubbub needs a TOML config file, either passed using `--config` or at | ||||
| `.config/hubbub/config.toml`: | ||||
| 
 | ||||
| ```toml | ||||
| username = 'shrike420' | ||||
| privkey = '~/.ssh/id_ed25519' | ||||
| nick = 'shrike420_voice' | ||||
| channel = '#hubbub' | ||||
| ``` | ||||
| 
 | ||||
| note that the nick is different than the username: if you're already logged onto | ||||
| irc you won't be able to use that nick. you can also pass a different server or | ||||
| port but you really don't have a reason to. | ||||
| 
 | ||||
| i'm writing this part about audio way in advance. i'll probably use alsa? either | ||||
| alsa or pipewire if i can be bothered with pipewire. | ||||
| 
 | ||||
| here's the controls: | ||||
| ``` | ||||
| ,/m/space   toggle mute | ||||
| ./d         toggle deafen | ||||
| q/^c/^d     quit :( | ||||
| ``` | ||||
| 
 | ||||
| ## license | ||||
| this monstrosity is licensed under the Dayenu Public License. see [the notice] | ||||
| for details. | ||||
| 
 | ||||
| [the notice]: LICENSE | ||||
							
								
								
									
										4
									
								
								config.example.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								config.example.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| username = 'shrike420' | ||||
| privkey = '~/.ssh/id_ed25519' | ||||
| nick = 'shrike420_voice' | ||||
| channel = '#hubbub' | ||||
							
								
								
									
										53
									
								
								config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								config/config.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| package config | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"github.com/spf13/viper" | ||||
| ) | ||||
| 
 | ||||
| type Config struct { | ||||
| 	Server   string | ||||
| 	Port     uint16 | ||||
| 	Username string | ||||
| 	Privkey  string | ||||
| 	Nick     string | ||||
| 	Channel  string | ||||
| } | ||||
| 
 | ||||
| func LoadConfigFile(path string) (Config, error) { | ||||
| 	viper.SetDefault("Server", "tilde.town") | ||||
| 	viper.SetDefault("Port", 22) | ||||
| 
 | ||||
| 	viper.SetConfigName("config") | ||||
| 	viper.SetConfigType("toml") | ||||
| 	viper.AddConfigPath(".") | ||||
| 	viper.AddConfigPath(os.ExpandEnv("$HOME/.config/hubbub")) | ||||
| 	if path != "" { | ||||
| 		viper.SetConfigFile(path) | ||||
| 	} | ||||
| 
 | ||||
| 	var config Config | ||||
| 	if err := viper.ReadInConfig(); err != nil { | ||||
| 		return config, err | ||||
| 	} | ||||
| 	if err := viper.Unmarshal(&config); err != nil { | ||||
| 		return config, err | ||||
| 	} | ||||
| 
 | ||||
| 	if config.Username == "" { | ||||
| 		return config, errors.New("username is required") | ||||
| 	} | ||||
| 	if config.Privkey == "" { | ||||
| 		return config, errors.New("privkey is required") | ||||
| 	} | ||||
| 	if config.Nick == "" { | ||||
| 		return config, errors.New("nick is required") | ||||
| 	} | ||||
| 	if config.Channel == "" { | ||||
| 		return config, errors.New("channel is required") | ||||
| 	} | ||||
| 
 | ||||
| 	return config, nil | ||||
| } | ||||
							
								
								
									
										48
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| module git.tilde.town/nbsp/hubbub | ||||
| 
 | ||||
| go 1.24.0 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/charmbracelet/bubbletea v1.3.10 | ||||
| 	github.com/charmbracelet/lipgloss v1.1.0 | ||||
| 	github.com/spf13/viper v1.21.0 | ||||
| 	golang.org/x/crypto v0.43.0 | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect | ||||
| 	github.com/fsnotify/fsnotify v1.9.0 // indirect | ||||
| 	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.2.4 // indirect | ||||
| 	github.com/russross/blackfriday/v2 v2.1.0 // indirect | ||||
| 	github.com/sagikazarmark/locafero v0.11.0 // indirect | ||||
| 	github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect | ||||
| 	github.com/spf13/afero v1.15.0 // indirect | ||||
| 	github.com/spf13/cast v1.10.0 // indirect | ||||
| 	github.com/spf13/pflag v1.0.10 // indirect | ||||
| 	github.com/subosito/gotenv v1.6.0 // indirect | ||||
| 	github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect | ||||
| 	go.yaml.in/yaml/v3 v3.0.4 // indirect | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | ||||
| 	github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect | ||||
| 	github.com/charmbracelet/x/ansi v0.10.1 // indirect | ||||
| 	github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect | ||||
| 	github.com/charmbracelet/x/term v0.2.1 // indirect | ||||
| 	github.com/ergochat/irc-go v0.4.0 | ||||
| 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | ||||
| 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||
| 	github.com/mattn/go-localereader v0.0.1 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.16 // indirect | ||||
| 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | ||||
| 	github.com/muesli/cancelreader v0.2.2 // indirect | ||||
| 	github.com/muesli/termenv v0.16.0 // indirect | ||||
| 	github.com/rivo/uniseg v0.4.7 // indirect | ||||
| 	github.com/urfave/cli/v2 v2.27.7 | ||||
| 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect | ||||
| 	golang.org/x/sys v0.37.0 // indirect | ||||
| 	golang.org/x/text v0.30.0 // indirect | ||||
| ) | ||||
							
								
								
									
										82
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | ||||
| github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= | ||||
| github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= | ||||
| github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= | ||||
| github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= | ||||
| github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= | ||||
| github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= | ||||
| github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= | ||||
| github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= | ||||
| github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= | ||||
| github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= | ||||
| github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= | ||||
| github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= | ||||
| github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= | ||||
| github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= | ||||
| github.com/ergochat/irc-go v0.4.0 h1:0YibCKfAAtwxQdNjLQd9xpIEPisLcJ45f8FNsMHAuZc= | ||||
| github.com/ergochat/irc-go v0.4.0/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0= | ||||
| github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= | ||||
| github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | ||||
| github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= | ||||
| github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= | ||||
| github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= | ||||
| github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | ||||
| github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | ||||
| github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | ||||
| github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||
| github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= | ||||
| github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= | ||||
| github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= | ||||
| github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||
| github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= | ||||
| github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= | ||||
| github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= | ||||
| github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= | ||||
| github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= | ||||
| github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= | ||||
| github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= | ||||
| github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= | ||||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | ||||
| github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||||
| github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= | ||||
| github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||
| github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= | ||||
| github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= | ||||
| github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= | ||||
| github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= | ||||
| github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= | ||||
| github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= | ||||
| github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= | ||||
| github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= | ||||
| github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= | ||||
| github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||
| github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= | ||||
| github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= | ||||
| github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= | ||||
| github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= | ||||
| github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= | ||||
| github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= | ||||
| github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= | ||||
| github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= | ||||
| github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= | ||||
| github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= | ||||
| go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= | ||||
| go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= | ||||
| golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= | ||||
| golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= | ||||
| golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= | ||||
| golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= | ||||
| golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= | ||||
| golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | ||||
| golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= | ||||
| golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | ||||
| golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= | ||||
| golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= | ||||
| golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= | ||||
| golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
							
								
								
									
										283
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,283 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"slices" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"git.tilde.town/nbsp/hubbub/config" | ||||
| 	tea "github.com/charmbracelet/bubbletea" | ||||
| 	"github.com/charmbracelet/lipgloss" | ||||
| 	"github.com/ergochat/irc-go/ircevent" | ||||
| 	"github.com/ergochat/irc-go/ircmsg" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| 	"golang.org/x/crypto/ssh" | ||||
| ) | ||||
| 
 | ||||
| var app = &cli.App{ | ||||
| 	HelpName: "hubbub", | ||||
| 	Usage:    "voice chat over IRC over SSH", | ||||
| 	Action:   start, | ||||
| 	Flags: []cli.Flag{ | ||||
| 		&cli.StringFlag{ | ||||
| 			Name:    "config", | ||||
| 			Aliases: []string{"c"}, | ||||
| 			Usage:   "load config from file", | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| var styleNotification = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(3)) | ||||
| var styleMic = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(1)) | ||||
| var styleInactive = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(0)) | ||||
| var deafened = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(1)).Render("D") | ||||
| var muted = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(3)).Render("M") | ||||
| var speaking = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(2)).Render("-") | ||||
| 
 | ||||
| type user struct { | ||||
| 	lastSpoke  time.Time | ||||
| 	isMuted    bool | ||||
| 	isDeafened bool | ||||
| } | ||||
| 
 | ||||
| type notification struct { | ||||
| 	timestamp time.Time | ||||
| 	text      string | ||||
| } | ||||
| 
 | ||||
| type model struct { | ||||
| 	users         map[string]user | ||||
| 	notifications []notification | ||||
| 	nick          string | ||||
| 	channel       string | ||||
| 	mute          bool | ||||
| 	deafen        bool | ||||
| 	conn          *Conn | ||||
| } | ||||
| 
 | ||||
| type Conn struct { | ||||
| 	ircevent.Connection | ||||
| } | ||||
| 
 | ||||
| func tick() tea.Cmd { | ||||
| 	return tea.Tick(time.Millisecond*250, func(t time.Time) tea.Msg { | ||||
| 		return t | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (m *model) setMute(is bool) { | ||||
| 	m.mute = is | ||||
| 	u := m.users[m.nick] | ||||
| 	u.isMuted = is | ||||
| 	m.users[m.nick] = u | ||||
| 	un := "" | ||||
| 	if !is { | ||||
| 		un = "UN" | ||||
| 	} | ||||
| 	m.conn.Privmsg(m.channel, un+"MUTE") | ||||
| } | ||||
| func (m *model) setDeafen(is bool) { | ||||
| 	m.deafen = is | ||||
| 	u := m.users[m.nick] | ||||
| 	u.isDeafened = is | ||||
| 	m.users[m.nick] = u | ||||
| 	un := "" | ||||
| 	if !is { | ||||
| 		un = "UN" | ||||
| 	} | ||||
| 	m.conn.Privmsg(m.channel, un+"DEAFEN") | ||||
| } | ||||
| 
 | ||||
| func (m model) Init() tea.Cmd { | ||||
| 	m.conn.AddCallback("PRIVMSG", func(e ircmsg.Message) { | ||||
| 		target, message := e.Params[0], e.Params[1] | ||||
| 		if target != m.channel { | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		u, ok := m.users[e.Nick()] | ||||
| 		if !ok { | ||||
| 			m.users[e.Nick()] = user{} | ||||
| 		} | ||||
| 
 | ||||
| 		switch message { | ||||
| 		case "DEAFEN": | ||||
| 			u.isDeafened = true | ||||
| 		case "UNDEAFEN": | ||||
| 			u.isDeafened = false | ||||
| 		case "MUTE": | ||||
| 			u.isMuted = true | ||||
| 		case "UNMUTE": | ||||
| 			u.isDeafened = false | ||||
| 			u.isMuted = false | ||||
| 		default: | ||||
| 			u.lastSpoke = time.Now() | ||||
| 		} | ||||
| 
 | ||||
| 		m.users[e.Nick()] = u | ||||
| 	}) | ||||
| 
 | ||||
| 	m.conn.AddCallback("JOIN", func(e ircmsg.Message) { | ||||
| 		m.users[e.Nick()] = user{} | ||||
| 
 | ||||
| 		// advertise your muted and deafened statuses on someone joining | ||||
| 		if m.deafen { | ||||
| 			m.conn.Privmsg(m.channel, "DEAFEN") | ||||
| 		} else { | ||||
| 			m.conn.Privmsg(m.channel, "UNDEAFEN") | ||||
| 		} | ||||
| 		if m.mute { | ||||
| 			m.conn.Privmsg(m.channel, "MUTE") | ||||
| 		} else { | ||||
| 			m.conn.Privmsg(m.channel, "UNMUTE") | ||||
| 		} | ||||
| 	}) | ||||
| 
 | ||||
| 	m.conn.AddCallback("PART", func(e ircmsg.Message) { | ||||
| 		delete(m.users, e.Nick()) | ||||
| 	}) | ||||
| 
 | ||||
| 	m.conn.Join(m.channel) | ||||
| 
 | ||||
| 	return tea.Batch(tick()) | ||||
| } | ||||
| 
 | ||||
| func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||||
| 	switch msg := msg.(type) { | ||||
| 	case time.Time: | ||||
| 		return m, tick() | ||||
| 	case tea.KeyMsg: | ||||
| 		switch msg.String() { | ||||
| 		case "ctrl+c", "q", "ctrl+d": | ||||
| 			return m, tea.Quit | ||||
| 		case " ", "m", ",": | ||||
| 			// if deafened, unmute and undeafen | ||||
| 			if m.deafen { | ||||
| 				m.setMute(false) | ||||
| 				m.setDeafen(false) | ||||
| 			} else { | ||||
| 				m.setMute(!m.mute) | ||||
| 			} | ||||
| 		case "d", ".": | ||||
| 			m.setDeafen(!m.deafen) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return m, nil | ||||
| } | ||||
| 
 | ||||
| func (m model) View() (s string) { | ||||
| 	s = fmt.Sprintf("connected to voice on %s\n", m.channel) | ||||
| 	if m.deafen { | ||||
| 		s += styleMic.Render("deafened: press period to undeafen or comma to unmute.") + "\n" | ||||
| 	} else if m.mute { | ||||
| 		s += styleMic.Render("muted: press comma to unmute.") + "\n" | ||||
| 	} | ||||
| 
 | ||||
| 	for i, notification := range m.notifications { | ||||
| 		if notification.timestamp.Add(time.Second * 5).Before(time.Now()) { | ||||
| 			m.notifications = append(m.notifications[:i], m.notifications[i+1:]...) | ||||
| 		} else { | ||||
| 			s += styleNotification.Render(notification.text) + "\n" | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	plural := "s" | ||||
| 	numUsers := len(m.users) | ||||
| 	if numUsers == 1 { | ||||
| 		plural = "" | ||||
| 	} | ||||
| 	s += fmt.Sprintf("%d user%s connected:\n", numUsers, plural) | ||||
| 
 | ||||
| 	keys := make([]string, 0, numUsers) | ||||
| 	for k := range m.users { | ||||
| 		keys = append(keys, k) | ||||
| 	} | ||||
| 	slices.Sort(keys) | ||||
| 	for _, nick := range keys { | ||||
| 		user := m.users[nick] | ||||
| 		status := " " | ||||
| 		nickStyled := styleInactive.Render(nick) | ||||
| 		if user.isDeafened { | ||||
| 			status = deafened | ||||
| 		} else if user.isMuted { | ||||
| 			status = muted | ||||
| 		} else if user.lastSpoke.Add(time.Second * 2).After(time.Now()) { | ||||
| 			status = speaking | ||||
| 			nickStyled = nick | ||||
| 		} | ||||
| 		s += fmt.Sprintf("[%s] %s\n", status, nickStyled) | ||||
| 	} | ||||
| 
 | ||||
| 	s += ", mute  |  . deafen  |  q quit" | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func start(c *cli.Context) error { | ||||
| 	config, err := config.LoadConfigFile(c.String("config")) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("unable to read config file: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	key, err := os.ReadFile(os.ExpandEnv(strings.Replace(config.Privkey, "~", "$HOME", 1))) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("unable to read private key: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	signer, err := ssh.ParsePrivateKey(key) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("unable to parse private key: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", config.Server, config.Port), &ssh.ClientConfig{ | ||||
| 		User: config.Username, | ||||
| 		Auth: []ssh.AuthMethod{ | ||||
| 			ssh.PublicKeys(signer), | ||||
| 		}, | ||||
| 		HostKeyCallback: ssh.InsecureIgnoreHostKey(), // XXX | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Fatalf("unable to connect to ssh: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 	defer client.Close() | ||||
| 
 | ||||
| 	conn := &Conn{ | ||||
| 		Connection: ircevent.Connection{ | ||||
| 			Server: "localhost:6667", | ||||
| 			Nick:   config.Nick, | ||||
| 			UseTLS: false, | ||||
| 			DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { | ||||
| 				return client.Dial(network, addr) | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	if err := conn.Connect(); err != nil { | ||||
| 		log.Fatalf("unable to connect to irc: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 	p := tea.NewProgram(model{ | ||||
| 		channel:       config.Channel, | ||||
| 		nick:          config.Nick, | ||||
| 		conn:          conn, | ||||
| 		users:         map[string]user{}, | ||||
| 		notifications: []notification{}, | ||||
| 	}) | ||||
| 
 | ||||
| 	_, err = p.Run() | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	if err := app.Run(os.Args); err != nil { | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user