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