├── .gitignore
├── .gitlab-ci.yml
├── .gitmodules
├── 6cord.toml
├── LICENSE
├── README.md
├── _screenshots
├── clean.png
├── commands.png
├── guildview.png
├── highlight.png
├── img.png
├── latest.png
├── mentions.png
└── reactions.png
├── _tests
├── feh.sh
├── main.go.bak
├── term_size
│ └── main.go
├── tui-go
│ └── main.go
├── typing
│ └── main.go
└── voice
│ ├── main.go
│ └── test.go
├── antitele
└── antitele.go
├── center
└── center.go
├── clipboard.go
├── command.go
├── commandblock.go
├── commandcopy.go
├── commanddebug.go
├── commanddelete.go
├── commanddm.go
├── commandedit.go
├── commandeditor.go
├── commandgoto.go
├── commandheated.go
├── commandmentions.go
├── commandmessages.go
├── commandnick.go
├── commandreact.go
├── commandsixel.go
├── commandstatus.go
├── commandtoken.go
├── commandupload.go
├── config.go
├── configfile.go
├── demojis
├── emojis.go
└── fuzzy.go
├── docs
├── 6cord.png
├── CNAME
└── index.html
├── editor.go
├── fmtemojis.go
├── fmtmessage.go
├── fuzzy.go
├── fuzzychannels.go
├── fuzzycommands.go
├── fuzzyemojis.go
├── fuzzymentions.go
├── fuzzymessages.go
├── fuzzyupload.go
├── go.mod
├── go.sum
├── guildevents.go
├── guildmembers.go
├── guildsettings.go
├── heated.go
├── humanize.go
├── image.go
├── image
├── backend.go
├── image.go
├── query.go
├── sixel.go
├── ueberzug.go
├── w3m.go
├── winch.go
└── winch_test.go
├── imagecache.go
├── input.go
├── keyring
├── keyring.go
└── keyring_null.go
├── lastauthor_state.go
├── loadchannel.go
├── login.go
├── main.go
├── math.go
├── md
├── chroma.go
├── highlighter.go
├── md.go
└── md_test.go
├── mentions.go
├── message.go
├── messagecreate.go
├── messagedelete.go
├── messagerenderer.go
├── messagescroll.go
├── messageupdate.go
├── notify.go
├── onready.go
├── ontyping.go
├── parsementions.go
├── reactionevents.go
├── reactions.go
├── readstate.go
├── relationships.go
├── screenguild.go
├── screenmiddleware.go
├── scroll.go
├── shortener
├── port.go
├── shortener.go
└── url.go
├── sixel.go
├── sortchannels.go
├── syscall.go
├── syscall_linux.go
├── syscall_unix.go
├── syscall_windows.go
├── treenodefns.go
├── typing.go
├── ui.go
├── user.go
├── user_store.go
├── voice.go
├── voiceaudio.go
├── w3m
├── locate.go
└── w3m.go
├── warn.go
└── wraplines.go
/.gitignore:
--------------------------------------------------------------------------------
1 | 6cord
2 | 6cord.upx
3 | .vscode
4 | .history
5 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: golang:alpine
2 |
3 | variables:
4 | GO111MODULE: "on"
5 | CGO_ENABLED: 0
6 |
7 | before_script:
8 | - apk add git upx
9 |
10 | stages:
11 | - build
12 |
13 | linux:
14 | stage: build
15 | script:
16 | - time go get
17 | - export FLAGS="-ldflags -w -ldflags -s"
18 | # compiles 6cord without cgo so that it's statically linked
19 | # may affect performance - disabled for ueberzug + xorg
20 | - time go build $FLAGS -o $CI_PROJECT_DIR/6cord
21 | - upx -q --8086 -9 $CI_PROJECT_DIR/6cord
22 | - time go build $FLAGS -o $CI_PROJECT_DIR/6cord_nk -tags nokeyring
23 | - upx -q --8086 -9 $CI_PROJECT_DIR/6cord_nk
24 | artifacts:
25 | paths:
26 | - 6cord
27 | - 6cord_nk
28 |
29 | linux_arm64:
30 | stage: build
31 | script:
32 | - export GOOS=linux GOARCH=arm64
33 | - time go get
34 | - time go build -o $CI_PROJECT_DIR/6cord_arm64
35 | artifacts:
36 | paths:
37 | - 6cord_arm64
38 |
39 | windows:
40 | stage: build
41 | script:
42 | - export GOOS=windows
43 | - time go get
44 | - time go build -o $CI_PROJECT_DIR/6cord.exe
45 | artifacts:
46 | paths:
47 | - 6cord.exe
48 |
49 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "xterm-343"]
2 | path = xterm-343
3 | url = https://gitlab.com/diamondburned/xterm-343
4 |
--------------------------------------------------------------------------------
/6cord.toml:
--------------------------------------------------------------------------------
1 | # Email and password authentication flow
2 | # NOT recommended, neither me nor Discord
3 | username = ""
4 | password = ""
5 |
6 | # Token authentication flow
7 | # RECOMMENDED
8 | # Uses the token stored in keyrings if empty, will attempt to
9 | # store the token if the first token works
10 | token = ""
11 |
12 | [ properties ]
13 | # Refer to ./6cord -h for details.
14 | # Defaults are rather defined in the code, which is why this
15 | # should only be used as a reference on how to configure.
16 | # Keys are optional.
17 | true-color = true
18 | default-name-color = "#CCCCCC"
19 | mention-color = "#0D4A91"
20 | mention-self-color = "#17AC86"
21 | compact-mode = false
22 | show-channels = true
23 | chat-padding = 2
24 | sidebar-ratio = 3
25 | sidebar-indent = 2
26 | hide-blocked = true
27 | trigger-typing = true
28 | foreground-color = 15
29 | background-color = -1
30 | author-format = "[#{color}::b]{name}"
31 | command-prefix = "[${GUILD}${CHANNEL}] "
32 | default-status = "Send a message or input a command"
33 | syntax-highlight-colorscheme = "emacs"
34 | show-emoji-urls = true
35 | obfuscate-words = false
36 | chat-max-width = 0
37 | image-fetch-timeout = 1
38 | image-width = 400
39 | image-height = 400
40 | shorten-url = true
41 |
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Deprecated
2 |
3 |
4 | 6cord has been deprecated in favor of [gtkcord3](https://github.com/diamondburned/gtkcord3). Important bugs may not be
5 | fixed, but PRs are still welcomed.
6 |
7 |
8 | 6cord
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ## Installation
18 |
19 | ### Method 1. (recommended, precompiled binaries)
20 |
21 | **[Linux](https://gitlab.com/diamondburned/6cord/builds/artifacts/master/file/6cord?job=linux)**
22 | **[Linux (no dbus)](https://gitlab.com/diamondburned/6cord/builds/artifacts/master/file/6cord_nk?job=linux)**
23 | **[Linux (arm64)](https://gitlab.com/diamondburned/6cord/builds/artifacts/master/file/6cord_arm64?job=linux_arm64)**
24 | **[Windows](https://gitlab.com/diamondburned/6cord/builds/artifacts/master/file/6cord.exe?job=windows)**
25 |
26 | Only do this if the CI passed (a green tick in the commit bar)
27 |
28 | ### Method 2. (building from source)
29 |
30 | ```sh
31 | git clone https://gitlab.com/diamondburned/6cord
32 | cd 6cord && go build
33 | ./6cord
34 |
35 | # Optional
36 | mkdir -p ~/bin/
37 | mv ./6cord ~/bin/
38 | echo PATH="$HOME/bin:$PATH" >> ~/.bashrc && . ~/.bashrc # or any shellrc
39 | ```
40 |
41 | ### Method 3. (package manager)
42 |
43 | ```sh
44 | # Arch Linux, using your favourite AUR helper:
45 | yay install 6cord
46 | # Alternatively you can install '6cord-git'
47 | # which is the latest development version.
48 |
49 | # FreeBSD:
50 | pkg install 6cord
51 |
52 | # Void Linux:
53 | xbps-install 6cord
54 |
55 | # Alpine Linux (in the testing repo):
56 | apk add 6cord
57 | ```
58 |
59 | ## Getting the token
60 |
61 | This is possible from both the web client and the Electron client.
62 |
63 | 1. Hit Ctrl +Shift +I
64 | 2. Switch to the `Network` tab
65 | 3. Find Discord API requests. This is usually called `messages`, `ack`, `typing`, etc
66 | 4. Search for the `Authorization` header. This is the token.
67 |
68 | ## Running 6cord with the token
69 |
70 | `./6cord -t "TOKEN_HERE"`
71 |
72 | - If you have Gnome Keyring (usually the case on most DEs), the token would automatically be stored securely. This could be tested by running `./6cord` without any arguments.
73 | - To reset the token, override it with a new one using `-t`
74 | - It is also possible to move the `6cord.toml` file from the root of this Git repository to `~/.config/6cord/`, then run without any arguments.
75 |
76 | ## Additional things
77 |
78 | ### Quirks
79 |
80 | - The ~ key could be used to both preview images and select a message ID
81 | - `/mentions` is useless at the moment. This is planned to change in the future.
82 | - There is currently no global emoji support. This is also planned to change, along with emoji previews.
83 |
84 | ### Additional keybinds
85 |
86 | - Refer to the Quick Start section displayed when starting 6cord
87 | - Tab to show/hide the server list
88 | - Input field history is cycled with Alt + Up /Down
89 | - PgUp and PgDn can be used to jump between servers in the list
90 | - There are some Vim binds available ie ^n and ^p to move between fuzzy listed items
91 |
92 | ### `command-prefix`
93 |
94 | - The following variables are available: `CHANNEL`, `GUILD`, `USERNAME` and `DISCRIM`
95 | - This follows `tview`'s rich text format:
96 | - Coloring text with `[#424242]`
97 | - Bold text with `[::b]`
98 | - Both can be done with `[#424242::b]`
99 | - Reset with `[-]`, `[::-]` or `[-::-]`
100 | - You need to manually escape square brackets by adding an opening (`[`) bracket before a closing (`]`) bracket
101 | - Example: `[${guild}]` to `[${guild}[]`
102 |
103 | ### Color support
104 |
105 | 6cord runs in 256 color mode most of the time. To force true color, run:
106 |
107 | ```sh
108 | TERM=xterm-truecolor ./6cord`
109 | ```
110 |
111 | (`xterm-truecolor` is known to break a lot of applications including `htop`, only use it with `6cord`)
112 |
113 | To limit 6cord to strictly 16 colors, run:
114 |
115 | ```sh
116 | TERM=xterm-basic ./6cord
117 | ```
118 |
119 | To run 6cord in monochrome mode:
120 |
121 | ```sh
122 | TERM=xterm ./6cord
123 | ```
124 |
125 | ### Supported Image backends
126 |
127 | Currently, Xorg is the only supported image backend. SIXEL support proved itself to be challenging with how `tcell` and `tview` call redraws. There is no Kitty terminal implementation in Golang that is available as a library yet (`termui` has a PR with Kitty support). There are things in my priority list right now. That said, PRs are welcomed.
128 |
129 | ## Screenshots
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | ## Credits
142 |
143 | - XTerm from
144 | - https://invisible-island.net/xterm/
145 | - https://gist.github.com/saitoha/7822989
146 | - Fishy ([RumbleFrog](https://github.com/rumblefrog)) for his
147 | - [discordgo fork](https://github.com/rumblefrog/discordgo)
148 | - [Channel sort lib ~~that he stole from my shittercord~~](https://gist.github.com/rumblefrog/c9ebd9fb84a8955495d4fb7983345530)
149 | - Some people on unixporn and nix nest (ym555, tdeo, ...)
150 | - [cordless](https://github.com/Bios-Marcel/cordless) [(author)](https://github.com/Bios-Marcel) for some of the functions
151 |
152 |
--------------------------------------------------------------------------------
/_screenshots/clean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamondburned/6cord/979d7a6ace67070cc2ffa13cf35b22048da70754/_screenshots/clean.png
--------------------------------------------------------------------------------
/_screenshots/commands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamondburned/6cord/979d7a6ace67070cc2ffa13cf35b22048da70754/_screenshots/commands.png
--------------------------------------------------------------------------------
/_screenshots/guildview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamondburned/6cord/979d7a6ace67070cc2ffa13cf35b22048da70754/_screenshots/guildview.png
--------------------------------------------------------------------------------
/_screenshots/highlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamondburned/6cord/979d7a6ace67070cc2ffa13cf35b22048da70754/_screenshots/highlight.png
--------------------------------------------------------------------------------
/_screenshots/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamondburned/6cord/979d7a6ace67070cc2ffa13cf35b22048da70754/_screenshots/img.png
--------------------------------------------------------------------------------
/_screenshots/latest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamondburned/6cord/979d7a6ace67070cc2ffa13cf35b22048da70754/_screenshots/latest.png
--------------------------------------------------------------------------------
/_screenshots/mentions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamondburned/6cord/979d7a6ace67070cc2ffa13cf35b22048da70754/_screenshots/mentions.png
--------------------------------------------------------------------------------
/_screenshots/reactions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamondburned/6cord/979d7a6ace67070cc2ffa13cf35b22048da70754/_screenshots/reactions.png
--------------------------------------------------------------------------------
/_tests/feh.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | [ -z $1 ] && exit 1
3 |
4 | case "$1" in
5 | *.jpg*|*.png*|*.PNG*|*.jpeg*|*.gif*)
6 | H="800"
7 | W="600"
8 |
9 | [[ "$1" = *"/emojis/"* ]] && {
10 | H="50"
11 | W="50"
12 | }
13 |
14 | feh -H $H -W $W -b trans --auto-zoom --xinerama-index 0 -B black -. -x "$1"
15 | ;;
16 | *)
17 | xdg-open "$1"
18 | ;;
19 | esac
20 |
21 |
--------------------------------------------------------------------------------
/_tests/main.go.bak:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "time"
6 |
7 | "github.com/bwmarrin/discordgo"
8 | "github.com/davecgh/go-spew/spew"
9 | )
10 |
11 | func main() {
12 | d, _ := discordgo.New(os.Args[1])
13 |
14 | d.AddHandler(func(s *discordgo.Session, ts *discordgo.TypingStart) {
15 | spew.Dump(ts)
16 | })
17 |
18 | defer d.Close()
19 |
20 | e := d.Open()
21 | if e != nil {
22 | panic(e)
23 | }
24 |
25 | time.Sleep(time.Minute * 10)
26 | }
27 |
--------------------------------------------------------------------------------
/_tests/term_size/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "os"
6 | )
7 |
8 | func main() {
9 |
10 | scanner := bufio.NewScanner(os.Stdin)
11 |
12 | var text string
13 | for text != "t" { // break the loop if text == "q"
14 | scanner.Scan()
15 | print("\033[14t")
16 | text = scanner.Text()
17 | }
18 |
19 | println(text)
20 | }
21 |
--------------------------------------------------------------------------------
/_tests/tui-go/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "image"
5 | "os"
6 | "time"
7 |
8 | _ "image/png"
9 |
10 | tui "github.com/marcusolsson/tui-go"
11 | sixel "github.com/mattn/go-sixel"
12 | )
13 |
14 | func main() {
15 | path := "/home/diamond/Pictures/peni_ava.png"
16 |
17 | file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
18 | if err != nil {
19 | panic(err)
20 | }
21 |
22 | defer file.Close()
23 |
24 | img, _, err := image.Decode(file)
25 | if err != nil {
26 | panic(err)
27 | }
28 |
29 | tty, err := os.Open("/dev/tty")
30 | if err != nil {
31 | panic(err)
32 | }
33 |
34 | go func() {
35 | time.Sleep(time.Second * 1)
36 | if err := sixel.NewEncoder(tty).Encode(img); err != nil {
37 | panic(err)
38 | }
39 | }()
40 |
41 | text := tui.NewTextEdit()
42 | text.SetText("homo tard")
43 |
44 | box := tui.NewVBox(text)
45 |
46 | ui, err := tui.New(box)
47 | if err != nil {
48 | panic(err)
49 | }
50 |
51 | ui.SetKeybinding("Esc", func() { ui.Quit() })
52 |
53 | if err := ui.Run(); err != nil {
54 | panic(err)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/_tests/typing/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/bwmarrin/discordgo"
7 | "github.com/davecgh/go-spew/spew"
8 | )
9 |
10 | func main() {
11 | d, _ := discordgo.New(os.Args[1])
12 | if err := d.Open(); err != nil {
13 | panic(err)
14 | }
15 |
16 | defer d.Close()
17 |
18 | d.AddHandler(func(s *discordgo.Session, t *discordgo.TypingStart) {
19 | spew.Dump(t)
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/_tests/voice/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "sync"
8 |
9 | "github.com/bwmarrin/dgvoice"
10 | "github.com/bwmarrin/discordgo"
11 | "layeh.com/gopus"
12 | )
13 |
14 | func main() {
15 | d, _ := discordgo.New(os.Args[1])
16 | if err := d.Open(); err != nil {
17 | panic(err)
18 | }
19 |
20 | defer d.Close()
21 |
22 | c, err := d.State.Channel(os.Args[2])
23 | if err != nil {
24 | panic(err)
25 | }
26 |
27 | dgv, err := d.ChannelVoiceJoin(
28 | c.GuildID, c.ID, false, false,
29 | )
30 |
31 | defer dgv.Close()
32 |
33 | if err != nil {
34 | panic(err)
35 | }
36 |
37 | //dgv.AddHandler(func(vc *discordgo.VoiceConnection, vs *discordgo.VoiceSpeakingUpdate) {
38 | //spew.Dump(vc, vs)
39 | //})
40 |
41 | recv := make(chan *discordgo.Packet, 2)
42 | go ReceivePCM(dgv, recv)
43 |
44 | send := make(chan []int16, 2)
45 | go dgvoice.SendPCM(dgv, send)
46 |
47 | dgv.Speaking(true)
48 | defer dgv.Speaking(false)
49 |
50 | for {
51 | p, ok := <-recv
52 | if !ok {
53 | return
54 | }
55 |
56 | send <- p.PCM
57 | }
58 |
59 | //portaudio.Initialize()
60 | //defer portaudio.Terminate()
61 |
62 | //out := make([]int16, 960)
63 |
64 | //stream, err := portaudio.OpenDefaultStream(0, 2, 48000, len(out), &out)
65 | //if err != nil {
66 | //panic(err)
67 | //}
68 |
69 | //defer stream.Close()
70 |
71 | //if err := stream.Start(); err != nil {
72 | //panic(err)
73 | //}
74 |
75 | //defer stream.Stop()
76 |
77 | //recv := make(chan *discordgo.Packet, 2)
78 | //go dgvoice.ReceivePCM(dgv, recv)
79 |
80 | //for {
81 | //p, ok := <-recv
82 | //if !ok {
83 | //return
84 | //}
85 |
86 | //log.Println(len(p.PCM))
87 |
88 | //copy(out, p.PCM)
89 |
90 | //if err := stream.Write(); err != nil {
91 | //panic(err)
92 | //}
93 | //}
94 | }
95 |
96 | var (
97 | speakers map[uint32]*gopus.Decoder
98 | opusEncoder *gopus.Encoder
99 | mu sync.Mutex
100 | )
101 |
102 | func ReceivePCM(v *discordgo.VoiceConnection, c chan *discordgo.Packet) {
103 | if c == nil {
104 | return
105 | }
106 |
107 | var err error
108 |
109 | for {
110 | if v.Ready == false || v.OpusRecv == nil {
111 | OnError(fmt.Sprintf("Discordgo not to receive opus packets. %+v : %+v", v.Ready, v.OpusSend), nil)
112 | return
113 | }
114 |
115 | log.Println("I'm desperate")
116 |
117 | p, ok := <-v.OpusRecv
118 | if !ok {
119 | log.Println("Closed ch")
120 | return
121 | }
122 |
123 | log.Println("Received")
124 |
125 | if speakers == nil {
126 | speakers = make(map[uint32]*gopus.Decoder)
127 | }
128 |
129 | _, ok = speakers[p.SSRC]
130 | if !ok {
131 | speakers[p.SSRC], err = gopus.NewDecoder(48000, 2)
132 | if err != nil {
133 | OnError("error creating opus decoder", err)
134 | continue
135 | }
136 | }
137 |
138 | p.PCM, err = speakers[p.SSRC].Decode(p.Opus, 960, false)
139 | if err != nil {
140 | OnError("Error decoding opus data", err)
141 | continue
142 | }
143 |
144 | c <- p
145 | }
146 | }
147 |
148 | func OnError(str string, err error) {
149 | println(str)
150 | panic(err)
151 | }
152 |
--------------------------------------------------------------------------------
/_tests/voice/test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "github.com/bwmarrin/discordgo"
7 | "gopkg.in/hraban/opus.v2"
8 | "os"
9 | "path"
10 | "strconv"
11 | "time"
12 | )
13 |
14 | type recordedUser struct {
15 | user *discordgo.User
16 | ssrc uint32
17 | decoder *opus.Decoder
18 | currentFilePath string
19 | fileOpened time.Time
20 | file *os.File
21 | lastTimestamp uint32 // raw timestamp - sample no.
22 | baseTimestamp timestamp
23 | }
24 |
25 | type timestamp struct {
26 | remoteTimestamp uint32 // first raw timestamp received
27 | localTimestamp int64 // time when we received first frame in milliseconds
28 | }
29 |
30 | type mixData struct {
31 | pcm []int16
32 | remotePacketTimestamp uint32
33 | baseTimestamp timestamp
34 | }
35 |
36 | type UserMap map[uint32]*recordedUser
37 |
38 | const frameSize = 2 * 20 * 48000 / 1000
39 | const timeoutFramesCount = (2 * 60 * 1000) / 20
40 |
41 | var silence = []byte{0xF8, 0xFF, 0xFE}
42 |
43 | func (bot *Bot) StartRecording() (chan bool, error) {
44 | channel, err := bot.Session.Channel(bot.Config.RecordedChannel)
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | closeChan := make(chan bool)
50 | go recordChannel(channel, closeChan)
51 |
52 | return closeChan, err
53 | }
54 |
55 | /*func mixUsers(input chan mixData, close chan bool) {
56 | var startTime int64 = 0
57 | buffer := make([]int16, frameSize*100) // 2 seconds buffer
58 |
59 | mixFile, err := os.OpenFile("mix.pcm", os.O_RDWR|os.O_CREATE, 0755)
60 | if err != nil {
61 | return
62 | }
63 |
64 | for {
65 | select {
66 | case data := <-input:
67 | localPacketTimestamp := data.baseTimestamp.localTimestamp + int64(((data.remotePacketTimestamp - data.baseTimestamp.remoteTimestamp) / 960) * 20)
68 |
69 | if startTime == 0 {
70 | startTime = data.baseTimestamp.localTimestamp
71 | buffer = append(buffer, data.pcm...)
72 |
73 | startTime += 20
74 | }
75 |
76 | if localPacketTimestamp >= startTime
77 | case <-close:
78 | mixFile.Close()
79 | return
80 | }
81 | }
82 | }*/
83 |
84 | func recordChannel(channel *discordgo.Channel, closeChan chan bool) {
85 | //mix := make(chan mixData)
86 | users := make(UserMap)
87 |
88 | /*log, err := os.OpenFile("packetlog2.csv", os.O_RDWR|os.O_CREATE, 0755)
89 | if err != nil {
90 | return
91 | }
92 |
93 | log.Write([]byte("Timestamp;SSRC;Sequence\n"))*/
94 |
95 | pcm := make([]int16, frameSize)
96 |
97 | voice, err := bot.Session.ChannelVoiceJoin(channel.GuildID, channel.ID, true, false)
98 | if err != nil {
99 | return
100 | }
101 |
102 | voice.AddHandler(func(vc *discordgo.VoiceConnection, vs *discordgo.VoiceSpeakingUpdate) {
103 | user, err := bot.Session.User(vs.UserID)
104 | if err != nil {
105 | return
106 | }
107 |
108 | if rec, exists := users[uint32(vs.SSRC)]; !exists {
109 | rec, err := initUser(uint32(vs.SSRC), user)
110 | if err != nil {
111 | return
112 | }
113 |
114 | users[uint32(vs.SSRC)] = rec
115 | } else if rec.user == nil {
116 | err = rec.setUser(user)
117 | if err != nil {
118 | return
119 | }
120 | }
121 | })
122 |
123 | for i := 0; i < 10; i++ {
124 | voice.OpusSend <- silence
125 | }
126 |
127 | for {
128 | select {
129 | case packet := <-voice.OpusRecv:
130 | //log.Write([]byte(fmt.Sprintf("%d;%d;%d\n", packet.Timestamp, packet.SSRC, packet.Sequence)))
131 |
132 | // throw away silence
133 | if bytes.Equal(packet.Opus, silence) {
134 | continue
135 | }
136 |
137 | if _, exists := users[packet.SSRC]; !exists {
138 | rec, err := initUser(packet.SSRC, nil)
139 | if err != nil {
140 | return
141 | }
142 |
143 | users[packet.SSRC] = rec
144 | }
145 |
146 | user := users[packet.SSRC]
147 |
148 | if user.lastTimestamp != 0 {
149 | silentFrames := (packet.Timestamp - user.lastTimestamp) / 960
150 |
151 | if silentFrames < timeoutFramesCount {
152 | for i := uint32(0); i < silentFrames-1; i++ {
153 | binary.Write(user.file, binary.LittleEndian, new([frameSize]byte))
154 | }
155 | } else {
156 | user.openFile()
157 | }
158 | } else {
159 | user.baseTimestamp.remoteTimestamp = packet.Timestamp
160 | user.baseTimestamp.localTimestamp = time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
161 | }
162 |
163 | user.lastTimestamp = packet.Timestamp
164 |
165 | _, err := user.decoder.Decode(packet.Opus, pcm)
166 | if err != nil {
167 | return
168 | }
169 |
170 | //mix <- mixData{pcm: pcm, baseTimestamp: user.baseTimestamp, remotePacketTimestamp: packet.Timestamp}
171 |
172 | if time.Now().UTC().After(user.fileOpened.Add(1 * time.Hour)) {
173 | user.openFile()
174 | }
175 |
176 | err = binary.Write(user.file, binary.LittleEndian, pcm)
177 | if err != nil {
178 | return
179 | }
180 |
181 | case <-closeChan:
182 | voice.Close()
183 |
184 | //log.Close()
185 |
186 | for _, user := range users {
187 | err := user.file.Close()
188 | if err != nil {
189 | return
190 | }
191 | }
192 |
193 | return
194 | }
195 | }
196 | }
197 |
198 | func initUser(ssrc uint32, user *discordgo.User) (*recordedUser, error) {
199 | var newUser = recordedUser{ssrc: ssrc, user: user}
200 |
201 | err = newUser.openFile()
202 | if err != nil {
203 | return nil, err
204 | }
205 |
206 | newUser.decoder, err = opus.NewDecoder(48000, 2)
207 | if err != nil {
208 | return nil, err
209 | }
210 |
211 | return &newUser, nil
212 | }
213 |
214 | func (ru *recordedUser) openFile() error {
215 | if ru.file != nil {
216 | err = ru.file.Close()
217 | if err != nil {
218 | return err
219 | }
220 | }
221 |
222 | var identifier string
223 |
224 | if ru.user == nil {
225 | identifier = strconv.Itoa(int(ru.ssrc))
226 | } else {
227 | identifier = ru.user.Username
228 | }
229 |
230 | ru.currentFilePath = path.Clean(path.Join(bot.Config.OutputPath, identifier+"_"+time.Now().UTC().Format("2006-01-02_15-04-05")) + ".pcm")
231 | ru.file, err = os.OpenFile(ru.currentFilePath, os.O_RDWR|os.O_CREATE, 0755)
232 | ru.fileOpened = time.Now().UTC()
233 |
234 | return err
235 | }
236 |
237 | func (ru *recordedUser) setUser(user *discordgo.User) error {
238 | ru.user = user
239 |
240 | err = ru.file.Close()
241 | if err != nil {
242 | return err
243 | }
244 |
245 | newPath := path.Clean(path.Join(bot.Config.OutputPath, user.Username+"_"+time.Now().UTC().Format("2006-01-02_15-04-05")) + ".pcm")
246 |
247 | err = os.Rename(ru.currentFilePath, newPath)
248 | if err != nil {
249 | return err
250 | }
251 |
252 | ru.currentFilePath = newPath
253 | ru.file, err = os.OpenFile(ru.currentFilePath, os.O_RDWR|os.O_CREATE, 0755)
254 |
255 | return err
256 | }
257 |
--------------------------------------------------------------------------------
/antitele/antitele.go:
--------------------------------------------------------------------------------
1 | // Package antitele attempts to insert invisible runes into messages, so that
2 | // collected telemetric data aren't usable for selling to companies, protecting
3 | // privacy
4 | package antitele
5 |
6 | import (
7 | "math/rand"
8 | "strings"
9 | "time"
10 | "unicode"
11 | )
12 |
13 | // Probability modifies the probability of an invisible character appearing.
14 | // The lower this is, the less visible characters you can send.
15 | // Rules: [0, n) 0 > n >= +Inf
16 | var Probability = 5
17 |
18 | // ZeroWidthRunes is the array containing all invisible runes.
19 | // U+200B is used for obfuscating.
20 | var ZeroWidthRunes = []rune{
21 | '\u200b', '\u200c', '\u200d', '\ufeff',
22 | }
23 |
24 | func init() {
25 | rand.Seed(time.Now().UnixNano())
26 | }
27 |
28 | // Insert works its magic
29 | func Insert(s string) string {
30 | var words = strings.Fields(s)
31 |
32 | var amount = max(0, 2048-len(s))
33 | var needle int
34 |
35 | for i, w := range words {
36 | needle++
37 | if amount < needle {
38 | break
39 | }
40 |
41 | // Skip over links for them to be clickable
42 | if strings.HasPrefix(w, "http") {
43 | continue
44 | }
45 |
46 | // Skip if it's not a word
47 | if strings.IndexFunc(w, func(c rune) bool {
48 | return !unicode.IsLetter(c)
49 | }) != -1 {
50 | continue
51 | }
52 |
53 | // Words too short probably doens't need
54 | // to be obfuscated
55 | if len(w) < 3 {
56 | continue
57 | }
58 |
59 | words[i] = obf(w)
60 | }
61 |
62 | return strings.Join(words, " ")
63 | }
64 |
65 | func obf(s string) string {
66 | var r = []rune(s)
67 | var i = rand.Intn(len(r))
68 |
69 | return string(r[:i]) + "\u200b" + string(r[i:])
70 | }
71 |
72 | func max(i, j int) int {
73 | if i > j {
74 | return i
75 | }
76 |
77 | return j
78 | }
79 |
80 | func containsRunes(s string, trs ...rune) bool {
81 | var i int
82 | for _, r := range []rune(s) {
83 | for _, tr := range trs {
84 | if tr == r {
85 | i++
86 | }
87 | }
88 | }
89 |
90 | return i == len(trs)
91 | }
92 |
--------------------------------------------------------------------------------
/center/center.go:
--------------------------------------------------------------------------------
1 | package center
2 |
3 | import "github.com/diamondburned/tview/v2"
4 |
5 | type Center struct {
6 | MaxWidth int
7 | MaxHeight int
8 |
9 | x, y int
10 | w, h int
11 |
12 | tview.Primitive
13 | }
14 |
15 | var _ tview.Primitive = (*Center)(nil)
16 |
17 | func New(p tview.Primitive) *Center {
18 | return &Center{
19 | Primitive: p,
20 | }
21 | }
22 |
23 | // GetRect overrides the embedded Primitive's GetRect method.
24 | func (c *Center) GetRect() (int, int, int, int) {
25 | return c.x, c.y, c.w, c.h
26 | }
27 |
28 | // SetRect overrides the embedded Primitive's SetRect method.
29 | func (c *Center) SetRect(x, y, w, h int) {
30 | c.x, c.y = x, y
31 | c.w, c.h = w, h
32 |
33 | // Get the default primitive positions and sizes
34 | var (
35 | pW, pH = w, h
36 | pX, pY = x, y
37 | )
38 |
39 | // If the height is bigger than the max height
40 | if c.MaxHeight > 0 && h > c.MaxHeight {
41 | pH = c.MaxHeight // min
42 | pY = y + (h-c.MaxHeight)/2 // also center the primitive
43 | }
44 |
45 | // If the width is bigger than the max width
46 | if c.MaxWidth > 0 && w > c.MaxWidth {
47 | pW = c.MaxWidth // min
48 | pX = x + (w-c.MaxWidth)/2 // center
49 | }
50 |
51 | c.Primitive.SetRect(pX, pY, pW, pH)
52 | }
53 |
--------------------------------------------------------------------------------
/clipboard.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | // IsFile checks if f clipboard is plain text
8 | func IsFile(b []byte) bool {
9 | return http.DetectContentType(b) != "text/plain; charset=utf-8"
10 | }
11 |
--------------------------------------------------------------------------------
/command.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | var (
9 | senderRegex = strings.NewReplacer()
10 | cmdHistory = make([]string, 0, 256)
11 | )
12 |
13 | // Commands contains multiple commands
14 | type Commands []Command
15 |
16 | // Command contains a command's info
17 | type Command struct {
18 | Command string
19 | Function func([]string)
20 | Description string
21 | }
22 |
23 | var commands = Commands{
24 | Command{
25 | Command: "/goto",
26 | Function: gotoChannel,
27 | Description: "[channel name] - jumps to a channel",
28 | },
29 | Command{
30 | Command: "/editor",
31 | Function: commandEditor,
32 | Description: "Pop up $EDITOR to send a message ",
33 | },
34 | Command{
35 | Command: "/mentions",
36 | Function: commandMentions,
37 | Description: "shows the last mentions",
38 | },
39 | Command{
40 | Command: "/nick",
41 | Function: changeSelfNick,
42 | Description: "[nickname] - changes nickname for the current guild",
43 | },
44 | Command{
45 | Command: "/status",
46 | Function: setStatus,
47 | Description: "[online|busy|away|invisible] - sets your status",
48 | },
49 | Command{
50 | Command: "/edit",
51 | Function: editMessage,
52 | Description: "[n:int optional] - edits the latest n message of yours",
53 | },
54 | Command{
55 | Command: "/delete",
56 | Function: deleteMessage,
57 | Description: "[messageID:int] - deletes the message",
58 | },
59 | Command{
60 | Command: "/presence",
61 | Function: setGame,
62 | Description: "[string] - sets your \"Playing\" or \"Listening to\" presence, empty to reset",
63 | },
64 | Command{
65 | Command: "/react",
66 | Function: reactMessage,
67 | Description: "[messageID:int] [emoji:string] - toggle reaction on a message",
68 | },
69 | Command{
70 | Command: "/upload",
71 | Function: uploadFile,
72 | Description: "[file path] - uploads file",
73 | },
74 | Command{
75 | Command: "/heated",
76 | Function: cmdHeated,
77 | Description: "warns you when a message is sent, regardless of settings",
78 | },
79 | Command{
80 | Command: "/copy",
81 | Function: matchCopyMessage,
82 | Description: "[n:int] - copies the entire last n message",
83 | },
84 | Command{
85 | Command: "/highlight",
86 | Function: highlightMessage,
87 | Description: "[ID:int64] - highlights the message ID if possible",
88 | },
89 | Command{
90 | Command: "/dm",
91 | Function: makeDirectMessage,
92 | Description: "[@mention] - starts a new direct message",
93 | },
94 | Command{
95 | Command: "/block",
96 | Function: blockUser,
97 | Description: "[@mention] - blocks someone",
98 | },
99 | Command{
100 | Command: "/unblock",
101 | Function: unblockUser,
102 | Description: "[@mention] - unblocks someone",
103 | },
104 | Command{
105 | Command: "/copytoken",
106 | Function: commandCopyToken,
107 | Description: "prints your token",
108 | },
109 | Command{
110 | Command: "/debug",
111 | Function: commandDebug,
112 | Description: "prints extra debug info",
113 | },
114 | Command{
115 | Command: "/quit",
116 | Function: commandExit,
117 | Description: "quits ",
118 | },
119 | }
120 |
121 | func commandExit(text []string) {
122 | app.Stop()
123 | }
124 |
125 | // CommandHandler .
126 | func CommandHandler() {
127 | text := input.GetText()
128 | if text == "" {
129 | return
130 | }
131 |
132 | defer input.SetText("")
133 |
134 | if len(cmdHistory) >= 256 {
135 | cmdHistory = cmdHistory[255:]
136 | }
137 |
138 | cmdHistory = append(
139 | cmdHistory, text,
140 | )
141 |
142 | switch {
143 | case strings.HasPrefix(text, "s/"):
144 | go editMessageRegex(text)
145 |
146 | case strings.HasPrefix(text, "/"):
147 | f := strings.Fields(text)
148 | if len(f) < 0 {
149 | return
150 | }
151 |
152 | for _, cmd := range commands {
153 | if f[0] == cmd.Command && cmd.Function != nil {
154 | go func() {
155 | defer func() {
156 | if r := recover(); r != nil {
157 | Warn(fmt.Sprintf("%v", r))
158 | }
159 | }()
160 |
161 | cmd.Function(f)
162 | }()
163 |
164 | return
165 | }
166 | }
167 |
168 | fallthrough
169 | default:
170 | // Trim literal backslash, in case "\/actual message"
171 | text = strings.TrimPrefix(text, `\`)
172 |
173 | if Channel == nil {
174 | Message("You're not in a channel!")
175 | return
176 | }
177 |
178 | go func(text string) {
179 | _, err := d.ChannelMessageSend(Channel.ID, processString(text))
180 | if err != nil {
181 | Warn("Failed to send message:\n" + text + "\nError: " + err.Error())
182 | }
183 |
184 | messagesView.ScrollToEnd()
185 | app.Draw()
186 | }(text)
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/commandblock.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 | )
6 |
7 | func parseUserID(text []string) int64 {
8 | mention := text[1]
9 |
10 | mention = mention[2:]
11 | mention = mention[:len(mention)-1]
12 |
13 | id, err := strconv.ParseInt(mention, 10, 64)
14 | if err != nil {
15 | Message(err.Error())
16 | return 0
17 | }
18 |
19 | return id
20 | }
21 |
22 | func blockUser(text []string) {
23 | id := parseUserID(text)
24 | if id == 0 {
25 | return
26 | }
27 |
28 | if err := d.RelationshipUserBlock(id); err != nil {
29 | Warn(err.Error())
30 | return
31 | }
32 | }
33 |
34 | func unblockUser(text []string) {
35 | id := parseUserID(text)
36 | if id == 0 {
37 | return
38 | }
39 |
40 | if err := d.RelationshipDelete(id); err != nil {
41 | Warn(err.Error())
42 | return
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/commandcopy.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/atotto/clipboard"
7 | "github.com/diamondburned/discordgo"
8 | )
9 |
10 | func matchCopyMessage(text []string) {
11 | if len(text) != 2 {
12 | Message("Invalid args! Refer to description.")
13 | return
14 | }
15 |
16 | residue, err := strconv.Atoi(text[1])
17 | if err != nil {
18 | Message(err.Error())
19 | return
20 | }
21 |
22 | if Channel == nil {
23 | Message("You're not in a channel!")
24 | return
25 | }
26 |
27 | var message *discordgo.Message
28 |
29 | m, err := d.State.Message(Channel.ID, int64(residue))
30 | if err == nil {
31 | message = m
32 | } else {
33 | for i := len(messageStore) - 1; i >= 0; i-- {
34 | if ID := getIDfromindex(i); ID != 0 {
35 | m, err := d.State.Message(Channel.ID, ID)
36 | if err != nil {
37 | continue
38 | }
39 |
40 | if residue == 0 {
41 | message = m
42 | break
43 | }
44 |
45 | residue--
46 | }
47 | }
48 | }
49 |
50 | if message == nil {
51 | Message("Can't find any message to copy.")
52 | return
53 | }
54 |
55 | if err := clipboard.WriteAll(message.Content); err != nil {
56 | Warn(err.Error())
57 | } else {
58 | Message("Copied message from " + message.Author.Username)
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/commanddebug.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | "runtime/debug"
7 | "strings"
8 | "text/tabwriter"
9 | "time"
10 | )
11 |
12 | func commandDebug(text []string) {
13 | s := strings.Builder{}
14 | w := tabwriter.NewWriter(&s, 0, 0, 1, ' ', 0)
15 |
16 | fmt.Fprintf(w, "Channel ID:\t%d\n", Channel.ID)
17 | fmt.Fprintf(w, "Channel Icon:\t%s\n", Channel.Icon)
18 | fmt.Fprintf(w, "Guild ID:\t%d\n", Channel.GuildID)
19 |
20 | if g, _ := d.State.Guild(Channel.GuildID); g != nil {
21 | fmt.Fprintf(w,
22 | "Guild Icon:\thttps://cdn.discordapp.com/icons/%d/%s.png\n",
23 | g.ID, g.Icon,
24 | )
25 | }
26 |
27 | fmt.Fprintf(w, "Number of goroutines:\t%d\n", runtime.NumGoroutine())
28 | fmt.Fprintf(w, "GOMAXPROCS:\t%d\n", runtime.GOMAXPROCS(-1))
29 | fmt.Fprintf(w, "GOOS:\t%s\n", runtime.GOOS)
30 | fmt.Fprintf(w, "GOARCH:\t%s\n", runtime.GOARCH)
31 | fmt.Fprintf(w, "Go version:\t%s", runtime.Version())
32 |
33 | var gc = &debug.GCStats{}
34 | debug.ReadGCStats(gc)
35 |
36 | if gc != nil {
37 | fmt.Fprintf(w, "\nLast garbage collection:\t%s\n", gc.LastGC.Format(time.Kitchen))
38 | fmt.Fprintf(w, "Total garbage collection:\t%d\n", gc.NumGC)
39 | fmt.Fprintf(w, "Total pause for GC:\t%d", gc.PauseTotal)
40 | }
41 |
42 | var mem = &runtime.MemStats{}
43 | runtime.ReadMemStats(mem)
44 |
45 | if mem != nil {
46 | fmt.Fprintf(w,
47 | "\nTotal RAM usage:\t%.2f MB\n",
48 | float64(mem.Alloc)/1000000,
49 | )
50 | fmt.Fprintf(w,
51 | "Total heap allocated:\t%.2f MB\n",
52 | float64(mem.HeapAlloc)/1000000,
53 | )
54 | }
55 |
56 | if err := w.Flush(); err != nil {
57 | Warn(err.Error())
58 | return
59 | }
60 |
61 | Message(s.String())
62 |
63 | runtime.GC()
64 | }
65 |
--------------------------------------------------------------------------------
/commanddelete.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | )
7 |
8 | func deleteMessage(text []string) {
9 | toDelete := make([]int64, 0, len(text))
10 |
11 | if len(text) == 1 {
12 | lastMsg := matchMyMessage(0)
13 | if lastMsg == nil {
14 | Message("Can't find your last message :(")
15 | return
16 | }
17 |
18 | toDelete = append(toDelete, lastMsg.ID)
19 | } else {
20 | for i, a := range text[1:] {
21 | m, err := strconv.Atoi(a)
22 | if err != nil {
23 | Message(fmt.Sprintf("Failed to parse argument %d", i-1))
24 | return
25 | }
26 |
27 | lastMsg := matchMyMessage(m)
28 | if lastMsg == nil {
29 | Message("Can't find your last message :(")
30 | return
31 | }
32 |
33 | toDelete = append(toDelete, lastMsg.ID)
34 | }
35 | }
36 |
37 | for _, m := range toDelete {
38 | if err := d.ChannelMessageDelete(Channel.ID, m); err != nil {
39 | Warn(err.Error())
40 | return
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/commanddm.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | )
7 |
8 | func parseUserMention(m string) int64 {
9 | i := 0
10 | trim := strings.TrimFunc(m, func(r rune) bool {
11 | switch {
12 | case r == '<', r == '>', r == '@':
13 | i++
14 | return true
15 | default:
16 | return false
17 | }
18 | })
19 |
20 | id, _ := strconv.ParseInt(trim, 10, 64)
21 | if i != 3 {
22 | return 0
23 | }
24 |
25 | return id
26 | }
27 |
28 | func makeDirectMessage(text []string) {
29 | if len(text) != 2 {
30 | Message("No channels given!")
31 | return
32 | }
33 |
34 | id := parseUserMention(text[1])
35 | if id == 0 {
36 | Message("Invalid user mention!")
37 | return
38 | }
39 |
40 | ch, err := d.UserChannelCreate(id)
41 | if err != nil {
42 | Message(err.Error())
43 | }
44 |
45 | loadChannel(ch.ID)
46 | }
47 |
--------------------------------------------------------------------------------
/commandedit.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/csv"
5 | "fmt"
6 | "regexp"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/diamondburned/discordgo"
11 | "github.com/diamondburned/tcell"
12 | )
13 |
14 | const (
15 | // EditMessageLabel is used to detect which function to use
16 | EditMessageLabel = "Edit "
17 | )
18 |
19 | var toEditMessage int64
20 |
21 | func editMessage(text []string) {
22 | if Channel == nil {
23 | Message("You're not in a channel!")
24 | return
25 | }
26 |
27 | var messageN int
28 |
29 | if len(text) == 2 {
30 | i, err := strconv.Atoi(text[1])
31 | if err != nil {
32 | Message(err.Error())
33 | return
34 | }
35 |
36 | messageN = i
37 | }
38 |
39 | lastMsg := matchMyMessage(messageN)
40 | if lastMsg == nil {
41 | Message("Can't find your last message :(")
42 | return
43 | }
44 |
45 | toEditMessage = lastMsg.ID
46 |
47 | input.SetBackgroundColor(tcell.ColorBlue)
48 | input.SetFieldBackgroundColor(tcell.ColorBlue)
49 | input.SetPlaceholderTextColor(tcell.ColorWhite)
50 | input.SetLabel("Edit ")
51 | input.SetPlaceholder("Send this empty message to delete it")
52 | input.SetText(lastMsg.Content)
53 |
54 | messagesView.Highlight(strconv.FormatInt(lastMsg.ID, 10))
55 | }
56 |
57 | func editHandler() {
58 | var (
59 | edit = toEditMessage
60 | i = input.GetText()
61 | err error
62 | )
63 |
64 | resetInputBehavior()
65 |
66 | if i != "" {
67 | _, err = d.ChannelMessageEdit(
68 | Channel.ID, edit, processString(i),
69 | )
70 | } else {
71 | err = d.ChannelMessageDelete(
72 | Channel.ID, edit,
73 | )
74 | }
75 |
76 | if err != nil {
77 | Warn(err.Error())
78 | }
79 | }
80 |
81 | func editMessageRegex(text string) {
82 | if Channel == nil {
83 | Message("You're not in a channel!")
84 | }
85 |
86 | input := csv.NewReader(strings.NewReader(text))
87 | input.Comma = '/' // delimiter
88 | args, err := input.Read()
89 | if err != nil {
90 | Warn(err.Error())
91 | return
92 | }
93 |
94 | if len(args) != 3 && len(args) != 4 {
95 | Message(fmt.Sprintf("Invalid arguments! %d", len(args)))
96 | return
97 | }
98 |
99 | var (
100 | regexArg = args[1]
101 | withArg = args[2]
102 | messageN int
103 | )
104 |
105 | if len(args) == 4 {
106 | order := args[3]
107 |
108 | if order != "" && order != "g" {
109 | messageN, _ = strconv.Atoi(order)
110 | }
111 | }
112 |
113 | regex, err := regexp.Compile(regexArg)
114 | if err != nil {
115 | Message(err.Error())
116 | return
117 | }
118 |
119 | lastMsg := matchMyMessage(messageN)
120 | if lastMsg == nil {
121 | Message("Can't find your last message :(")
122 | return
123 | }
124 |
125 | repl := regex.ReplaceAllString(lastMsg.Content, withArg)
126 |
127 | _, err = d.ChannelMessageEdit(
128 | lastMsg.ChannelID,
129 | lastMsg.ID,
130 | repl,
131 | )
132 |
133 | if err != nil {
134 | Warn(err.Error())
135 | }
136 | }
137 |
138 | func matchMyMessage(residue int) *discordgo.Message {
139 | m, err := d.State.Message(Channel.ID, int64(residue))
140 | if err == nil {
141 | return m
142 | }
143 |
144 | for i := len(messageStore) - 1; i >= 0; i-- {
145 | if ID := getIDfromindex(i); ID != 0 {
146 | m, err := d.State.Message(Channel.ID, ID)
147 | if err != nil {
148 | continue
149 | }
150 |
151 | if m.Author.ID == d.State.User.ID {
152 | if residue == 0 {
153 | return m
154 | }
155 |
156 | residue--
157 | }
158 | }
159 | }
160 |
161 | return nil
162 | }
163 |
--------------------------------------------------------------------------------
/commandeditor.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | func commandEditor(text []string) {
4 | if Channel == nil {
5 | Message("You're not in a channel!")
6 | return
7 | }
8 |
9 | b, err := summonEditor()
10 | if err != nil {
11 | Warn(err.Error())
12 | return
13 | }
14 |
15 | if _, err := d.ChannelMessageSend(Channel.ID, string(b)); err != nil {
16 | Warn(err.Error())
17 | return
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/commandgoto.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 |
7 | "github.com/diamondburned/discordgo"
8 | "github.com/diamondburned/tview/v2"
9 | )
10 |
11 | func parseChannelID(input string) int64 {
12 | chID := strings.TrimSpace(input)
13 |
14 | chID = chID[2:]
15 | chID = chID[:len(chID)-1]
16 |
17 | id, err := strconv.ParseInt(chID, 10, 64)
18 | if err != nil {
19 | Message(err.Error())
20 | return 0
21 | }
22 |
23 | return id
24 | }
25 |
26 | func gotoChannel(text []string) {
27 | if len(text) != 2 {
28 | Message("No channels given!")
29 | return
30 | }
31 |
32 | var id int64
33 |
34 | switch {
35 | case strings.HasPrefix(text[1], "<#"):
36 | id = parseChannelID(text[1])
37 | case strings.HasPrefix(text[1], "<@"):
38 | ch, err := d.UserChannelCreate(parseUserMention(text[1]))
39 | if err != nil {
40 | Warn(err.Error())
41 | return
42 | }
43 |
44 | id = ch.ID
45 | }
46 |
47 | if id == 0 {
48 | Message("No channels given!")
49 | return
50 | }
51 |
52 | go func() {
53 | root := guildView.GetRoot()
54 | if root == nil {
55 | return
56 | }
57 |
58 | root.Walk(func(node, parent *tview.TreeNode) bool {
59 | if parent == nil {
60 | CollapseAll(node)
61 | return true
62 | }
63 |
64 | refr, ok := node.GetReference().(*discordgo.Channel)
65 | if !ok {
66 | return true
67 | }
68 |
69 | if id != refr.ID {
70 | return false
71 | }
72 |
73 | node.Expand()
74 | parent.Expand()
75 | guildView.SetCurrentNode(node)
76 |
77 | return false
78 | })
79 | }()
80 |
81 | resetInputBehavior()
82 |
83 | loadChannel(id)
84 | }
85 |
--------------------------------------------------------------------------------
/commandheated.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | func cmdHeated(text []string) {
4 | if Channel == nil {
5 | Message("You're not in a channel!")
6 | return
7 | }
8 |
9 | if heatedChannelsToggle(Channel.ID) {
10 | Message("Added this channel. We'll warn you when there's a message.")
11 | } else {
12 | Message("Removed this channel.")
13 | }
14 |
15 | // Heated servers are checked in notify.go
16 | // Check function is in heated.go
17 | }
18 |
--------------------------------------------------------------------------------
/commandmentions.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 | "time"
6 |
7 | "github.com/diamondburned/tview/v2"
8 | )
9 |
10 | func commandMentions(text []string) {
11 | input.SetPlaceholder("Loading mentions...")
12 | defer input.SetPlaceholder(cfg.Prop.DefaultStatus)
13 |
14 | mentions, err := getMentions()
15 | if err != nil {
16 | Warn(err.Error())
17 | return
18 | }
19 |
20 | Channel = nil
21 | messageStore = []string{}
22 | messagesView.Clear()
23 |
24 | for i := len(mentions) - 1; i >= 0; i-- {
25 | m := mentions[i]
26 |
27 | username, color := us.DiscordThis(m)
28 |
29 | sentTime, err := m.Timestamp.Parse()
30 | if err != nil {
31 | sentTime = time.Now()
32 | }
33 |
34 | messagesView.Write([]byte(
35 | authorTmpl.ExecuteString(map[string]interface{}{
36 | "color": fmtHex(color),
37 | "name": tview.Escape(username),
38 | "time": sentTime.Format(time.Stamp),
39 | }),
40 | ))
41 |
42 | messagesView.Write([]byte(
43 | messageTmpl.ExecuteString(map[string]interface{}{
44 | "ID": strconv.FormatInt(m.ID, 10),
45 | "content": fmtMessage(m),
46 | }),
47 | ))
48 | }
49 |
50 | wrapFrame.SetTitle("[Mentions[]")
51 | input.SetPlaceholder("Done.")
52 | app.Draw()
53 |
54 | time.Sleep(time.Second * 5)
55 | }
56 |
--------------------------------------------------------------------------------
/commandmessages.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | func highlightMessage(text []string) {
4 | if len(text) != 2 {
5 | messagesView.Highlight()
6 | return
7 | }
8 |
9 | messagesView.Highlight(text[1])
10 | messagesView.ScrollToHighlight()
11 | }
12 |
--------------------------------------------------------------------------------
/commandnick.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "strings"
4 |
5 | func changeSelfNick(text []string) {
6 | if Channel == nil {
7 | Message("You're not in a channel!")
8 | return
9 | }
10 |
11 | if len(text) < 2 {
12 | Message("Missing nickname argument!")
13 | return
14 | }
15 |
16 | nickname := strings.Join(text[1:], " ")
17 |
18 | ch, err := d.State.Channel(Channel.ID)
19 | if err != nil {
20 | Warn(err.Error())
21 | return
22 | }
23 |
24 | if ch.GuildID == 0 {
25 | Message("You can't set a nickname in a DM")
26 | return
27 | }
28 |
29 | err = d.GuildMemberNicknameMe(
30 | ch.GuildID,
31 | nickname,
32 | )
33 |
34 | if err != nil {
35 | Message(err.Error())
36 | return
37 | }
38 |
39 | Message("Changed successfully")
40 |
41 | go func() {
42 | i, u := us.GetUser(ch.GuildID, d.State.User.ID)
43 | if u != nil {
44 | us.Guilds[ch.GuildID][i].Nick = nickname
45 | }
46 | }()
47 | }
48 |
--------------------------------------------------------------------------------
/commandreact.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | )
7 |
8 | func reactMessage(text []string) {
9 | if Channel == nil {
10 | Message("You're not in a channel!")
11 | return
12 | }
13 |
14 | if len(text) < 3 {
15 | Message("Invalid arguments! Refer to description.")
16 | return
17 | }
18 |
19 | messageID, err := strconv.ParseInt(text[1], 10, 64)
20 | if err != nil {
21 | Message("Failed to find the message.")
22 | return
23 | }
24 |
25 | message, err := d.State.Message(Channel.ID, messageID)
26 | if err != nil {
27 | Message("Failed to find the message.")
28 | return
29 | }
30 |
31 | var (
32 | emoji = make([]string, len(text)-2)
33 | reacted = make([]bool, len(text)-2)
34 | )
35 |
36 | for i := 0; i < len(text)-2; i++ {
37 | regres := EmojiRegex.FindAllStringSubmatch(text[i+2], -1)
38 | if len(regres) > 0 && len(regres[0]) == 4 {
39 | emoji[i] = regres[0][2] + ":" + regres[0][3]
40 |
41 | for _, r := range message.Reactions {
42 | if r.Emoji == nil {
43 | continue
44 | }
45 |
46 | if strconv.FormatInt(r.Emoji.ID, 10) == regres[0][3] {
47 | reacted[i] = r.Me
48 | break
49 | }
50 | }
51 | } else {
52 | emoji[i] = strings.TrimSpace(text[i+2])
53 |
54 | for _, r := range message.Reactions {
55 | if r.Emoji == nil {
56 | continue
57 | }
58 |
59 | if r.Emoji.Name == text[i+2] {
60 | reacted[i] = r.Me
61 | break
62 | }
63 | }
64 | }
65 | }
66 |
67 | for i := 0; i < len(emoji); i++ {
68 | if reacted[i] {
69 | err = d.MessageReactionRemoveMe(
70 | Channel.ID,
71 | message.ID,
72 | emoji[i],
73 | )
74 | } else {
75 | err = d.MessageReactionAdd(
76 | Channel.ID,
77 | message.ID,
78 | emoji[i],
79 | )
80 | }
81 |
82 | if err != nil {
83 | Warn(err.Error())
84 | return
85 | }
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/commandsixel.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "image"
7 | "net/http"
8 | "os"
9 |
10 | _ "image/gif"
11 | _ "image/jpeg"
12 | _ "image/png"
13 |
14 | "github.com/disintegration/imaging"
15 | "github.com/mattn/go-sixel"
16 | )
17 |
18 | var redrawDisabled bool
19 |
20 | func commandSixelTest(text []string) {
21 | r, err := http.Get(text[1])
22 | if err != nil {
23 | Warn(err.Error())
24 | return
25 | }
26 |
27 | defer r.Body.Close()
28 |
29 | img, _, err := image.Decode(r.Body)
30 | if err != nil {
31 | Warn(err.Error())
32 | return
33 | }
34 |
35 | if img.Bounds().Dx() > 200 {
36 | img = imaging.Resize(img, 200, 0, imaging.Linear)
37 | }
38 |
39 | var b bytes.Buffer
40 |
41 | enc := sixel.NewEncoder(&b)
42 | enc.Dither = false
43 |
44 | if err := enc.Encode(img); err != nil {
45 | Warn(err.Error())
46 | return
47 | }
48 |
49 | redrawDisabled = true
50 | fmt.Fprint(os.Stdout, string(b.Bytes()))
51 | }
52 |
--------------------------------------------------------------------------------
/commandstatus.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/diamondburned/discordgo"
7 | )
8 |
9 | func setStatus(input []string) {
10 | if d.State.Settings == nil {
11 | Message("Settings are uninitialized")
12 | return
13 | }
14 |
15 | s := d.State.Settings.Status
16 |
17 | if len(input) < 2 {
18 | switch s {
19 | case discordgo.StatusOnline:
20 | Message("Status: Online")
21 | case discordgo.StatusIdle:
22 | Message("Status: Idle")
23 | case discordgo.StatusDoNotDisturb:
24 | Message("Status: Do not disturb")
25 | case discordgo.StatusInvisible:
26 | Message("Status: Invisible")
27 | default:
28 | Message(string(s))
29 | }
30 |
31 | return
32 | }
33 |
34 | switch strings.Join(input[1:], " ") {
35 | case string(discordgo.StatusOnline), "Online":
36 | s = discordgo.StatusOnline
37 |
38 | case string(discordgo.StatusIdle), "Idle",
39 | "Away", "away":
40 | s = discordgo.StatusIdle
41 |
42 | case string(discordgo.StatusDoNotDisturb),
43 | "do not disturb", "Do not disturb", "Do Not Disturb",
44 | "Busy", "busy":
45 | s = discordgo.StatusDoNotDisturb
46 |
47 | case string(discordgo.StatusInvisible), "invis", "Invisible":
48 | s = discordgo.StatusInvisible
49 |
50 | default:
51 | Message("Unknown status to set, check description")
52 | return
53 | }
54 |
55 | if _, err := d.UserUpdateStatus(s); err != nil {
56 | Warn(err.Error())
57 | return
58 | }
59 |
60 | Message("Set status to " + string(s))
61 | }
62 |
63 | func setListen(text []string) {
64 | if len(text) < 2 {
65 | Message("Missing string!")
66 | return
67 | }
68 |
69 | s := strings.Join(text[1:], " ")
70 |
71 | if err := d.UpdateListeningStatus(s); err != nil {
72 | Message(err.Error())
73 | } else {
74 | Message("Set listening status to " + s)
75 | }
76 | }
77 |
78 | func setGame(text []string) {
79 | var s string
80 | if len(text) > 1 {
81 | s = strings.Join(text[1:], " ")
82 | }
83 |
84 | var (
85 | msg string
86 | gametype = discordgo.GameTypeGame
87 | )
88 |
89 | switch {
90 | case strings.HasPrefix(strings.ToLower(s), "listening to "):
91 | s = s[13:]
92 | gametype = discordgo.GameTypeListening
93 | msg = "Set listening to "
94 | case strings.HasPrefix(strings.ToLower(s), "watching "):
95 | s = s[9:]
96 | gametype = discordgo.GameTypeWatching
97 | msg = "Set watching "
98 | default:
99 | msg = "Set game to "
100 | }
101 |
102 | usd := discordgo.UpdateStatusData{
103 | Status: string(d.State.Settings.Status),
104 | Game: &discordgo.Game{
105 | Name: s,
106 | Type: gametype,
107 | },
108 | }
109 |
110 | if err := d.UpdateStatusComplex(usd); err != nil {
111 | Message(err.Error())
112 | return
113 | }
114 |
115 | if s != "" {
116 | Message(msg + s + ".")
117 | } else {
118 | Message("Reset presence successfully.")
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/commandtoken.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/atotto/clipboard"
4 |
5 | func commandCopyToken(text []string) {
6 | if err := clipboard.WriteAll(d.Token); err != nil {
7 | Warn(err.Error())
8 | return
9 | }
10 |
11 | Message("Token copied to clipboard.")
12 | }
13 |
--------------------------------------------------------------------------------
/commandupload.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "strings"
6 | )
7 |
8 | func uploadFile(args []string) {
9 | if Channel == nil {
10 | Message("You're not in a channel!")
11 | return
12 | }
13 |
14 | if len(args) < 2 {
15 | Message("Missing file path!")
16 | return
17 | }
18 |
19 | file, err := os.Open(strings.Join(args[1:], " "))
20 | if err != nil {
21 | Warn(err.Error())
22 | return
23 | }
24 |
25 | fileparts := strings.Split(file.Name(), "/")
26 |
27 | _, err = d.ChannelFileSend(
28 | Channel.ID,
29 | fileparts[len(fileparts)-1],
30 | file,
31 | )
32 |
33 | if err != nil {
34 | Warn(err.Error())
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // CustomCommands is for user-made commands
4 | var CustomCommands = []Command{
5 | Command{
6 | Command: "/shrug",
7 | Function: cmdShrug,
8 | Description: `¯\_(ツ)_/¯`,
9 | },
10 | }
11 |
12 | // `text` is the chat argument, split into arrays.
13 | // This is done with strings.Fields(messageContent).
14 | // For shell-like argument splitting, join the array and run it through
15 | // a CSV reader, delimiter ' '.
16 | func cmdShrug(text []string) {
17 | if Channel == nil {
18 | // Error handling in case nil crashes the entire app
19 | Message("You're not in a channel!")
20 | }
21 |
22 | // Channel is a global variable indicating the current channel.
23 | // Writing to this variable will screw _everthing_ up.
24 | if _, err := d.ChannelMessageSend(Channel.ID, `¯\_(ツ)_/¯`); err != nil {
25 | Warn(err.Error())
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/configfile.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "path/filepath"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/stevenroose/gonfig"
11 | fast "github.com/valyala/fasttemplate"
12 | "gitlab.com/diamondburned/6cord/md"
13 | )
14 |
15 | var cfg Config
16 |
17 | // Properties ..
18 | type Properties struct {
19 | CompactMode bool `id:"compact-mode" default:"true" desc:"Compact Mode"`
20 | TrueColor bool `id:"true-color" default:"true" desc:"Enable True color mode instead of 256 color mode"`
21 | DefaultNameColor string `id:"default-name-color" default:"#CCCCCC" desc:"Sets the default name color, format: #XXXXXX"`
22 | MentionColor string `id:"mention-color" default:"#0D4A91" desc:"The default mention background color"`
23 | MentionSelfColor string `id:"mention-self-color" default:"#17AC86" desc:"The default mention background color, when the target is you"`
24 | ShowChannelsOnStartup bool `id:"show-channels" default:"true" desc:"Show the left channel bar on startup."`
25 | ChatPadding int `id:"chat-padding" default:"2" desc:"Determine the default indentation of messages from the left side."`
26 | SidebarRatio int `id:"sidebar-ratio" default:"3" desc:"The sidebar width in ratio of 1:N, whereas N is the ratio for the message box. The higher the number is, the narrower the sidebar is."`
27 | SidebarIndent int `id:"sidebar-indent" default:"2" desc:"Width in spaces each indentation level on the sidebar adds."`
28 | HideBlocked bool `id:"hide-blocked" default:"true" desc:"Ignore all blocked users."`
29 | TriggerTyping bool `id:"trigger-typing" default:"true" desc:"Send a TypingStart event periodically to the Discord server, default behavior of clients."`
30 | ForegroundColor int `id:"foreground-color" default:"15" desc:"Default foreground color, 0-255, 0 is black, 15 is white."`
31 | BackgroundColor int `id:"background-color" default:"-1" desc:"Acceptable values: tcell.Color*, -1, 0-255 (terminal colors)."`
32 | AuthorFormat string `id:"author-format" default:"[#{color}::b]{name}" desc:"The formatting of message authors"`
33 | CommandPrefix string `id:"command-prefix" default:"[${GUILD}${CHANNEL}] " desc:"The prefix of the input box"`
34 | DefaultStatus string `id:"default-status" default:"Send a message or input a command" desc:"The message in the status bar."`
35 | SyntaxHighlightColorscheme string `id:"syntax-highlight-colorscheme" default:"emacs" desc:"The color scheme for syntax highlighting, refer to https://xyproto.github.io/splash/docs/all.html."`
36 | ShowEmojiURLs bool `id:"show-emoji-urls" default:"true" desc:"Converts emojis into clickable URLs."`
37 | ObfuscateWords bool `id:"obfuscate-words" default:"false" desc:"Insert a zero-width space to obfuscate word-logging telemetry."`
38 | ChatMaxWidth int `id:"chat-max-width" default:"0" desc:"The maximum width of the chat box, if smaller, will be centered."`
39 | ImageFetchTimeout int `id:"image-fetch-timeout" default:"1" desc:"The timeout to fetch images, in seconds."`
40 | ImageWidth int `id:"image-width" default:"400" desc:"The maximum width for an image."`
41 | ImageHeight int `id:"image-height" default:"400" desc:"The maximum height for an image."`
42 | ShortenURL bool `id:"shorten-url" default:"true" desc:"Opens a webserver to redirect URLs"`
43 | RPCServer bool `id:"rpc-server" default:"true" desc:"Start a Rich Presence server for applications to use. Experimental. Source: https://gitlab.com/diamondburned/drpc-server"`
44 | }
45 |
46 | type Config struct {
47 | Username string `id:"username" short:"u" default:"" desc:"Used when token is empty, avoid if 2FA"`
48 | Password string `id:"password" short:"p" default:"" desc:"Used when token is empty"`
49 | Token string `id:"token" short:"t" default:"" desc:"Authentication Token, recommended way of using"`
50 |
51 | Login bool `id:"login" short:"l" default:"false" desc:"Force pop up a login prompt"`
52 |
53 | Prop Properties `id:"properties"`
54 |
55 | Debug bool `id:"debug" short:"d" default:"false" desc:"Enables debug mode"`
56 |
57 | Config string `short:"c"`
58 | }
59 |
60 | var (
61 | authorRawFormat string
62 | authorPrefix string
63 | messageRawFormat string
64 |
65 | // color, name, time
66 | authorTmpl *fast.Template
67 |
68 | // ID, content
69 | messageTmpl *fast.Template
70 |
71 | chatPadding string
72 |
73 | defaultNameColor int
74 | )
75 |
76 | func loadCfg() error {
77 | // Get the XDG paths
78 | var xdg = os.Getenv("XDG_CONFIG_HOME")
79 | if xdg == "" {
80 | if h, err := os.UserHomeDir(); err == nil {
81 | xdg = filepath.Join(h, ".config")
82 | }
83 | }
84 |
85 | err := gonfig.Load(&cfg, gonfig.Conf{
86 | ConfigFileVariable: "config",
87 | FileDefaultFilename: filepath.Join(xdg, "6cord", "6cord.toml"),
88 | FileDecoder: gonfig.DecoderTOML,
89 | EnvPrefix: "sixcord_",
90 | })
91 |
92 | if err != nil {
93 | return err
94 | }
95 |
96 | if cfg.Config != "" {
97 | return gonfig.Load(&cfg, gonfig.Conf{
98 | ConfigFileVariable: "config",
99 | FileDefaultFilename: cfg.Config,
100 | FileDecoder: gonfig.DecoderTOML,
101 | EnvPrefix: "sixcord_",
102 | })
103 | }
104 |
105 | if cfg.Prop.TrueColor {
106 | term := os.Getenv("TERM")
107 | if strings.Contains(term, "-") {
108 | os.Setenv("TERM", strings.Split(term, "-")[0]+"-truecolor")
109 | }
110 | }
111 |
112 | hex := cfg.Prop.DefaultNameColor
113 | if len(hex) < 6 {
114 | return errors.New("Invalid format for name color, refer to help")
115 | }
116 |
117 | if hex[0] == '#' {
118 | hex = hex[1:]
119 | }
120 |
121 | hex64, err := strconv.ParseInt(hex, 16, 64)
122 | if err != nil {
123 | return err
124 | }
125 |
126 | defaultNameColor = int(hex64)
127 | messageRawFormat = `["{ID}"][-]{content}[-::-]["ENDMESSAGE"]`
128 |
129 | if cfg.Prop.CompactMode {
130 | messageRawFormat = " " + messageRawFormat
131 | authorPrefix = "\n[\"author\"]"
132 | authorRawFormat = authorPrefix + cfg.Prop.AuthorFormat + `[-:-:-][""]`
133 | } else {
134 | messageRawFormat = "\n" + messageRawFormat
135 | authorPrefix = "\n\n[\"author\"]"
136 | authorRawFormat = authorPrefix + cfg.Prop.AuthorFormat + ` [-:-:-][::d]{time}[::-][""]`
137 | }
138 |
139 | authorTmpl = fast.New(authorRawFormat, "{", "}")
140 | messageTmpl = fast.New(messageRawFormat, "{", "}")
141 |
142 | chatPadding = strings.Repeat(" ", cfg.Prop.ChatPadding)
143 |
144 | showChannels = cfg.Prop.ShowChannelsOnStartup
145 | md.HighlightStyle = cfg.Prop.SyntaxHighlightColorscheme
146 |
147 | return nil
148 | }
149 |
--------------------------------------------------------------------------------
/demojis/fuzzy.go:
--------------------------------------------------------------------------------
1 | package demojis
2 |
3 | import (
4 | "github.com/diamondburned/discordgo"
5 | "github.com/sahilm/fuzzy"
6 | )
7 |
8 | // Emojis contains a list of emojis
9 | // DiscordEmojis generate Discordgo emojis
10 | // with ID always being -2
11 | var Emojis, DiscordEmojis = makeArray()
12 |
13 | func makeArray() (a []string, d []*discordgo.Emoji) {
14 | a = make([]string, len(emojiCodeMap))
15 | d = make([]*discordgo.Emoji, len(emojiCodeMap))
16 |
17 | i := 0
18 | for e := range emojiCodeMap {
19 | a[i] = e
20 | d[i] = &discordgo.Emoji{
21 | ID: -2,
22 | Name: e,
23 | }
24 |
25 | i++
26 | }
27 |
28 | return
29 | }
30 |
31 | // FuzzyEmojis fuzzy searches the emojis
32 | // Argument: p == pattern
33 | func FuzzyEmojis(p string) []fuzzy.Match {
34 | return fuzzy.Find(p, Emojis)
35 | }
36 |
37 | // MatchEmoji matches a fuzzy search with the emoji
38 | func MatchEmoji(m fuzzy.Match) string {
39 | vl, ok := emojiCodeMap[m.Str]
40 | if !ok {
41 | // should never happen
42 | return ""
43 | }
44 |
45 | return vl
46 | }
47 |
48 | // GetEmojiFromKey returns "", false if emoji isn't found
49 | func GetEmojiFromKey(k string) (string, bool) {
50 | v, ok := emojiCodeMap[k]
51 | return v, ok
52 | }
53 |
--------------------------------------------------------------------------------
/docs/6cord.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diamondburned/6cord/979d7a6ace67070cc2ffa13cf35b22048da70754/docs/6cord.png
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | 6cord.diamondb.xyz
2 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
57 |
58 |
59 |
63 |
64 |
65 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/editor.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "os/exec"
8 | )
9 |
10 | func summonEditor() (b []byte, err error) {
11 | editor := os.Getenv("EDITOR")
12 | if editor == "" {
13 | editor = "nano"
14 | }
15 |
16 | f, err := ioutil.TempFile("", "6cord-editor-*.md")
17 | if err != nil {
18 | return
19 | }
20 |
21 | defer os.Remove(f.Name())
22 |
23 | for {
24 | cmd := exec.Command(editor, f.Name())
25 | cmd.Stdout = os.Stdout
26 | cmd.Stdin = os.Stdin
27 |
28 | app.Suspend(func() error {
29 | fmt.Println("Opening", editor+"...")
30 | err = cmd.Run()
31 |
32 | return nil
33 | })
34 |
35 | if err != nil {
36 | return
37 | }
38 |
39 | b, err = ioutil.ReadAll(f)
40 | if err != nil {
41 | return
42 | }
43 |
44 | if len(b) > 2000 {
45 | Warn("Content too long! The limit is 2000 characters!")
46 | }
47 |
48 | break
49 | }
50 |
51 | return
52 | }
53 |
--------------------------------------------------------------------------------
/fmtemojis.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strings"
7 |
8 | "github.com/diamondburned/tview/v2"
9 | )
10 |
11 | var (
12 | // EmojiRegex to get emoji IDs
13 | // thanks ym
14 | EmojiRegex = regexp.MustCompile(`<(a?):(.+?):(\d+)>`)
15 | )
16 |
17 | // returns map[ID][]{name, url}
18 | func parseEmojis(content string) (fmtted string, emojiMap map[string][]string) {
19 | emojiMap = make(map[string][]string)
20 | fmtted = content
21 |
22 | emojiIDs := EmojiRegex.FindAllStringSubmatch(content, -1)
23 | for _, nameandID := range emojiIDs {
24 | if len(nameandID) < 4 {
25 | continue
26 | }
27 |
28 | if _, ok := emojiMap[nameandID[3]]; !ok {
29 | var format = "png"
30 | if nameandID[1] != "" {
31 | format = "gif"
32 | }
33 |
34 | fmtted = strings.Replace(
35 | fmtted,
36 | strings.TrimSpace(nameandID[0]),
37 | ":"+nameandID[2]+":",
38 | -1,
39 | )
40 |
41 | if cfg.Prop.ShowEmojiURLs {
42 | emojiMap[nameandID[3]] = []string{
43 | tview.Escape(nameandID[2]),
44 | fmt.Sprintf(
45 | `https://cdn.discordapp.com/emojis/%s.%s`,
46 | nameandID[3], format,
47 | ),
48 | }
49 | }
50 | }
51 | }
52 |
53 | return
54 | }
55 |
--------------------------------------------------------------------------------
/fmtmessage.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/diamondburned/discordgo"
9 | "github.com/diamondburned/tview/v2"
10 | "gitlab.com/diamondburned/6cord/antitele"
11 | "gitlab.com/diamondburned/6cord/shortener"
12 | )
13 |
14 | const zeroes = "000000"
15 |
16 | func fmtHex(hex int) string {
17 | h := zeroes + strconv.FormatInt(int64(hex), 16)
18 | return h[len(h)-len(zeroes):]
19 | }
20 |
21 | func fmtMessage(m *discordgo.Message) string {
22 | ct, emojiMap := parseMessageContent(m)
23 | ct = strings.Map(func(r rune) rune {
24 | for _, z := range antitele.ZeroWidthRunes {
25 | if z == r {
26 | return -1
27 | }
28 | }
29 |
30 | return r
31 | }, ct)
32 |
33 | if m.EditedTimestamp != "" {
34 | ct += " " + readChannelColorPrefix + "(edited)[-::-]"
35 |
36 | // Prevent cases where the message is empty
37 | // " (edited)"
38 | ct = strings.TrimPrefix(ct, " ")
39 | }
40 |
41 | var (
42 | c strings.Builder
43 | l = strings.Split(ct, "\n")
44 |
45 | attachments = m.Attachments
46 | reactions, reactMap = formatReactions(m.Reactions)
47 | )
48 |
49 | if ct != "" {
50 | for i := 0; i < len(l); i++ {
51 | if !cfg.Prop.CompactMode || i != 0 {
52 | c.WriteString(chatPadding)
53 | }
54 |
55 | c.WriteString(l[i])
56 |
57 | if i != len(l)-1 {
58 | c.WriteByte('\n')
59 | }
60 | }
61 | }
62 |
63 | for k, v := range reactMap {
64 | emojiMap[k] = v
65 | }
66 |
67 | for _, arr := range emojiMap {
68 | attachments = append(
69 | attachments,
70 | &discordgo.MessageAttachment{
71 | Filename: arr[0],
72 | URL: arr[1],
73 | },
74 | )
75 | }
76 |
77 | for _, e := range m.Embeds {
78 | var embed = make([]string, 0, 5)
79 |
80 | if e.URL != "" {
81 | attachments = append(
82 | attachments,
83 | &discordgo.MessageAttachment{
84 | Filename: "EmbedURL",
85 | URL: e.URL,
86 | },
87 | )
88 | }
89 |
90 | if e.Author != nil {
91 | embed = append(
92 | embed,
93 | "[::du]"+e.Author.Name+"[::-]",
94 | )
95 |
96 | if e.Author.IconURL != "" {
97 | attachments = append(
98 | m.Attachments,
99 | &discordgo.MessageAttachment{
100 | Filename: "AuthorIcon",
101 | URL: e.Author.IconURL,
102 | },
103 | )
104 | }
105 |
106 | if e.Author.URL != "" {
107 | attachments = append(
108 | m.Attachments,
109 | &discordgo.MessageAttachment{
110 | Filename: "AuthorURL",
111 | URL: e.Author.URL,
112 | },
113 | )
114 | }
115 | }
116 |
117 | if e.Title != "" {
118 | embed = append(
119 | embed,
120 | splitEmbedLine(e.Title, "[::b]", "[#0096cf]")...,
121 | )
122 | }
123 |
124 | if e.Description != "" {
125 | var desc, emojis = parseEmojis(e.Description)
126 |
127 | embed = append(embed, splitEmbedLine(desc)...)
128 |
129 | for _, arr := range emojis {
130 | attachments = append(
131 | m.Attachments,
132 | &discordgo.MessageAttachment{
133 | Filename: arr[0],
134 | URL: arr[1],
135 | },
136 | )
137 | }
138 | }
139 |
140 | if len(e.Fields) > 0 {
141 | embed = append(embed, "")
142 |
143 | for _, f := range e.Fields {
144 | embed = append(embed,
145 | splitEmbedLine(f.Name, " [::b]")...)
146 | embed = append(embed,
147 | splitEmbedLine(f.Value, " [::d]")...)
148 | embed = append(embed, "")
149 | }
150 | }
151 |
152 | var footer []string
153 | if e.Footer != nil {
154 | footer = append(
155 | footer,
156 | "[::d]"+tview.Escape(e.Footer.Text)+"[::-]",
157 | )
158 |
159 | if e.Footer.IconURL != "" {
160 | attachments = append(
161 | m.Attachments,
162 | &discordgo.MessageAttachment{
163 | Filename: "FooterIcon",
164 | URL: e.Footer.IconURL,
165 | },
166 | )
167 | }
168 | }
169 |
170 | if e.Timestamp != "" {
171 | footer = append(
172 | footer,
173 | "[::d]"+e.Timestamp+"[::-]",
174 | )
175 | }
176 |
177 | if len(footer) > 0 {
178 | embed = append(
179 | embed,
180 | strings.Join(footer, " - "),
181 | )
182 | }
183 |
184 | //if e.Thumbnail != nil {
185 | //attachments = append(
186 | //m.Attachments,
187 | //&discordgo.MessageAttachment{
188 | //Filename: "Thumbnail",
189 | //URL: e.Thumbnail.URL,
190 | //},
191 | //)
192 | //}
193 |
194 | if e.Image != nil {
195 | attachments = append(
196 | m.Attachments,
197 | &discordgo.MessageAttachment{
198 | Filename: "Image",
199 | URL: e.Image.URL,
200 | },
201 | )
202 | }
203 |
204 | if e.Video != nil {
205 | attachments = append(
206 | m.Attachments,
207 | &discordgo.MessageAttachment{
208 | Filename: "Video",
209 | URL: e.Video.URL,
210 | },
211 | )
212 | }
213 |
214 | var embedPadding = chatPadding
215 | if len(embedPadding) > 2 {
216 | embedPadding = chatPadding[:len(chatPadding)-2]
217 | }
218 |
219 | c.WriteByte('\n')
220 |
221 | for i, l := range embed {
222 | c.WriteString(embedPadding + fmt.Sprintf("[#%06X]", e.Color) + "┃[-::] " + l)
223 |
224 | if i != len(embed)-1 {
225 | c.WriteByte('\n')
226 | }
227 | }
228 | }
229 |
230 | if len(m.Reactions) > 0 { // Reactions
231 | c.WriteString("\n" + chatPadding + chatPadding + reactions)
232 | }
233 |
234 | if len(attachments) > 0 {
235 | for _, a := range attachments {
236 | c.WriteString(fmt.Sprintf(
237 | "\n%s[::d][%s[]: %s[::-]",
238 | chatPadding,
239 | tview.Escape(a.Filename),
240 | shortener.ShortenURL(a.URL),
241 | ))
242 | }
243 | }
244 |
245 | if cfg.Prop.CompactMode {
246 | // If the message begins with a code block,
247 | // we don't want the first line of the code
248 | // block to warp in the first line.
249 | if len(m.Content) > 3 && m.Content[:3] == "```" {
250 | return "\n" + chatPadding + c.String()
251 | }
252 | }
253 |
254 | return c.String()
255 | }
256 |
--------------------------------------------------------------------------------
/fuzzy.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/diamondburned/discordgo"
5 | "github.com/diamondburned/tcell"
6 | "github.com/diamondburned/tview/v2"
7 | "github.com/sahilm/fuzzy"
8 | )
9 |
10 | func stateResetter() {
11 | channelFuzzyCache = allChannels([]fuzzyReadState{})
12 | discordEmojis = DiscordEmojis([]*discordgo.Emoji{})
13 | allMessages = make([]*tview.ListItem, 0, len(messageStore))
14 | autocomp.SetChangedFunc(nil)
15 | messagesView.Highlight()
16 |
17 | current, total := getLineStatus()
18 |
19 | // If the scroll offset is < 20
20 | if total-current < 20 {
21 | scrollChat()
22 | }
23 |
24 | if imageRendererPipeline != nil {
25 | imageRendererPipeline.clean()
26 | }
27 | }
28 |
29 | func clearList() {
30 | rightflex.ResizeItem(autocomp, 1, 1)
31 |
32 | if autocomp.GetItemCount() != 0 {
33 | autocomp.Clear()
34 | }
35 | }
36 |
37 | func formatNeedle(m fuzzy.Match) (f string) {
38 | isHL := false
39 |
40 | for i := 0; i < len(m.Str); i++ {
41 | if fuzzyHasNeedle(i, m.MatchedIndexes) {
42 | f += "[::u]" + string(m.Str[i])
43 | isHL = true
44 | } else {
45 | if isHL {
46 | f += "[::-]"
47 | }
48 |
49 | f += string(m.Str[i])
50 | }
51 | }
52 |
53 | return
54 | }
55 |
56 | func fuzzyHasNeedle(needle int, haystack []int) bool {
57 | for _, i := range haystack {
58 | if needle == i {
59 | return true
60 | }
61 | }
62 | return false
63 | }
64 |
65 | func autocompHandler(ev *tcell.EventKey) *tcell.EventKey {
66 | i := autocomp.GetCurrentItem()
67 |
68 | switch ev.Key() {
69 | case tcell.KeyDown, tcell.KeyCtrlN:
70 | if i+1 == autocomp.GetItemCount() {
71 | app.SetFocus(input)
72 | return nil
73 | }
74 |
75 | return ev
76 |
77 | case tcell.KeyUp, tcell.KeyCtrlP:
78 | if i == 0 {
79 | app.SetFocus(input)
80 | return nil
81 | }
82 |
83 | return ev
84 |
85 | case tcell.KeyLeft:
86 | imageRendererPipeline.prev()
87 | return nil
88 |
89 | case tcell.KeyRight:
90 | imageRendererPipeline.next()
91 | return nil
92 |
93 | case tcell.KeyEnter:
94 | return ev
95 | }
96 |
97 | if ev.Rune() >= 0x31 && ev.Rune() <= 0x122 {
98 | return ev
99 | }
100 |
101 | app.SetFocus(input)
102 | return nil
103 | }
104 |
105 | var autofillfunc func(i int)
106 | var onhoverfn func(i int)
107 |
--------------------------------------------------------------------------------
/fuzzychannels.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 |
8 | "github.com/diamondburned/discordgo"
9 | "github.com/diamondburned/tview/v2"
10 | "github.com/sahilm/fuzzy"
11 | )
12 |
13 | type allChannels []fuzzyReadState
14 |
15 | type fuzzyReadState struct {
16 | *discordgo.Channel
17 | Unread bool
18 | Format string
19 | }
20 |
21 | var channelFuzzyCache = allChannels([]fuzzyReadState{})
22 |
23 | // String returns the fuzzy search part of the struct
24 | func (ac allChannels) String(i int) string {
25 | if ac[i].Unread {
26 | return "[::b]#" + ac[i].Format + "[::-]"
27 | }
28 |
29 | return "[::d]#" + ac[i].Format + "[::-]"
30 | }
31 |
32 | // Len returns the length
33 | func (ac allChannels) Len() int {
34 | return len(ac)
35 | }
36 |
37 | func fuzzyChannels(last string) {
38 | var fuzzied []fuzzy.Match
39 |
40 | if len(last) > 0 {
41 | if len(channelFuzzyCache) == 0 {
42 | for _, c := range d.State.PrivateChannels {
43 | var name = c.Name
44 |
45 | if c.Name == "" {
46 | recips := make([]string, len(c.Recipients))
47 | for i, r := range c.Recipients {
48 | recips[i] = r.Username
49 | }
50 |
51 | name = HumanizeStrings(recips)
52 | }
53 |
54 | channelFuzzyCache = append(
55 | channelFuzzyCache,
56 | fuzzyReadState{c, isUnread(c), name},
57 | )
58 | }
59 |
60 | for _, g := range d.State.Guilds {
61 | for _, c := range g.Channels {
62 | if !isSendCh(c.Type) {
63 | continue
64 | }
65 |
66 | channelFuzzyCache = append(
67 | channelFuzzyCache,
68 | fuzzyReadState{
69 | c,
70 | isUnread(c),
71 | c.Name + " (" + g.Name + ")",
72 | },
73 | )
74 | }
75 | }
76 | }
77 |
78 | fuzzied = fuzzy.FindFrom(
79 | strings.TrimPrefix(last, "#"),
80 | channelFuzzyCache,
81 | )
82 |
83 | if Channel != nil {
84 | c, err := d.State.Channel(Channel.ID)
85 | if err == nil {
86 | guildID := c.GuildID
87 | sort.SliceStable(fuzzied, func(i, j int) bool {
88 | return channelFuzzyCache[fuzzied[i].Index].GuildID == guildID
89 | })
90 | }
91 | }
92 | }
93 |
94 | clearList()
95 |
96 | if len(fuzzied) > 0 {
97 | for i, fz := range fuzzied {
98 | autocomp.InsertItem(i, &tview.ListItem{fz.Str, "", 0, nil})
99 |
100 | if i == 25 {
101 | break
102 | }
103 | }
104 |
105 | rightflex.ResizeItem(autocomp, min(len(fuzzied), 10), 1)
106 |
107 | autofillfunc = func(i int) {
108 | defer stateResetter()
109 |
110 | words := strings.Fields(input.GetText())
111 |
112 | withoutlast := words[:len(words)-1]
113 | withoutlast = append(withoutlast, fmt.Sprintf(
114 | "<#%d> ", channelFuzzyCache[fuzzied[i].Index].ID,
115 | ))
116 |
117 | switch {
118 | case strings.HasPrefix(input.GetText(), "/goto "):
119 | input.SetText("")
120 | gotoChannel(withoutlast)
121 | return
122 |
123 | default:
124 | input.SetText(strings.Join(withoutlast, " "))
125 | }
126 |
127 | clearList()
128 |
129 | app.SetFocus(input)
130 | }
131 |
132 | } else {
133 | rightflex.ResizeItem(autocomp, 1, 1)
134 | }
135 |
136 | app.Draw()
137 | }
138 |
--------------------------------------------------------------------------------
/fuzzycommands.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "sort"
5 | "strings"
6 |
7 | "github.com/diamondburned/tview/v2"
8 | "github.com/sahilm/fuzzy"
9 | )
10 |
11 | // String returns the fuzzy search part of the struct
12 | func (cmds Commands) String(i int) string {
13 | return cmds[i].Command
14 | }
15 |
16 | // Len returns the length of the Emojis slice
17 | func (cmds Commands) Len() int {
18 | return len(cmds)
19 | }
20 |
21 | func fuzzyCommands(last string) {
22 | var fuzzied Commands
23 |
24 | if len(last) > 1 {
25 | results := fuzzy.FindFrom(
26 | strings.TrimPrefix(last, "/"),
27 | commands,
28 | )
29 |
30 | for i, r := range results {
31 | if i == 10 {
32 | break
33 | }
34 |
35 | fuzzied = append(
36 | fuzzied,
37 | commands[r.Index],
38 | )
39 | }
40 |
41 | sort.Slice(fuzzied, func(i, j int) bool {
42 | return len(fuzzied[i].Command) < len(fuzzied[j].Command)
43 | })
44 | } else {
45 | fuzzied = append(fuzzied, commands...)
46 | }
47 |
48 | clearList()
49 |
50 | if len(fuzzied) > 0 {
51 | for i, u := range fuzzied {
52 | autocomp.InsertItem(i, &tview.ListItem{
53 | "[::b]" + u.Command + "[::-] - " + tview.Escape(u.Description),
54 | "", 0, nil,
55 | })
56 | }
57 |
58 | rightflex.ResizeItem(autocomp, min(len(fuzzied), 10), 1)
59 |
60 | autofillfunc = func(i int) {
61 | input.SetText(fuzzied[i].Command + " ")
62 | clearList()
63 | app.SetFocus(input)
64 | }
65 |
66 | } else {
67 | rightflex.ResizeItem(autocomp, 1, 1)
68 | }
69 |
70 | app.Draw()
71 | }
72 |
--------------------------------------------------------------------------------
/fuzzyemojis.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/diamondburned/discordgo"
8 | "github.com/diamondburned/tview/v2"
9 | "github.com/sahilm/fuzzy"
10 | "gitlab.com/diamondburned/6cord/demojis"
11 | )
12 |
13 | // DiscordEmojis ..
14 | type DiscordEmojis []*discordgo.Emoji
15 |
16 | func (de DiscordEmojis) String(i int) string {
17 | return de[i].Name
18 | }
19 |
20 | func (de DiscordEmojis) Len() int {
21 | return len(de)
22 | }
23 |
24 | var discordEmojis = DiscordEmojis([]*discordgo.Emoji{})
25 |
26 | func fuzzyEmojis(last string) {
27 | var fuzzied []fuzzy.Match
28 |
29 | if len(last) > 0 && Channel != nil {
30 | if len(discordEmojis) < 1 {
31 | c, err := d.State.Channel(Channel.ID)
32 | if err != nil {
33 | return
34 | }
35 |
36 | emojis := DiscordEmojis{}
37 |
38 | g, err := d.State.Guild(c.GuildID)
39 | if err == nil {
40 | emojis = g.Emojis
41 | emojis = append(emojis, demojis.DiscordEmojis...)
42 | } else {
43 | emojis = demojis.DiscordEmojis
44 | }
45 |
46 | discordEmojis = emojis
47 | }
48 |
49 | fuzzied = fuzzy.FindFrom(
50 | strings.TrimPrefix(last, ":"),
51 | discordEmojis,
52 | )
53 | }
54 |
55 | clearList()
56 |
57 | if len(fuzzied) > 0 {
58 | for i, m := range fuzzied {
59 | autocomp.InsertItem(i, &tview.ListItem{
60 | ":" + m.Str + ":", "", 0, nil,
61 | })
62 |
63 | if i == 25 {
64 | break
65 | }
66 | }
67 |
68 | rightflex.ResizeItem(autocomp, min(len(fuzzied), 10), 1)
69 |
70 | autofillfunc = func(i int) {
71 | defer stateResetter()
72 |
73 | var (
74 | words = strings.Fields(input.GetText())
75 | emoji = discordEmojis[fuzzied[i].Index]
76 | insert string
77 | )
78 |
79 | if emoji.ID == -2 {
80 | e, ok := demojis.GetEmojiFromKey(emoji.Name)
81 | if ok {
82 | insert = e
83 | }
84 | } else {
85 | var a string
86 | if emoji.Animated {
87 | a = "a"
88 | }
89 |
90 | insert = fmt.Sprintf(
91 | "<%s:%s:%d>",
92 | a, emoji.Name, emoji.ID,
93 | )
94 | }
95 |
96 | withoutlast := words[:len(words)-1]
97 | withoutlast = append(
98 | withoutlast,
99 | insert+" ",
100 | )
101 |
102 | input.SetText(strings.Join(withoutlast, " "))
103 |
104 | clearList()
105 |
106 | app.SetFocus(input)
107 | }
108 |
109 | } else {
110 | rightflex.ResizeItem(autocomp, 1, 1)
111 | }
112 |
113 | app.Draw()
114 | }
115 |
--------------------------------------------------------------------------------
/fuzzymentions.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/diamondburned/tview/v2"
8 | "github.com/sahilm/fuzzy"
9 | )
10 |
11 | // String returns the fuzzy search part of the struct
12 | func (gm UserStoreArray) String(i int) string {
13 | var s = gm[i].Name
14 |
15 | if gm[i].Nick != "" {
16 | s += " " + gm[i].Nick
17 | }
18 |
19 | return s
20 | }
21 |
22 | // Len returns the length
23 | func (gm UserStoreArray) Len() int {
24 | return len(gm)
25 | }
26 |
27 | // FuzzyMembers fuzzy searches and returns the slice of results
28 | func FuzzyMembers(pattern string, s *UserStore, guildID int64) (fzr UserStoreArray) {
29 | s.RLock()
30 | defer s.RUnlock()
31 |
32 | this, ok := s.Guilds[guildID]
33 | if !ok {
34 | return
35 | }
36 |
37 | results := fuzzy.FindFrom(pattern, this)
38 | for i := 0; i < len(results) && i < 10; i++ {
39 | fzr = append(fzr, this[results[i].Index])
40 | }
41 |
42 | return
43 | }
44 |
45 | func fuzzyMentions(last string) {
46 | var fuzzied UserStoreArray
47 |
48 | if len(last) > 0 && Channel != nil {
49 | fuzzied = FuzzyMembers(
50 | strings.TrimPrefix(last, "@"), us, Channel.GuildID,
51 | )
52 | }
53 |
54 | clearList()
55 |
56 | if len(fuzzied) > 0 {
57 | g, _ := d.State.Guild(Channel.GuildID)
58 |
59 | for i, u := range fuzzied {
60 | var username = u.Name + "[::d]#" + u.Discrim + "[::-]"
61 | if u.Nick != "" {
62 | username += " [::d](" + u.Nick + ")[::-]"
63 | }
64 |
65 | if g != nil {
66 | for _, p := range g.Presences {
67 | if p.User.ID == fuzzied[i].ID {
68 | username = fmt.Sprintf(
69 | "[%s]%s[-]",
70 | ReflectStatusColor(p.Status),
71 | username,
72 | )
73 | }
74 | }
75 | }
76 |
77 | autocomp.InsertItem(i, &tview.ListItem{"@" + username, "", 0, nil})
78 | }
79 |
80 | rightflex.ResizeItem(autocomp, min(len(fuzzied), 10), 1)
81 |
82 | autofillfunc = func(i int) {
83 | words := strings.Fields(input.GetText())
84 |
85 | withoutlast := words[:len(words)-1]
86 | withoutlast = append(withoutlast, fmt.Sprintf(
87 | "<@%d> ", fuzzied[i].ID,
88 | ))
89 |
90 | input.SetText(strings.Join(withoutlast, " "))
91 |
92 | clearList()
93 |
94 | app.SetFocus(input)
95 | }
96 |
97 | } else {
98 | rightflex.ResizeItem(autocomp, 1, 1)
99 | }
100 |
101 | app.Draw()
102 | }
103 |
--------------------------------------------------------------------------------
/fuzzymessages.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | "time"
8 |
9 | "github.com/diamondburned/tview/v2"
10 | )
11 |
12 | // [0]:format [1]:ID
13 | var allMessages []*tview.ListItem
14 |
15 | func fuzzyMessages(text string) {
16 | var fuzzied []*tview.ListItem
17 |
18 | if len(allMessages) == 0 && Channel != nil {
19 | for i := len(messageStore) - 1; i >= 0; i-- {
20 | ID := getIDfromindex(i)
21 | if ID == 0 {
22 | continue
23 | }
24 |
25 | allMessages = append(allMessages, makeMessageItem(ID))
26 | }
27 | }
28 |
29 | if len(text) > 1 {
30 | text := strings.TrimPrefix(text, "~")
31 | fuzzied = make([]*tview.ListItem, 0, len(allMessages))
32 |
33 | for _, m := range allMessages {
34 | if strings.Contains(m.MainText, text) {
35 | fuzzied = append(fuzzied)
36 | }
37 | }
38 | } else {
39 | fuzzied = allMessages
40 | }
41 |
42 | clearList()
43 |
44 | if len(fuzzied) > 0 {
45 | for i, j := 0, len(fuzzied)-1; i < j; i, j = i+1, j-1 {
46 | fuzzied[i], fuzzied[j] = fuzzied[j], fuzzied[i]
47 | }
48 |
49 | autocomp.SetItems(fuzzied)
50 | autocomp.SetCurrentItem(-1)
51 |
52 | rightflex.ResizeItem(autocomp, min(len(fuzzied), 10), 1)
53 |
54 | autofillfunc = func(i int) {
55 | words := strings.Fields(input.GetText())
56 |
57 | withoutlast := words[:len(words)-1]
58 | withoutlast = append(withoutlast, fuzzied[i].SecondaryText)
59 |
60 | input.SetText(strings.Join(withoutlast, " ") + " ")
61 |
62 | clearList()
63 | app.SetFocus(input)
64 | }
65 |
66 | } else {
67 | rightflex.ResizeItem(autocomp, 1, 1)
68 | }
69 |
70 | app.Draw()
71 |
72 | autocomp.SetChangedFunc(func(i int, t string, st string, s rune) {
73 | if i >= len(fuzzied) {
74 | return
75 | }
76 |
77 | ID := fuzzied[i].SecondaryText
78 |
79 | if Channel != nil {
80 | id, _ := strconv.ParseInt(ID, 10, 64)
81 | if id != 0 {
82 | m, err := d.State.Message(Channel.ID, id)
83 | if err == nil {
84 | // Update the list entry
85 | item := makeMessageItem(id)
86 | if item.MainText != t {
87 | fuzzied[i].MainText = item.MainText
88 | }
89 |
90 | imageRendererPipeline.add(m)
91 | }
92 | }
93 | }
94 |
95 | messagesView.Highlight(ID)
96 | messagesView.ScrollToHighlight()
97 | })
98 | }
99 |
100 | func makeMessageItem(ID int64) *tview.ListItem {
101 | id := strconv.FormatInt(ID, 10)
102 |
103 | m, err := d.State.Message(Channel.ID, ID)
104 | if err != nil {
105 | return &tview.ListItem{
106 | MainText: id + " - ???",
107 | SecondaryText: id,
108 | Shortcut: 0,
109 | Selected: nil,
110 | }
111 | }
112 |
113 | username, color := us.DiscordThis(m)
114 |
115 | sentTime, err := m.Timestamp.Parse()
116 | if err != nil {
117 | sentTime = time.Now()
118 | }
119 |
120 | var fetchedColor = readChannelColorPrefix
121 | if s := imageRendererPipeline.cache.get(m.ID); s != nil {
122 | fetchedColor = string(s.state)
123 | }
124 |
125 | return &tview.ListItem{
126 | MainText: fmt.Sprintf(
127 | "%s%s[-] - [#%06X]%s[-] [::d]- %s[::-]",
128 | fetchedColor, id, color, username,
129 | sentTime.Local().Format(time.Stamp),
130 | ),
131 | SecondaryText: id,
132 | Shortcut: 0,
133 | Selected: nil,
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/fuzzyupload.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "strings"
6 |
7 | "github.com/diamondburned/tview/v2"
8 | "github.com/sahilm/fuzzy"
9 | )
10 |
11 | func joinFolder(fs []string) (p string) {
12 | for _, f := range fs {
13 | p += f + "/"
14 | }
15 |
16 | return
17 | }
18 |
19 | func fuzzyUpload(text string) {
20 | fuzzied := [][]string{}
21 |
22 | if len(text) > 8 {
23 | text = strings.TrimPrefix(text, "/upload ")
24 |
25 | var (
26 | pathparts = strings.Split(text, "/")
27 | inputfile = pathparts[len(pathparts)-1]
28 |
29 | path = joinFolder(pathparts[:len(pathparts)-1])
30 | )
31 |
32 | f, err := os.Open(path)
33 | if err != nil {
34 | return
35 | }
36 |
37 | defer f.Close()
38 |
39 | files, err := f.Readdir(-1)
40 | if err != nil {
41 | return
42 | }
43 |
44 | filenames := make([]string, len(files))
45 |
46 | for i, f := range files {
47 | filename := f.Name()
48 | if f.IsDir() {
49 | filename += "/"
50 | }
51 |
52 | filenames[i] = filename
53 | }
54 |
55 | results := fuzzy.Find(inputfile, filenames)
56 | fuzzied = make([][]string, len(results))
57 |
58 | for i, r := range results {
59 | fuzzied[i] = []string{
60 | "[::u]" + path + "[::-]\u200B" + formatNeedle(r),
61 | path + filenames[r.Index],
62 | }
63 | }
64 | }
65 |
66 | clearList()
67 |
68 | if len(fuzzied) > 0 {
69 | for i, filename := range fuzzied {
70 | autocomp.InsertItem(
71 | i, &tview.ListItem{filename[0], "", 0, nil},
72 | )
73 | }
74 |
75 | rightflex.ResizeItem(autocomp, min(len(fuzzied), 10), 1)
76 |
77 | autofillfunc = func(i int) {
78 | input.SetText("/upload " + fuzzied[i][1])
79 | clearList()
80 | app.SetFocus(input)
81 | }
82 |
83 | } else {
84 | rightflex.ResizeItem(autocomp, 1, 1)
85 | }
86 |
87 | app.Draw()
88 | }
89 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module gitlab.com/diamondburned/6cord
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect
7 | github.com/alecthomas/chroma v0.6.7
8 | github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect
9 | github.com/atotto/clipboard v0.1.2
10 | github.com/davecgh/go-spew v1.1.1
11 | github.com/diamondburned/discordgo v1.2.1
12 | github.com/diamondburned/tcell v1.1.8
13 | github.com/diamondburned/tview/v2 v2.4.0
14 | github.com/disintegration/imaging v1.6.1
15 | github.com/dlclark/regexp2 v1.2.0 // indirect
16 | github.com/francoispqt/gojay v1.2.13 // indirect
17 | github.com/gen2brain/beeep v0.0.0-20190719094215-ece0cb67ca77
18 | github.com/go-test/deep v1.0.3
19 | github.com/gobwas/ws v1.0.2 // indirect
20 | github.com/gopherjs/gopherjs v0.0.0-20190915194858-d3ddacdb130f // indirect
21 | github.com/gopherjs/gopherwasm v1.1.0 // indirect
22 | github.com/hashicorp/go-retryablehttp v0.6.2 // indirect
23 | github.com/jonas747/gojay v0.0.0-20190906102056-b3bd5c8fcd50
24 | github.com/kylelemons/godebug v1.1.0 // indirect
25 | github.com/mattn/go-isatty v0.0.10 // indirect
26 | github.com/mattn/go-sixel v0.0.1
27 | github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859
28 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
29 | github.com/pelletier/go-toml v1.5.0 // indirect
30 | github.com/sahilm/fuzzy v0.1.0
31 | github.com/soniakeys/quant v1.0.0 // indirect
32 | github.com/stevenroose/gonfig v0.1.4
33 | github.com/stretchr/objx v0.2.0 // indirect
34 | github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
35 | github.com/valyala/fasttemplate v1.1.0
36 | github.com/zalando/go-keyring v0.0.0-20190913082157-62750a1ff80d
37 | gitlab.com/diamondburned/go-w3m v0.0.0-20190608163716-1b390b8a3d1f
38 | gitlab.com/diamondburned/ueberzug-go v0.0.0-20190521043425-7c15a5f63b06
39 | gitlab.com/shihoya-inc/ws v0.0.0-20190921174751-da4704c313d7 // indirect
40 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
41 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
42 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
43 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
44 | gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 // indirect
45 | )
46 |
--------------------------------------------------------------------------------
/guildevents.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/diamondburned/discordgo"
4 |
5 | func guildCreate(s *discordgo.Session, gc *discordgo.GuildCreate) {
6 | onReady(s, &d.State.Ready)
7 | }
8 |
9 | func guildDelete(s *discordgo.Session, gc *discordgo.GuildCreate) {
10 | onReady(s, &d.State.Ready)
11 | }
12 |
--------------------------------------------------------------------------------
/guildmembers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/diamondburned/discordgo"
5 | )
6 |
7 | func guildMemberAdd(s *discordgo.Session, gma *discordgo.GuildMemberAdd) {
8 | guildMemberDoSomething(gma.Member)
9 | }
10 |
11 | func guildMemberRemove(s *discordgo.Session, gmr *discordgo.GuildMemberRemove) {
12 | if gmr.User == nil {
13 | return
14 | }
15 |
16 | us.RemoveUser(gmr.GuildID, gmr.User.ID)
17 | }
18 |
19 | func guildMemberUpdate(s *discordgo.Session, gma *discordgo.GuildMemberUpdate) {
20 | guildMemberDoSomething(gma.Member)
21 | }
22 |
23 | func guildMemberDoSomething(gm *discordgo.Member) {
24 | if gm.User == nil {
25 | return
26 | }
27 |
28 | us.UpdateUser(
29 | gm.GuildID,
30 | gm.User.ID,
31 | gm.User.Username,
32 | gm.Nick,
33 | gm.User.Discriminator,
34 | getUserColor(gm.GuildID, gm.Roles),
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/guildsettings.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/diamondburned/discordgo"
5 | )
6 |
7 | func messagePingable(m *discordgo.Message, gID int64) bool {
8 | c, err := d.State.Channel(m.ChannelID)
9 | if err != nil {
10 | return false
11 | }
12 |
13 | if c.GuildID == 0 {
14 | if m.Author.ID != d.State.User.ID {
15 | return true
16 | }
17 |
18 | return false
19 | }
20 |
21 | s := getGuildFromSettings(c.GuildID)
22 |
23 | if m.MentionEveryone {
24 | //log.Println(2)
25 | return settingGuildAllowEveryone(s)
26 | }
27 |
28 | for _, mention := range m.Mentions {
29 | if mention.ID == d.State.User.ID {
30 | //log.Println(4)
31 | return true
32 | }
33 | }
34 |
35 | /* if s == nil {
36 | switch g.DefaultMessageNotifications {
37 | case 0:
38 | log.Println(3)
39 | return true
40 | case 2:
41 | default:
42 | }
43 | } else {
44 | var notify = s.MessageNotifications
45 | if c := getChannelFromGuildSettings(m.ChannelID, s); c != nil {
46 | notify = c.MessageNotifications
47 | }
48 |
49 | switch notify {
50 | case 0: // all messages
51 | log.Println(3)
52 | return true
53 | case 2:
54 | return false
55 | default: // case 1 - mentions only
56 | }
57 |
58 | for _, mention := range m.Mentions {
59 | if mention.ID == d.State.User.ID {
60 | log.Println(4)
61 | return true
62 | }
63 | }
64 | }
65 | */
66 |
67 | return false
68 | }
69 |
70 | func getGuildFromSettings(guildID int64) *discordgo.UserGuildSettings {
71 | for _, ugs := range d.State.UserGuildSettings {
72 | if ugs.GuildID == guildID {
73 | return ugs
74 | }
75 | }
76 |
77 | return nil
78 | }
79 |
80 | func settingGuildIsMuted(s *discordgo.UserGuildSettings) bool {
81 | if s == nil {
82 | return false
83 | }
84 |
85 | return s.Muted
86 | }
87 |
88 | func settingGuildAllowEveryone(s *discordgo.UserGuildSettings) bool {
89 | if s == nil {
90 | return true
91 | }
92 |
93 | return !s.SupressEveryone
94 | }
95 |
96 | func getChannelFromGuildSettings(chID int64, s *discordgo.UserGuildSettings,
97 | ) *discordgo.UserGuildSettingsChannelOverride {
98 |
99 | if s == nil {
100 | return nil
101 | }
102 |
103 | for _, c := range s.ChannelOverrides {
104 | if c.ChannelID == chID {
105 | return c
106 | }
107 | }
108 |
109 | return nil
110 | }
111 |
112 | func settingChannelIsMuted(
113 | cho *discordgo.UserGuildSettingsChannelOverride,
114 | s *discordgo.UserGuildSettings) (m bool) {
115 |
116 | if cho == nil {
117 | return false
118 | }
119 |
120 | m = cho.Muted
121 |
122 | c, err := d.State.Channel(cho.ChannelID)
123 | if err != nil {
124 | return
125 | }
126 |
127 | if c.ParentID != 0 {
128 | if cs := getChannelFromGuildSettings(c.ParentID, s); cs != nil {
129 | return cs.Muted
130 | }
131 | }
132 |
133 | return
134 | }
135 |
--------------------------------------------------------------------------------
/heated.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "sync"
4 |
5 | var (
6 | heatedChannels = struct {
7 | sync.Mutex
8 | buffer map[int64]struct{}
9 | }{
10 | buffer: map[int64]struct{}{},
11 | }
12 | )
13 |
14 | // true == added, false == removed
15 | func heatedChannelsToggle(channelID int64) bool {
16 | heatedChannels.Lock()
17 | defer heatedChannels.Unlock()
18 |
19 | if _, ok := heatedChannels.buffer[channelID]; ok {
20 | delete(heatedChannels.buffer, channelID)
21 | return false
22 | }
23 |
24 | heatedChannels.buffer[channelID] = struct{}{}
25 | return true
26 | }
27 |
28 | func heatedChannelsExists(channelID int64) bool {
29 | heatedChannels.Lock()
30 | defer heatedChannels.Unlock()
31 |
32 | if _, ok := heatedChannels.buffer[channelID]; ok {
33 | delete(heatedChannels.buffer, channelID)
34 | return true
35 | }
36 |
37 | return false
38 | }
39 |
--------------------------------------------------------------------------------
/humanize.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // HumanizeStrings converts arrays to a friendly string
4 | func HumanizeStrings(a []string) (s string) {
5 | switch len(a) {
6 | case 0:
7 | case 1:
8 | s = a[0]
9 | case 2:
10 | s = a[0] + " and " + a[1]
11 | default:
12 | for i := 0; i < len(a)-2; i++ {
13 | s += a[i] + ", "
14 | }
15 |
16 | s += a[len(a)-2] + " and " + a[len(a)-1]
17 | }
18 |
19 | return
20 | }
21 |
--------------------------------------------------------------------------------
/image.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | _ "image/gif"
8 | _ "image/jpeg"
9 | _ "image/png"
10 |
11 | "github.com/diamondburned/discordgo"
12 | img "gitlab.com/diamondburned/6cord/image"
13 | )
14 |
15 | type imageCtx struct {
16 | state img.Backend
17 | index int
18 | }
19 |
20 | type imageRendererPipelineStruct struct {
21 | event chan interface{}
22 | state img.Backend
23 | message int64
24 | index int
25 |
26 | cache *imageCacheStruct
27 | assets []*imageCacheAsset
28 | }
29 |
30 | const (
31 | imagePipelineNextEvent int = iota
32 | imagePipelinePrevEvent
33 | )
34 |
35 | var imageRendererPipeline *imageRendererPipelineStruct
36 |
37 | func startImageRendererPipeline() *imageRendererPipelineStruct {
38 | p := &imageRendererPipelineStruct{
39 | event: make(chan interface{}, prefetchMessageCount),
40 | cache: &imageCacheStruct{
41 | Age: 5 * time.Minute,
42 | client: &http.Client{
43 | Timeout: 5 * time.Second,
44 | },
45 | store: map[int64]*imageCacheStore{},
46 | },
47 | }
48 |
49 | go func() {
50 | for i := range p.event {
51 | Switch:
52 | switch i := i.(type) {
53 | case *discordgo.Message:
54 | p.message = i.ID
55 |
56 | a := p.cache.get(i.ID)
57 | if a == nil || a.state != imageFetched {
58 | p.clean()
59 | break
60 | }
61 |
62 | p.assets = a.assets
63 | p.clean()
64 |
65 | if p.assets == nil {
66 | break Switch
67 | }
68 |
69 | p.show()
70 |
71 | case int:
72 | if p.assets == nil {
73 | break Switch
74 | }
75 |
76 | switch i {
77 | case imagePipelineNextEvent:
78 | p.index++
79 | if p.index >= len(p.assets) {
80 | p.index = 0
81 | }
82 | case imagePipelinePrevEvent:
83 | p.index--
84 | if p.index < 0 {
85 | p.index = len(p.assets) - 1
86 | }
87 | default:
88 | break Switch
89 | }
90 |
91 | p.show()
92 |
93 | default:
94 | break Switch
95 | }
96 | }
97 | }()
98 |
99 | return p
100 | }
101 |
102 | func (p *imageRendererPipelineStruct) add(m *discordgo.Message) {
103 | p.event <- m
104 | }
105 |
106 | func (p *imageRendererPipelineStruct) next() {
107 | p.event <- imagePipelineNextEvent
108 | }
109 |
110 | func (p *imageRendererPipelineStruct) prev() {
111 | p.event <- imagePipelinePrevEvent
112 | }
113 |
114 | func (p *imageRendererPipelineStruct) clean() {
115 | if p != nil {
116 | if p.state != nil {
117 | p.state.Delete()
118 | }
119 |
120 | p.cache.gc()
121 | p.index = 0
122 | }
123 | }
124 |
125 | func (p *imageRendererPipelineStruct) show() (err error) {
126 | p.clean()
127 |
128 | if p.assets == nil {
129 | return nil
130 | }
131 |
132 | if p.assets[p.index].i == nil {
133 | return nil
134 | }
135 |
136 | p.state, err = img.New(p.assets[p.index].i)
137 | if err != nil {
138 | return err
139 | }
140 |
141 | return nil
142 | }
143 |
--------------------------------------------------------------------------------
/image/backend.go:
--------------------------------------------------------------------------------
1 | package image
2 |
3 | import (
4 | "github.com/mattn/go-tty"
5 | "gitlab.com/diamondburned/ueberzug-go"
6 | )
7 |
8 | func ttySize(t *tty.TTY) (int, int) {
9 | _, _, w, h, _ := t.SizePixel()
10 | return w, h
11 | }
12 |
13 | func escapeSize(t *tty.TTY) (int, int) {
14 | return getTermSize(t)
15 | }
16 |
17 | func xSize(t *tty.TTY) (int, int) {
18 | w, h, err := ueberzug.GetParentSize()
19 | if err != nil {
20 | return 0, 0
21 | }
22 |
23 | return w, h
24 | }
25 |
--------------------------------------------------------------------------------
/image/image.go:
--------------------------------------------------------------------------------
1 | package image
2 |
3 | import (
4 | "errors"
5 | "image"
6 |
7 | "gitlab.com/diamondburned/ueberzug-go"
8 | )
9 |
10 | // Backend is the interface for various image
11 | // backends
12 | type Backend interface {
13 | // Available should return nil if an image
14 | // backend successfully runs. This could be
15 | // used for intializing backends.
16 | Available() error
17 |
18 | Spawn(image.Image, int, int) error
19 |
20 | Delete() error
21 | }
22 |
23 | // PossibleBackends is a list of possible image
24 | // backends
25 | var PossibleBackends = []Backend{
26 | &Ueberzug{},
27 | &W3M{},
28 | }
29 |
30 | var (
31 | // ErrNoBackend is returned if no backends
32 | // are available
33 | ErrNoBackend = errors.New("no backend")
34 | )
35 |
36 | // New finds a backend and spawns the image
37 | func New(i image.Image) (backend Backend, err error) {
38 | if t == nil {
39 | if err := Listen(); err != nil {
40 | return nil, err
41 | }
42 | }
43 |
44 | if PixelW < 1 || PixelH < 1 {
45 | return nil, ErrNoBackend
46 | }
47 |
48 | for _, b := range PossibleBackends {
49 | if b.Available() == nil {
50 | backend = b
51 | break
52 | }
53 | }
54 |
55 | if backend == nil {
56 | return nil, ErrNoBackend
57 | }
58 |
59 | w := i.Bounds().Dx()
60 |
61 | if err := backend.Spawn(i, PixelW/2-w/2, 0); err != nil {
62 | return nil, err
63 | }
64 |
65 | return backend, nil
66 | }
67 |
68 | // Close has to be ran on exit!
69 | func Close() {
70 | if t != nil {
71 | t.Close()
72 | t = nil
73 | }
74 |
75 | ueberzug.Close()
76 | }
77 |
--------------------------------------------------------------------------------
/image/query.go:
--------------------------------------------------------------------------------
1 | package image
2 |
3 | import (
4 | "strconv"
5 | "time"
6 |
7 | "github.com/mattn/go-tty"
8 | )
9 |
10 | // All these functions are taken from
11 | // https://github.com/gizak/termui/pull/233/files#diff-61ca5d3d7b39f5b633e6774d6a31aea5R91
12 | // and is modified promptly for this library.
13 | //
14 | // All credits reserved.
15 |
16 | func queryTerm(qs string, t *tty.TTY) (ret [][]rune) {
17 | var b []rune
18 |
19 | ch := make(chan bool, 1)
20 |
21 | go func() {
22 | Main:
23 | for {
24 | r, err := t.ReadRune()
25 | if err != nil {
26 | return
27 | }
28 | // handle key event
29 | switch r {
30 | case 'c', 't':
31 | ret = append(ret, b)
32 | break Main
33 | case '?', ';':
34 | ret = append(ret, b)
35 | b = []rune{}
36 | default:
37 | b = append(b, r)
38 | }
39 | }
40 |
41 | ch <- true
42 | }()
43 |
44 | timer := time.NewTimer(100 * time.Microsecond)
45 | defer timer.Stop()
46 |
47 | select {
48 | case <-ch:
49 | defer close(ch)
50 | case <-timer.C:
51 | }
52 |
53 | return
54 | }
55 |
56 | func getTermSize(t *tty.TTY) (w, h int) {
57 | q := queryTerm("\033[14t", t)
58 | if len(q) != 3 {
59 | return
60 | }
61 |
62 | if yy, err := strconv.Atoi(string(q[1])); err == nil {
63 | if xx, err := strconv.Atoi(string(q[2])); err == nil {
64 | w = xx
65 | h = yy
66 | }
67 | }
68 |
69 | return
70 | }
71 |
--------------------------------------------------------------------------------
/image/sixel.go:
--------------------------------------------------------------------------------
1 | package image
2 |
3 | /*
4 | // Ueberzug is the backend for ueberzug-go:
5 | // https://gitlab.com/diamondburned/ueberzug-go
6 | type Ueberzug struct {
7 | i *ueberzug.Image
8 | }
9 |
10 | func (u *Ueberzug) Available() error {
11 | return ueberzug.Initialize()
12 | }
13 |
14 | func (u *Ueberzug) Spawn(i image.Image, x, y int) error {
15 | ui, err := ueberzug.NewImage(i, x, y)
16 | if err != nil {
17 | return err
18 | }
19 |
20 | u.i = ui
21 | return nil
22 | }
23 |
24 | func (u *Ueberzug) Delete() error {
25 | u.i.Clear()
26 | u.i.Destroy()
27 | return nil
28 | }
29 | */
30 |
--------------------------------------------------------------------------------
/image/ueberzug.go:
--------------------------------------------------------------------------------
1 | package image
2 |
3 | import (
4 | "image"
5 |
6 | ueberzug "gitlab.com/diamondburned/ueberzug-go"
7 | )
8 |
9 | // Ueberzug is the backend for ueberzug-go:
10 | // https://gitlab.com/diamondburned/ueberzug-go
11 | type Ueberzug struct {
12 | i *ueberzug.Image
13 | }
14 |
15 | func (u *Ueberzug) Available() error {
16 | return ueberzug.Initialize()
17 | }
18 |
19 | func (u *Ueberzug) Spawn(i image.Image, x, y int) error {
20 | ui, err := ueberzug.NewImage(i, x, y)
21 | if err != nil {
22 | return err
23 | }
24 |
25 | u.i = ui
26 | return nil
27 | }
28 |
29 | func (u *Ueberzug) Delete() error {
30 | u.i.Clear()
31 | u.i.Destroy()
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/image/w3m.go:
--------------------------------------------------------------------------------
1 | package image
2 |
3 | import (
4 | "errors"
5 | "image"
6 | "image/png"
7 | "io/ioutil"
8 | "os"
9 | "strconv"
10 | "time"
11 |
12 | "gitlab.com/diamondburned/go-w3m"
13 | )
14 |
15 | type W3M struct {
16 | a *w3m.Arguments
17 | f *os.File
18 | }
19 |
20 | var Errw3mUnavailable = errors.New("w3m unavailable")
21 |
22 | func (w *W3M) Available() error {
23 | if w3m.GetExecPath() == "" {
24 | return Errw3mUnavailable
25 | }
26 |
27 | return nil
28 | }
29 |
30 | func (w *W3M) Spawn(i image.Image, x, y int) error {
31 | bounds := i.Bounds()
32 |
33 | w.a = &w3m.Arguments{
34 | Width: bounds.Dx(),
35 | Height: bounds.Dy(),
36 | Xoffset: x,
37 | Yoffset: y,
38 | }
39 |
40 | t := strconv.FormatInt(time.Now().UnixNano(), 10)
41 |
42 | f, err := ioutil.TempFile(os.TempDir(), t+".png")
43 | if err != nil {
44 | return err
45 | }
46 |
47 | defer f.Close()
48 |
49 | if err := png.Encode(f, i); err != nil {
50 | return err
51 | }
52 |
53 | w.f = f
54 |
55 | return w3m.Spawn(w.a, f.Name())
56 | }
57 |
58 | func (w *W3M) Delete() error {
59 | os.Remove(w.f.Name())
60 | return w3m.Clear(w.a)
61 | }
62 |
--------------------------------------------------------------------------------
/image/winch.go:
--------------------------------------------------------------------------------
1 | package image
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/mattn/go-tty"
7 | )
8 |
9 | var t *tty.TTY
10 |
11 | var (
12 | PixelW int
13 | PixelH int
14 | )
15 |
16 | func Listen() (err error) {
17 | t, err = tty.Open()
18 | if err != nil {
19 | return err
20 | }
21 |
22 | var (
23 | fn func(*tty.TTY) (int, int)
24 | w int
25 | h int
26 | )
27 |
28 | if w, h = ttySize(t); w > 0 && h > 0 {
29 | fn = ttySize
30 | } else if w, h = xSize(t); w > 0 && h > 0 {
31 | fn = xSize
32 | } else if w, h = escapeSize(t); w > 0 && h > 0 {
33 | fn = escapeSize
34 | }
35 |
36 | if fn == nil {
37 | return errors.New("No method of getting terminal size avilable")
38 | }
39 |
40 | PixelW = w
41 | PixelH = h
42 |
43 | go func() {
44 | winch := t.SIGWINCH()
45 | for range winch {
46 | PixelW, PixelH = fn(t)
47 | }
48 | }()
49 |
50 | return nil
51 | }
52 |
--------------------------------------------------------------------------------
/image/winch_test.go:
--------------------------------------------------------------------------------
1 | package image
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestWinch(t *testing.T) {
9 | if err := Listen(); err != nil {
10 | t.Fatal(t)
11 | }
12 |
13 | defer Close()
14 |
15 | if PixelH < 1 || PixelW < 1 {
16 | t.Fatal("failed")
17 | }
18 |
19 | fmt.Println(PixelH, PixelW)
20 | }
21 |
--------------------------------------------------------------------------------
/imagecache.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "log"
7 | "net/http"
8 | "path/filepath"
9 | "strings"
10 | "sync"
11 | "time"
12 |
13 | _ "image/jpeg"
14 | _ "image/png"
15 |
16 | "github.com/diamondburned/discordgo"
17 | )
18 |
19 | type imageCacheStruct struct {
20 | sync.RWMutex
21 | Age time.Duration
22 |
23 | client *http.Client
24 | store map[int64]*imageCacheStore
25 | lastCh int64
26 | }
27 |
28 | type imageCacheStore struct {
29 | assets []*imageCacheAsset
30 | time time.Time
31 | state imageFetchState
32 | }
33 |
34 | type imageCacheAsset struct {
35 | url string
36 | w, h int
37 | sizedURL string
38 |
39 | i image.Image
40 | }
41 |
42 | type imageFetchState string
43 |
44 | const (
45 | imageNotFetched imageFetchState = "[#424242]"
46 | imageFetching imageFetchState = "[green]"
47 | imageFetched imageFetchState = "[lime]"
48 | )
49 |
50 | func (c *imageCacheStruct) get(m int64) *imageCacheStore {
51 | c.RLock()
52 | defer c.RUnlock()
53 |
54 | if a, ok := c.store[m]; ok {
55 | return a
56 | }
57 |
58 | return nil
59 | }
60 |
61 | func (c *imageCacheStruct) calcURL(a *imageCacheAsset) {
62 | var (
63 | resizeW int
64 | resizeH int
65 | )
66 |
67 | if a.w < a.h {
68 | resizeH = cfg.Prop.ImageHeight
69 | resizeW = cfg.Prop.ImageHeight * a.w / a.h
70 | } else {
71 | resizeW = cfg.Prop.ImageWidth
72 | resizeH = cfg.Prop.ImageWidth * a.h / a.w
73 | }
74 |
75 | if a.sizedURL == "" {
76 | a.sizedURL = strings.Split(a.url, "?")[0] + fmt.Sprintf(
77 | "?width=%d&height=%d",
78 | resizeW, resizeH,
79 | )
80 | }
81 | }
82 |
83 | func (c *imageCacheStruct) markUnfetch(m *discordgo.Message) *imageCacheStore {
84 | s := &imageCacheStore{
85 | assets: make(
86 | []*imageCacheAsset,
87 | 0, len(m.Attachments)+len(m.Embeds),
88 | ),
89 | time: time.Now(),
90 | state: imageNotFetched,
91 | }
92 |
93 | for _, a := range m.Attachments {
94 | if a.Width < 1 || a.Height < 1 {
95 | continue
96 | }
97 |
98 | if !imageFormatIsSupported(a.Filename) {
99 | continue
100 | }
101 |
102 | a := &imageCacheAsset{
103 | url: a.ProxyURL,
104 | w: a.Width,
105 | h: a.Height,
106 | }
107 |
108 | c.calcURL(a)
109 | s.assets = append(s.assets, a)
110 | }
111 |
112 | for _, e := range m.Embeds {
113 | if t := e.Thumbnail; t != nil {
114 | if t.Width < 1 || t.Height < 1 {
115 | continue
116 | }
117 |
118 | if !imageFormatIsSupported(t.ProxyURL) {
119 | continue
120 | }
121 |
122 | a := &imageCacheAsset{
123 | url: t.ProxyURL,
124 | w: t.Width,
125 | h: t.Height,
126 | }
127 |
128 | c.calcURL(a)
129 | s.assets = append(s.assets, a)
130 | }
131 | }
132 |
133 | if len(s.assets) == 0 {
134 | return nil
135 | }
136 |
137 | c.Lock()
138 | defer c.Unlock()
139 |
140 | c.store[m.ID] = s
141 |
142 | return s
143 | }
144 |
145 | // set checks cache as well
146 | func (c *imageCacheStruct) upd(m *discordgo.Message) (*imageCacheStore, error) {
147 | s := c.get(m.ID)
148 | // If already fetched
149 | if s != nil && s.state == imageFetched {
150 | return s, nil
151 | }
152 |
153 | // If not fetched, but there's something
154 | if s == nil {
155 | s = c.markUnfetch(m)
156 | }
157 |
158 | // If there's nothing
159 | if s == nil {
160 | return nil, nil
161 | }
162 |
163 | c.Lock()
164 | defer c.Unlock()
165 |
166 | s.state = imageFetching
167 |
168 | for _, a := range s.assets {
169 | r, err := c.client.Get(a.sizedURL)
170 | if err != nil {
171 | return nil, err
172 | }
173 |
174 | i, _, err := image.Decode(r.Body)
175 | if err == nil {
176 | a.i = i
177 | } else {
178 | // Error is ignored, as skipping a non-supported
179 | // image is fine
180 | log.Println("Error on", a.sizedURL, "\n"+err.Error())
181 | }
182 |
183 | r.Body.Close()
184 | }
185 |
186 | s.state = imageFetched
187 | c.store[m.ID] = s
188 |
189 | go app.Draw()
190 |
191 | return s, nil
192 | }
193 |
194 | func (c *imageCacheStruct) reset() {
195 | c.Lock()
196 | defer c.Unlock()
197 |
198 | c.store = map[int64]*imageCacheStore{}
199 | }
200 |
201 | func (c *imageCacheStruct) gc() {
202 | c.Lock()
203 | defer c.Unlock()
204 |
205 | for k, store := range c.store {
206 | if Channel != nil && Channel.ID == k {
207 | continue
208 | }
209 |
210 | if time.Now().Sub(store.time) > c.Age {
211 | delete(c.store, k)
212 | }
213 | }
214 | }
215 |
216 | func imageFormatIsSupported(filename string) bool {
217 | fileExt := filepath.Ext(filename)
218 | for _, ext := range []string{".png", ".jpg", ".jpeg"} {
219 | if fileExt == ext {
220 | return true
221 | }
222 | }
223 |
224 | return false
225 | }
226 |
--------------------------------------------------------------------------------
/input.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "log"
6 | "strings"
7 |
8 | "github.com/atotto/clipboard"
9 | "github.com/diamondburned/tcell"
10 | "github.com/diamondburned/tview/v2"
11 | "gitlab.com/diamondburned/6cord/antitele"
12 | )
13 |
14 | var toReplaceRuneMap = map[byte]string{
15 | 'n': "\n",
16 | 't': " ",
17 | }
18 |
19 | var (
20 | currentHistoryItem = -1
21 | currentMessage string
22 | )
23 |
24 | func resetInputBehavior() {
25 | input.SetLabel(templatePrefix())
26 | input.SetLabelColor(tcell.Color(cfg.Prop.BackgroundColor))
27 | input.SetBackgroundColor(tcell.Color(cfg.Prop.BackgroundColor))
28 | input.SetFieldBackgroundColor(tcell.Color(cfg.Prop.BackgroundColor))
29 | input.SetPlaceholderTextColor(tcell.ColorDarkCyan)
30 | input.SetText("")
31 | clearList()
32 |
33 | stateResetter()
34 | toEditMessage = 0
35 | }
36 |
37 | func templatePrefix() string {
38 | var (
39 | channelTpl = "#nil"
40 | guildTpl = "nil"
41 | selfnameTpl = "nil"
42 | discrimTpl = "0000"
43 | )
44 |
45 | if Channel != nil {
46 | switch {
47 | case Channel.Name == "":
48 | channelTpl = "#dm"
49 | default:
50 | channelTpl = "#" + Channel.Name
51 | }
52 |
53 | g, _ := d.State.Guild(Channel.GuildID)
54 | if g != nil {
55 | guildTpl = g.Name
56 | }
57 | }
58 |
59 | if d != nil && d.State.User != nil {
60 | selfnameTpl = d.State.User.Username
61 | discrimTpl = d.State.User.Discriminator
62 | }
63 |
64 | return prefixTpl.ExecuteString(map[string]interface{}{
65 | "CHANNEL": channelTpl,
66 | "GUILD": guildTpl,
67 | "USERNAME": selfnameTpl,
68 | "DISCRIM": discrimTpl,
69 | })
70 | }
71 |
72 | func processString(input string) string {
73 | var output = strings.Builder{}
74 |
75 | RuneWalk:
76 | for i := 0; i < len(input); i++ {
77 | for match, with := range toReplaceRuneMap {
78 | if input[i] == '\\' && (i < len(input)-1 && input[i+1] == match) {
79 | if i == 0 || input[i-1] != '\\' {
80 | output.WriteString(with)
81 | if i < len(input)-2 && input[i+2] == ' ' {
82 | i++
83 | }
84 |
85 | i++
86 | continue RuneWalk
87 | } else {
88 | i++
89 | }
90 | }
91 | }
92 |
93 | output.WriteByte(input[i])
94 | }
95 |
96 | if cfg.Prop.ObfuscateWords {
97 | return antitele.Insert(output.String())
98 | }
99 |
100 | return output.String()
101 | }
102 |
103 | var store bool
104 |
105 | func handleHistoryItem() {
106 | if currentHistoryItem < 0 {
107 | input.SetText(currentMessage)
108 | store = true
109 | } else {
110 | if store {
111 | currentMessage = input.GetText()
112 | store = false
113 | }
114 |
115 | input.SetText(cmdHistory[currentHistoryItem])
116 | }
117 | }
118 |
119 | func inputKeyHandler(ev *tcell.EventKey) *tcell.EventKey {
120 | if ev.Modifiers() == tcell.ModAlt {
121 | switch {
122 | case ev.Key() == tcell.KeyEnter:
123 | input.SetText(input.GetText() + "\n")
124 | return nil
125 |
126 | case ev.Key() == tcell.KeyDown || ev.Rune() == 'j':
127 | currentHistoryItem++
128 | if currentHistoryItem > len(cmdHistory)-1 {
129 | currentHistoryItem = len(cmdHistory) - 1
130 | }
131 |
132 | handleHistoryItem()
133 | return nil
134 |
135 | case ev.Key() == tcell.KeyUp || ev.Rune() == 'k':
136 | currentHistoryItem--
137 | if currentHistoryItem < -1 {
138 | currentHistoryItem = -1
139 | }
140 |
141 | handleHistoryItem()
142 | return nil
143 | }
144 | } else {
145 | switch ev.Key() {
146 | case tcell.KeyEscape:
147 | resetInputBehavior()
148 | if showChannels {
149 | toggleChannels()
150 | }
151 |
152 | app.SetFocus(guildView)
153 | return nil
154 |
155 | case tcell.KeyCtrlV:
156 | cb, err := clipboard.ReadAll()
157 | if err != nil {
158 | log.Println("Couldn't get clipboard:", err)
159 | return nil
160 | }
161 |
162 | b := []byte(cb)
163 |
164 | if IsFile(b) {
165 | modal := tview.NewModal()
166 | modal.AddButtons([]string{"Cancel", "Yes"})
167 | modal.SetText("Upload file in clipboard?")
168 | modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
169 | switch buttonLabel {
170 | case "Yes":
171 | go func() {
172 | if Channel == nil {
173 | Warn("Not in a channel.")
174 | }
175 |
176 | input.SetPlaceholder("Uploading file...")
177 |
178 | br := bytes.NewReader(b)
179 | _, err = d.ChannelFileSend(
180 | Channel.ID,
181 | "clipboard.png",
182 | br,
183 | )
184 |
185 | input.SetPlaceholder(cfg.Prop.DefaultStatus)
186 |
187 | if err != nil {
188 | Warn(err.Error())
189 | }
190 | }()
191 | }
192 |
193 | app.SetRoot(appflex, true)
194 | app.SetFocus(input)
195 | })
196 |
197 | app.SetRoot(modal, false)
198 | app.SetFocus(modal)
199 |
200 | } else {
201 | input.SetText(input.GetText() + cb)
202 | }
203 | case tcell.KeyLeft, tcell.KeyCtrlH:
204 | if input.GetText() != "" {
205 | return ev
206 | }
207 |
208 | app.SetFocus(guildView)
209 | return nil
210 |
211 | case tcell.KeyUp, tcell.KeyCtrlP:
212 | if autocomp.GetItemCount() < 1 {
213 | // Todo: replace with /edit call
214 | app.SetFocus(messagesView)
215 | } else {
216 | acItem := autocomp.GetCurrentItem()
217 | if acItem == 0 {
218 | newitem := autocomp.GetItemCount() - 1
219 | autocomp.SetCurrentItem(newitem)
220 | }
221 |
222 | app.SetFocus(autocomp)
223 | }
224 |
225 | case tcell.KeyDown, tcell.KeyCtrlN:
226 | acItem := autocomp.GetCurrentItem()
227 | var newitem = acItem + 1
228 |
229 | switch {
230 | case autocomp.GetItemCount() == 0:
231 | return ev
232 | case newitem > autocomp.GetItemCount()-1:
233 | newitem = 0
234 | }
235 |
236 | autocomp.SetCurrentItem(newitem)
237 | app.SetFocus(autocomp)
238 |
239 | case tcell.KeyPgUp, tcell.KeyPgDn:
240 | // Handle scrolling with PgUp/Dn in the input box
241 | app.SetFocus(messagesView)
242 | messagesView.InputHandler()(ev, nil)
243 | handleScroll()
244 | return nil
245 |
246 | case tcell.KeyTab:
247 | if autocomp.GetItemCount() > 0 {
248 | acItem := autocomp.GetCurrentItem()
249 | autofillfunc(acItem)
250 | return nil
251 | }
252 |
253 | return ev
254 |
255 | case tcell.KeyEnter:
256 | if autocomp.GetItemCount() > 0 {
257 | acItem := autocomp.GetCurrentItem()
258 | autofillfunc(acItem)
259 | return nil
260 | }
261 |
262 | // log.Println(ev.Name())
263 |
264 | // if ev.Name() == "Shift+Enter" {
265 | // input.SetText(input.GetText() + "\\n")
266 | // return nil
267 | // }
268 |
269 | switch input.GetLabel() {
270 | case EditMessageLabel:
271 | go editHandler()
272 |
273 | default:
274 | CommandHandler()
275 | }
276 |
277 | return nil
278 | }
279 |
280 | if ev.Name() == "'" && ev.Modifiers() == tcell.ModCtrl {
281 | commandEditor(nil)
282 | return nil
283 | }
284 | }
285 |
286 | return ev
287 | }
288 |
--------------------------------------------------------------------------------
/keyring/keyring.go:
--------------------------------------------------------------------------------
1 | package keyring
2 |
3 | import (
4 | "log"
5 |
6 | keyring "github.com/zalando/go-keyring"
7 | )
8 |
9 | const (
10 | // AppName used for keyrings
11 | AppName = "6cord"
12 | )
13 |
14 | func Get() string { return Store.Get() }
15 | func Set(token string) { Store.Set(token) }
16 |
17 | var Store Storer = defaultKeyring{}
18 |
19 | type Storer interface {
20 | Get() string
21 | Set(token string)
22 | Delete()
23 | }
24 |
25 | type defaultKeyring struct{}
26 |
27 | func (defaultKeyring) Get() string {
28 | k, err := keyring.Get(AppName, "token")
29 | if err != nil {
30 | log.Println("Warning: Could not get token:", err)
31 | }
32 |
33 | return k
34 | }
35 |
36 | func (defaultKeyring) Set(token string) {
37 | if err := keyring.Set(AppName, "token", token); err != nil {
38 | log.Println("Warning: Token is not stored:", err)
39 | }
40 | }
41 |
42 | func (defaultKeyring) Delete() {
43 | if err := keyring.Delete(AppName, "token"); err != nil {
44 | log.Println("Warning: Token is not deleted:", err)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/keyring/keyring_null.go:
--------------------------------------------------------------------------------
1 | // +build nokeyring
2 |
3 | package keyring
4 |
5 | import (
6 | "log"
7 | )
8 |
9 | func init() {
10 | Store = nullKeyring{}
11 | }
12 |
13 | type nullKeyring struct{}
14 |
15 | func (nullKeyring) Get() string {
16 | log.Println("Warning: 6cord compiled without keyring")
17 | return ""
18 | }
19 |
20 | func (nullKeyring) Set(token string) {
21 | log.Println("Warning: 6cord compiled without keyring")
22 | }
23 |
24 | func (nullKeyring) Delete() {
25 | log.Println("Warning: 6cord compiled without keyring")
26 | }
27 |
--------------------------------------------------------------------------------
/lastauthor_state.go:
--------------------------------------------------------------------------------
1 | package main
2 |
--------------------------------------------------------------------------------
/loadchannel.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "sort"
6 | "strings"
7 | "sync"
8 | "time"
9 |
10 | "github.com/diamondburned/discordgo"
11 | "github.com/diamondburned/tview/v2"
12 | )
13 |
14 | const prefetchMessageCount = 35
15 |
16 | var fetchedMembersGuild = map[int64]struct{}{}
17 | var fetchedMembersGuildMu sync.Mutex
18 |
19 | var _loadingChannel bool
20 |
21 | func loadingChannel() func() {
22 | _loadingChannel = true
23 | return func() {
24 | _loadingChannel = false
25 | }
26 | }
27 |
28 | func isFetched(guildID int64) bool {
29 | fetchedMembersGuildMu.Lock()
30 | defer fetchedMembersGuildMu.Unlock()
31 |
32 | if _, ok := fetchedMembersGuild[guildID]; ok {
33 | return true
34 | }
35 |
36 | fetchedMembersGuild[guildID] = struct{}{}
37 | return false
38 | }
39 |
40 | func loadChannel(channelID int64) {
41 | if _loadingChannel {
42 | return
43 | }
44 |
45 | wrapFrame.SetTitle("[Loading...[]")
46 | app.Draw()
47 |
48 | go actualLoadChannel(channelID)
49 | }
50 |
51 | func actualLoadChannel(channelID int64) {
52 | defer loadingChannel()()
53 |
54 | ch, err := d.State.Channel(channelID)
55 | if err != nil {
56 | ch, err = d.Channel(channelID)
57 | if err != nil {
58 | Warn(err.Error())
59 | return
60 | }
61 | }
62 |
63 | log.Println("Booted stage 1")
64 | // time.Sleep(1 * time.Second)
65 |
66 | switch ch.Type {
67 | case discordgo.ChannelTypeGuildVoice:
68 | Message("Voice is currently not working D:")
69 | return
70 | case discordgo.ChannelTypeGuildCategory:
71 | return
72 | }
73 |
74 | Channel = ch
75 |
76 | resetInputBehavior()
77 | typing.Reset()
78 | app.SetFocus(input)
79 |
80 | if len(ch.Messages) > 0 {
81 | drawMsgs(ch.Messages)
82 | }
83 |
84 | msgs, err := d.ChannelMessages(
85 | Channel.ID, prefetchMessageCount,
86 | 0, 0, 0,
87 | )
88 |
89 | if err != nil {
90 | Warn(err.Error())
91 | return
92 | }
93 |
94 | log.Println("Booted stage 2")
95 | // time.Sleep(1 * time.Second)
96 |
97 | ch.Messages = msgs
98 |
99 | if len(msgs) > 0 {
100 | go func(c *discordgo.Channel, msgs []*discordgo.Message) {
101 | log.Println("Prepare to ack...")
102 | time.Sleep(3 * time.Second)
103 | log.Println("Acking...")
104 | ackMe(c.ID, msgs[len(msgs)-1].ID)
105 | }(ch, msgs)
106 | }
107 |
108 | drawMsgs(msgs)
109 |
110 | if len(msgs) == 0 {
111 | Message("There's nothing here!")
112 | }
113 |
114 | wrapFrame.SetTitle(generateTitle(ch))
115 | app.Draw()
116 |
117 | go func() {
118 | if ch.GuildID == 0 {
119 | return
120 | }
121 |
122 | // If the huge members list is already fetched, return
123 | if isFetched(ch.GuildID) {
124 | return
125 | }
126 |
127 | log.Println("Booted stage 3")
128 | time.Sleep(2 * time.Second)
129 |
130 | d.GatewayManager.SubscribeGuild(
131 | Channel.GuildID, true, true,
132 | )
133 |
134 | members := &([]*discordgo.Member{})
135 |
136 | guild, err := d.State.Guild(ch.GuildID)
137 | if err != nil {
138 | if guild, err = d.Guild(ch.GuildID); err != nil {
139 | Warn(err.Error())
140 | return
141 | }
142 | }
143 |
144 | log.Println("Halting stage 4...")
145 | return
146 |
147 | recurseMembers(members, ch.GuildID, 0)
148 |
149 | log.Println("Booted stage 4")
150 | time.Sleep(2 * time.Second)
151 |
152 | guild.Members = *members
153 |
154 | roles := guild.Roles
155 | sort.Slice(roles, func(i, j int) bool {
156 | return roles[i].Position > roles[j].Position
157 | })
158 |
159 | for _, m := range *members {
160 | color := defaultNameColor
161 |
162 | RoleLoop:
163 | for _, role := range roles {
164 | for _, roleID := range m.Roles {
165 | if role.ID == roleID && role.Color != 0 {
166 | color = role.Color
167 | break RoleLoop
168 | }
169 | }
170 | }
171 |
172 | us.UpdateUser(
173 | guild.ID,
174 | m.User.ID,
175 | m.User.Username,
176 | m.Nick,
177 | m.User.Discriminator,
178 | color,
179 | )
180 | }
181 | }()
182 | }
183 |
184 | func drawMsgs(msgs []*discordgo.Message) {
185 | sort.Slice(msgs, func(i, j int) bool {
186 | return msgs[i].ID < msgs[j].ID
187 | })
188 |
189 | // Clears the buffer
190 | messageRender <- nil
191 |
192 | for i := 0; i < len(msgs); i++ {
193 | imageRendererPipeline.cache.markUnfetch(msgs[i])
194 | }
195 |
196 | for i := 0; i < len(msgs); i++ {
197 | m := msgs[i]
198 |
199 | if rstore.Check(m.Author, RelationshipBlocked) {
200 | continue
201 | }
202 |
203 | messageRender <- m
204 | }
205 | }
206 |
207 | func generateTitle(ch *discordgo.Channel, custom ...string) (frameTitle string) {
208 | if ch == nil {
209 | return "[#nil[]"
210 | }
211 |
212 | var Custom = strings.Join(custom, " ")
213 |
214 | if ch.Name != "" {
215 | frameTitle = "[#" + ch.Name + "]"
216 |
217 | if Custom != "" {
218 | frameTitle += " - " + Custom
219 | } else if ch.Topic != "" {
220 | topic, _ := parseEmojis(ch.Topic)
221 | frameTitle += " - [" + tview.Escape(topic) + "]"
222 | }
223 | } else {
224 | if len(ch.Recipients) == 1 {
225 | frameTitle = "[" + ch.Recipients[0].String() + "]"
226 | } else if Custom != "" {
227 | frameTitle += " - " + Custom
228 | } else {
229 | var names = make([]string, len(ch.Recipients))
230 | for i, r := range ch.Recipients {
231 | names[i] = r.Username
232 | }
233 |
234 | frameTitle = "[" + HumanizeStrings(names) + "]"
235 | }
236 | }
237 |
238 | return
239 | }
240 |
241 | func messageisOld(m, l *discordgo.Message) bool {
242 | if m == nil || l == nil {
243 | return true
244 | }
245 |
246 | mt, err := m.Timestamp.Parse()
247 | if err != nil {
248 | return true
249 | }
250 |
251 | lt, err := l.Timestamp.Parse()
252 | if err != nil {
253 | return true
254 | }
255 |
256 | return lt.Add(time.Minute).Before(mt)
257 | }
258 |
259 | func recurseMembers(memstore *[]*discordgo.Member, guildID, after int64) {
260 | return
261 |
262 | /*
263 | members, err := d.GuildMembers(guildID, after, 1000)
264 | if err != nil {
265 | log.Println(err)
266 | return
267 | }
268 |
269 | if len(members) == 1000 {
270 | recurseMembers(
271 | memstore,
272 | guildID,
273 | members[999].User.ID,
274 | )
275 | }
276 |
277 | *memstore = append(*memstore, members...)
278 |
279 | return
280 | */
281 | }
282 |
--------------------------------------------------------------------------------
/login.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/diamondburned/discordgo"
7 | "github.com/diamondburned/tcell"
8 | "github.com/diamondburned/tview/v2"
9 | "gitlab.com/diamondburned/6cord/center"
10 | )
11 |
12 | func promptLogin(l *discordgo.Login, text string, mfa bool) (ok bool) {
13 | flex := tview.NewFlex()
14 | flex.SetDirection(tview.FlexRow)
15 | flex.AddItem(tview.NewTextView().SetText(text), 1, 1, false)
16 | flex.SetBackgroundColor(-1)
17 |
18 | f := tview.NewForm()
19 | f.SetBackgroundColor(tcell.Color237)
20 | f.SetButtonBackgroundColor(tcell.Color255)
21 | f.SetButtonTextColor(tcell.Color237)
22 |
23 | // Field
24 | f.SetFieldBackgroundColor(tcell.Color248)
25 | f.SetFieldTextColor(tcell.Color237)
26 |
27 | f.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
28 | switch ev.Key() {
29 | case tcell.KeyCtrlC:
30 | app.Stop()
31 | }
32 |
33 | return nil
34 | })
35 |
36 | if !mfa {
37 | f.AddInputField(
38 | "Email ", l.Email, 59, nil,
39 | func(s string) { l.Email = s },
40 | )
41 |
42 | f.AddPasswordField(
43 | "Password", l.Password, 59, '*',
44 | func(s string) { l.Password = s },
45 | )
46 | } else {
47 | f.AddInputField(
48 | "MFA ", l.MFA, 59, nil,
49 | func(s string) { l.MFA = s },
50 | )
51 | }
52 |
53 | f.AddButton("Login", func() {
54 | ok = true
55 | app.Stop()
56 | })
57 |
58 | f.SetCancelFunc(func() {
59 | app.Stop()
60 | })
61 |
62 | flex.AddItem(f, 0, 1, true)
63 |
64 | center := center.New(flex)
65 | center.MaxWidth = 70
66 | center.MaxHeight = 10
67 |
68 | app.SetRoot(center, true)
69 | if err := app.Run(); err != nil {
70 | fmt.Println(err.Error())
71 | panic(err)
72 | }
73 |
74 | return
75 | }
76 |
--------------------------------------------------------------------------------
/math.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | func max(x, y int) int {
4 | if x > y {
5 | return x
6 | }
7 | return y
8 | }
9 |
10 | func min(x, y int) int {
11 | if x < y {
12 | return x
13 | }
14 | return y
15 | }
16 |
--------------------------------------------------------------------------------
/md/highlighter.go:
--------------------------------------------------------------------------------
1 | package md
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/alecthomas/chroma"
7 | "github.com/alecthomas/chroma/formatters"
8 | "github.com/alecthomas/chroma/lexers"
9 | "github.com/alecthomas/chroma/styles"
10 | )
11 |
12 | // HighlightStyle determines the syntax highlighting colorstyle:
13 | // https://xyproto.github.io/splash/docs/all.html
14 | var HighlightStyle = "vs"
15 |
16 | var (
17 | style *chroma.Style
18 | fmtter chroma.Formatter
19 | )
20 |
21 | // RenderCodeBlock renders the node to a syntax
22 | // highlighted code
23 | func RenderCodeBlock(lang, content string) string {
24 | // Get the highlight style
25 | if style == nil {
26 | // Try and find it
27 | if s := styles.Get(HighlightStyle); s != nil {
28 | style = s
29 | } else {
30 | // Can't find, use fallback
31 | style = styles.Fallback
32 | }
33 | }
34 |
35 | if fmtter == nil {
36 | fmtter = formatters.Get("tview-256bit")
37 | }
38 |
39 | var lexer = lexers.Fallback
40 | if lang := string(lang); lang != "" {
41 | if l := lexers.Get(lang); l != nil {
42 | lexer = l
43 | } else {
44 | content = lang + "\n" + content
45 | }
46 | }
47 |
48 | iterator, err := lexer.Tokenise(nil, content)
49 | if err != nil {
50 | return wrapBlock(content)
51 | }
52 |
53 | var code strings.Builder
54 |
55 | if err := fmtter.Format(&code, style, iterator); err != nil {
56 | return wrapBlock(content)
57 | }
58 |
59 | return wrapBlock(code.String())
60 | }
61 |
62 | func wrapBlock(content string) string {
63 | var s strings.Builder
64 |
65 | // wrapped := tview.WordWrap(code.String(), 80)
66 | wrapped := strings.Split(content, "\n")
67 |
68 | for i := 0; i < len(wrapped); i++ {
69 | if wrapped[i] != "[-]" {
70 | s.WriteString("\n[grey]┃[-] " + wrapped[i])
71 | }
72 | }
73 |
74 | return s.String()[1:] + "\n"
75 | }
76 |
--------------------------------------------------------------------------------
/md/md.go:
--------------------------------------------------------------------------------
1 | package md
2 |
3 | import (
4 | "regexp"
5 | "strings"
6 |
7 | "github.com/diamondburned/tview/v2"
8 | "gitlab.com/diamondburned/6cord/shortener"
9 | )
10 |
11 | const mdRegex = `(?m)(?:^\x60\x60\x60 *(\w*)\n([\s\S]*?)\n\x60\x60\x60$)|((?:(?:^|\n)>\s+.*)+)|(?:(?:^|\n)(?:[>*+-]|\d+\.)\s+.*)+|(?:\x60([^\x60].*?)\x60)|(__|\*\*\*|\*\*|[_*]|~~|\|\|)|(https?:\/\S+(?:\.|:)\S+)`
12 |
13 | var r1 = regexp.MustCompile(mdRegex)
14 |
15 | type match struct {
16 | from, to int
17 | str string
18 | }
19 |
20 | type mdState struct {
21 | strings.Builder
22 | matches [][]match
23 | last int
24 | chunk string
25 | prev string
26 | context []string
27 | }
28 |
29 | func (s *mdState) tag(token string) string {
30 | var tags [2]string
31 |
32 | switch token {
33 | case "*":
34 | tags[0] = "[::i]"
35 | tags[1] = "[::-]"
36 | case "_":
37 | tags[0] = "[::i]"
38 | tags[1] = "[::-]"
39 | case "**":
40 | tags[0] = "[::b]"
41 | tags[1] = "[::-]"
42 | case "__":
43 | tags[0] = "[::u]"
44 | tags[1] = "[::-]"
45 | case "***":
46 | tags[0] = "[::ib]"
47 | tags[1] = "[::-]"
48 | case "~~":
49 | tags[0] = "[::s]"
50 | tags[1] = "[::-]"
51 | case "||":
52 | tags[0] = "[#777777]"
53 | tags[1] = "[-]"
54 | default:
55 | return token
56 | }
57 |
58 | var index = -1
59 | for i, t := range s.context {
60 | if t == token {
61 | index = i
62 | break
63 | }
64 | }
65 |
66 | if index >= 0 { // len(context) > 0 always
67 | s.context = append(s.context[:index], s.context[index+1:]...)
68 | return tags[1]
69 | } else {
70 | s.context = append(s.context, token)
71 | return tags[0]
72 | }
73 | }
74 |
75 | func (s mdState) getLastIndex(currentIndex int) int {
76 | if currentIndex >= len(s.matches) {
77 | return 0
78 | }
79 |
80 | return s.matches[currentIndex][0].to
81 | }
82 |
83 | func Parse(md string) string {
84 | var s mdState
85 | s.matches = submatch(r1, md)
86 |
87 | for i := 0; i < len(s.matches); i++ {
88 | s.prev = md[s.last:s.matches[i][0].from]
89 | s.last = s.getLastIndex(i)
90 | s.chunk = "" // reset chunk
91 |
92 | switch {
93 | case strings.Count(s.prev, "\\")%2 != 0:
94 | // escaped, print raw
95 | s.chunk = tview.Escape(s.matches[i][0].str)
96 | case s.matches[i][2].str != "":
97 | // codeblock
98 | s.chunk = RenderCodeBlock(
99 | tview.Escape(s.matches[i][1].str),
100 | tview.Escape(s.matches[i][2].str),
101 | )
102 | case s.matches[i][3].str != "":
103 | // blockquotes, greentext
104 | s.chunk = "\n[#789922]" +
105 | tview.Escape(strings.TrimPrefix(s.matches[i][3].str, "\n")) +
106 | "[-]"
107 | case s.matches[i][4].str != "":
108 | // inline code
109 | s.chunk = "[:#4f4f4f:]" + tview.Escape(s.matches[i][4].str) + "[:-:]"
110 | case s.matches[i][5].str != "":
111 | // inline stuff
112 | s.chunk = s.tag(s.matches[i][5].str)
113 | case s.matches[i][6].str != "":
114 | s.chunk = shortener.ShortenURL(s.matches[i][6].str)
115 | default:
116 | s.chunk = tview.Escape(s.matches[i][0].str)
117 | }
118 |
119 | s.WriteString(tview.Escape(s.prev))
120 | s.WriteString(s.chunk)
121 | }
122 |
123 | s.WriteString(md[s.last:])
124 |
125 | for len(s.context) > 0 {
126 | s.WriteString(s.tag(s.context[len(s.context)-1]))
127 | }
128 |
129 | return strings.TrimSpace(s.String())
130 | }
131 |
132 | func submatch(r *regexp.Regexp, s string) [][]match {
133 | found := r.FindAllStringSubmatchIndex(s, -1)
134 | indices := make([][]match, len(found))
135 |
136 | var m = match{-1, -1, ""}
137 |
138 | for i := range found {
139 | indices[i] = make([]match, len(found[i])/2)
140 |
141 | for a, b := range found[i] {
142 | if a%2 == 0 { // first pair
143 | m.from = b
144 | } else {
145 | m.to = b
146 |
147 | if m.from >= 0 && m.to >= 0 {
148 | m.str = s[m.from:m.to]
149 | } else {
150 | m.str = ""
151 | }
152 |
153 | indices[i][a/2] = m
154 | }
155 | }
156 | }
157 |
158 | return indices
159 | }
160 |
--------------------------------------------------------------------------------
/md/md_test.go:
--------------------------------------------------------------------------------
1 | package md
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/go-test/deep"
7 | )
8 |
9 | const s = `**test**
10 |
11 | __bro__, that is ***cringe!***
12 |
13 | ****test****
14 |
15 | ` + "```" + `go
16 | println('a')
17 | ` + "```" + `
18 |
19 | > cringe lmao
20 | > kek
21 |
22 | >lol
23 | > cringe
24 |
25 | __**test**__
26 |
27 | \*lol\*`
28 |
29 | const results = `[::b]test[::-]
30 |
31 | [::u]bro[::-], that is [::ib]cringe![::-]
32 |
33 | [::ib][::i]test[::-][::-]
34 |
35 | [grey]┃[-] println([#af0000]'a'[-])
36 |
37 | [#789922]
38 | > cringe lmao
39 | > kek[-]
40 |
41 | >lol[#789922]
42 | > cringe[-]
43 |
44 | [::u][::b]test[::-][::-]
45 |
46 | \*lol\*`
47 |
48 | func TestParse(t *testing.T) {
49 | html := Parse(s)
50 | if diff := deep.Equal(html, results); diff != nil {
51 | t.Fatal(diff)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/mentions.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/diamondburned/discordgo"
7 | "github.com/jonas747/gojay"
8 | )
9 |
10 | const mentionsEndpoint = "https://discordapp.com/api/v6/users/@me/mentions?limit=%d&roles=true&everyone=true"
11 |
12 | func getMentions() (ms []*discordgo.Message, err error) {
13 | resp, err := d.Request(
14 | "GET",
15 | fmt.Sprintf(mentionsEndpoint, 25),
16 | nil,
17 | )
18 |
19 | if err != nil {
20 | return
21 | }
22 |
23 | err = gojay.Unmarshal(resp, &ms)
24 | return
25 | }
26 |
--------------------------------------------------------------------------------
/message.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | // hopefully avoids messagesView going out of range after a
8 | // while
9 | func cleanupBuffer() {
10 | if len(messageStore) > prefetchMessageCount*6 {
11 | messageStore = messageStore[prefetchMessageCount*4:]
12 |
13 | messagesView.SetText(
14 | strings.Join(messageStore, ""),
15 | )
16 | }
17 | }
18 |
19 | func scrollChat() (clear bool) {
20 | // If the message box is not focused and the input is empty
21 | if !messagesView.HasFocus() && input.GetText() == "" {
22 | clear = true
23 | cleanupBuffer()
24 | }
25 |
26 | current, lines := getLineStatus()
27 | if lines-current > 5 {
28 | return false
29 | }
30 |
31 | if clear {
32 | messagesView.ScrollToEnd()
33 | }
34 |
35 | if Channel == nil {
36 | wrapFrame.SetTitle(generateTitle(Channel))
37 | }
38 |
39 | return
40 | }
41 |
--------------------------------------------------------------------------------
/messagecreate.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "github.com/diamondburned/discordgo"
8 | )
9 |
10 | var (
11 | highlightInterval = time.Duration(time.Second * 7)
12 | messageStore []string
13 | )
14 |
15 | func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
16 | if d == nil || Channel == nil {
17 | return
18 | }
19 |
20 | if rstore.Check(m.Author, RelationshipBlocked) && cfg.Prop.HideBlocked {
21 | return
22 | }
23 |
24 | // Notify mentions
25 | go mentionHandler(m)
26 |
27 | if m.ChannelID != Channel.ID {
28 | c, err := d.State.Channel(m.ChannelID)
29 | if err == nil {
30 | c.LastMessageID = m.ID
31 |
32 | } else {
33 | log.Println(err)
34 | }
35 |
36 | markUnread(m.Message)
37 |
38 | return
39 | }
40 |
41 | // ackMe(m.ChannelID, m.ID)
42 |
43 | typing.RemoveUser(&discordgo.TypingStart{
44 | UserID: m.Author.ID,
45 | ChannelID: m.ChannelID,
46 | })
47 |
48 | // messagerenderer.go
49 | messageRender <- m
50 | }
51 |
--------------------------------------------------------------------------------
/messagedelete.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/diamondburned/discordgo"
5 | )
6 |
7 | func messageDelete(s *discordgo.Session, rm *discordgo.MessageDelete) {
8 | if d == nil || Channel == nil {
9 | return
10 | }
11 |
12 | if rm.ChannelID != Channel.ID {
13 | return
14 | }
15 |
16 | messageRender <- rm
17 | }
18 |
19 | func messageDeleteBulk(s *discordgo.Session, rmb *discordgo.MessageDeleteBulk) {
20 | if d == nil || Channel == nil {
21 | return
22 | }
23 |
24 | if rmb.ChannelID != Channel.ID {
25 | return
26 | }
27 |
28 | for _, m := range rmb.Messages {
29 | messageRender <- &discordgo.MessageDelete{
30 | Message: &discordgo.Message{
31 | ChannelID: rmb.ChannelID,
32 | ID: m,
33 | },
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/messagerenderer.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "strconv"
7 | "strings"
8 | "time"
9 |
10 | "github.com/diamondburned/discordgo"
11 | )
12 |
13 | var (
14 | messageRender = make(chan interface{}, 12)
15 | )
16 |
17 | // Function takes in a messageCreate buffer
18 | func messageRenderer() {
19 | var lastmsg *discordgo.Message
20 |
21 | for i := range messageRender {
22 | switch m := i.(type) {
23 | case *discordgo.MessageCreate:
24 | imageRendererPipeline.cache.markUnfetch(m.Message)
25 | rendererCreate(m.Message, lastmsg)
26 |
27 | lastmsg = m.Message
28 | if m.Author.ID == d.State.User.ID {
29 | messagesView.ScrollToEnd()
30 | } else if !scrollChat() {
31 | messagesView.SetTitle(
32 | generateTitle(Channel, "[red]Unread messages[-]"),
33 | )
34 | }
35 |
36 | case *discordgo.Message:
37 | rendererCreate(m, lastmsg)
38 |
39 | lastmsg = m
40 | messagesView.ScrollToEnd()
41 |
42 | case *discordgo.MessageDelete:
43 | for i := len(messageStore) - 1; i >= 0; i-- {
44 | if ID := getIDfromindex(i); ID != 0 {
45 | if m.ID != ID {
46 | continue
47 | }
48 |
49 | prev := 0
50 |
51 | if (i > 1 && i == len(messageStore)-1 && strings.HasPrefix(messageStore[i-1], authorPrefix)) ||
52 | (i > 0 &&
53 | strings.HasPrefix(messageStore[i-1], authorPrefix) &&
54 | !strings.HasPrefix(messageStore[i+1], messageRawFormat[:3]) &&
55 | i != len(messageStore)-1) {
56 |
57 | prev = 1
58 | }
59 |
60 | messageStore = append(
61 | messageStore[:i-prev],
62 | messageStore[i+1:]...,
63 | )
64 |
65 | messagesView.SetText(strings.Join(messageStore, ""))
66 | break
67 | }
68 | }
69 |
70 | lastmsg = nil
71 |
72 | case *discordgo.MessageUpdate:
73 | message, err := d.State.Message(Channel.ID, m.ID)
74 | if err != nil {
75 | Warn(err.Error())
76 | break
77 | }
78 |
79 | id := strconv.FormatInt(m.ID, 10)
80 | for i, msg := range messageStore {
81 | if strings.HasPrefix(msg, messageRawFormat[:3]+id+"\"]") {
82 | msg := messageTmpl.ExecuteString(map[string]interface{}{
83 | "ID": id,
84 | "content": fmtMessage(message),
85 | })
86 |
87 | messageStore[i] = msg
88 |
89 | messagesView.SetText(strings.Join(messageStore, ""))
90 | break
91 | }
92 | }
93 |
94 | case string:
95 | msg := authorTmpl.ExecuteString(map[string]interface{}{
96 | "color": fmtHex(defaultNameColor),
97 | "name": "Not Clyde",
98 | "time": time.Now().Format(time.Stamp),
99 | })
100 |
101 | var (
102 | l = strings.Split(m, "\n")
103 | c []string
104 | )
105 |
106 | for i := 0; i < len(l); i++ {
107 | c = append(c, chatPadding+l[i])
108 | }
109 |
110 | msg += messageTmpl.ExecuteString(map[string]interface{}{
111 | "ID": "0",
112 | "content": strings.Join(c, "\n"),
113 | })
114 |
115 | messagesView.Write([]byte(msg))
116 | messageStore = append(messageStore, msg)
117 |
118 | scrollChat()
119 | lastmsg = nil
120 |
121 | case nil:
122 | messagesView.Clear()
123 | messageStore = make([]string, 0, prefetchMessageCount*2)
124 | imageRendererPipeline.cache.gc()
125 |
126 | default:
127 | Warn(fmt.Sprintf("Message renderer received event type:\n%T", i))
128 | log.Println(fmt.Sprintf("%#v", i))
129 |
130 | continue
131 | }
132 |
133 | app.Draw()
134 | }
135 | }
136 |
137 | func rendererCreate(m, lastmsg *discordgo.Message) {
138 | if m.Type != discordgo.MessageTypeDefault {
139 | var messageText string
140 |
141 | // https://github.com/Bios-Marcel/cordless
142 | switch m.Type {
143 | case discordgo.MessageTypeGuildMemberJoin:
144 | messageText = "joined the server."
145 | case discordgo.MessageTypeCall:
146 | messageText = "is calling you."
147 | case discordgo.MessageTypeChannelIconChange:
148 | messageText = "changed the channel icon."
149 | case discordgo.MessageTypeChannelNameChange:
150 | messageText = "changed the channel name to " + m.Content + "."
151 | case discordgo.MessageTypeChannelPinnedMessage:
152 | messageText = fmt.Sprintf("pinned message %d.", m.ID)
153 | case discordgo.MessageTypeRecipientAdd:
154 | messageText = "added " + m.Mentions[0].Username + " to the group."
155 | case discordgo.MessageTypeRecipientRemove:
156 | messageText = "removed " + m.Mentions[0].Username + " from the group."
157 | }
158 |
159 | if messageText != "" {
160 | msg := fmt.Sprintf(
161 | "\n\n[::d][\"%d\"]%s %s[\"\"][::-]",
162 | m.ID, m.Author.Username, messageText,
163 | )
164 |
165 | messagesView.Write([]byte(msg))
166 | messageStore = append(messageStore, msg)
167 | }
168 |
169 | return
170 | }
171 |
172 | msgFmt := messageTmpl.ExecuteString(map[string]interface{}{
173 | "ID": strconv.FormatInt(m.ID, 10),
174 | "content": fmtMessage(m),
175 | })
176 |
177 | go func() {
178 | if _, err := imageRendererPipeline.cache.upd(m); err != nil {
179 | Message(err.Error())
180 | }
181 | }()
182 |
183 | if cfg.Prop.CompactMode || (lastmsg == nil ||
184 | (lastmsg.Author.ID != m.Author.ID || messageisOld(m, lastmsg))) {
185 |
186 | sentTime, err := m.Timestamp.Parse()
187 | if err != nil {
188 | sentTime = time.Now()
189 | }
190 |
191 | username, color := us.DiscordThis(m)
192 |
193 | msg := authorTmpl.ExecuteString(map[string]interface{}{
194 | "color": fmtHex(color),
195 | "name": username,
196 | "time": sentTime.Format(time.Stamp),
197 | })
198 |
199 | messagesView.Write([]byte(msg + msgFmt))
200 | messageStore = append(messageStore, msg, msgFmt)
201 | } else {
202 | messagesView.Write([]byte(msgFmt))
203 | messageStore = append(messageStore, msgFmt)
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/messagescroll.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | "time"
7 | )
8 |
9 | func messageGetTopID() int64 {
10 | if len(messageStore) < 1 {
11 | return 0
12 | }
13 |
14 | for i := 0; i < len(messageStore); i++ {
15 | if index := getIDfromindex(i); index != 0 {
16 | return index
17 | }
18 | }
19 |
20 | return 0
21 | }
22 |
23 | func getIDfromindex(i int) int64 {
24 | if len(messageStore) <= i || i < 0 {
25 | return 0
26 | }
27 |
28 | // magic number lo
29 | if len(messageStore[i]) < 23 {
30 | return 0
31 | }
32 |
33 | switch {
34 | case messageStore[i][1] != '[':
35 | return 0
36 | case messageStore[i][2] != '"':
37 | return 0
38 | }
39 |
40 | var (
41 | idRune strings.Builder
42 | msg = messageStore[i]
43 | )
44 |
45 | for i := 3; i < len(msg); i++ {
46 | if msg[i] == '"' {
47 | break
48 | }
49 |
50 | idRune.WriteByte(msg[i])
51 | }
52 |
53 | r, _ := strconv.ParseInt(idRune.String(), 10, 64)
54 | return r
55 | }
56 |
57 | var loading bool
58 |
59 | func loadMore() {
60 | if d == nil || Channel == nil {
61 | return
62 | }
63 |
64 | if loading {
65 | return
66 | }
67 |
68 | beforeID := messageGetTopID()
69 | if beforeID == 0 {
70 | return
71 | }
72 |
73 | loading = true
74 | input.SetPlaceholder("Loading more...")
75 |
76 | defer func() {
77 | input.SetPlaceholder(cfg.Prop.DefaultStatus)
78 | loading = false
79 | }()
80 |
81 | c, err := d.State.Channel(Channel.ID)
82 | if err != nil {
83 | Warn(err.Error())
84 | return
85 | }
86 |
87 | msgs, err := d.ChannelMessages(Channel.ID, 35, beforeID, 0, 0)
88 | if err != nil {
89 | return
90 | }
91 |
92 | if len(msgs) == 0 {
93 | // Drop out early if no messages
94 | return
95 | }
96 |
97 | var reversed = make([]string, 0, len(msgs)*2)
98 |
99 | for i := len(msgs) - 1; i >= 0; i-- {
100 | m := msgs[i]
101 |
102 | if rstore.Check(m.Author, RelationshipBlocked) && cfg.Prop.HideBlocked {
103 | continue
104 | }
105 |
106 | sentTime, err := m.Timestamp.Parse()
107 | if err != nil {
108 | sentTime = time.Now()
109 | }
110 |
111 | if i < len(msgs)-1 && (msgs[i+1].Author.ID != m.Author.ID || messageisOld(m, msgs[i+1])) {
112 | username, color := us.DiscordThis(m)
113 |
114 | reversed = append(reversed,
115 | authorTmpl.ExecuteString(map[string]interface{}{
116 | "color": fmtHex(color),
117 | "name": username,
118 | "time": sentTime.Format(time.Stamp),
119 | }),
120 | )
121 | }
122 |
123 | reversed = append(reversed,
124 | messageTmpl.ExecuteString(map[string]interface{}{
125 | "ID": strconv.FormatInt(m.ID, 10),
126 | "content": fmtMessage(m),
127 | }),
128 | )
129 | }
130 |
131 | //wg.Wait()
132 |
133 | messageStore = append(reversed, messageStore...)
134 | messagesView.SetText(strings.Join(messageStore, ""))
135 |
136 | input.SetPlaceholder("Done.")
137 |
138 | for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 {
139 | msgs[i], msgs[j] = msgs[j], msgs[i]
140 | }
141 |
142 | d.State.Lock()
143 | c.Messages = append(msgs, c.Messages...)
144 | d.State.Unlock()
145 |
146 | messagesView.Highlight(strconv.FormatInt(beforeID, 10))
147 | messagesView.ScrollToHighlight()
148 |
149 | time.Sleep(time.Second * 5)
150 | }
151 |
--------------------------------------------------------------------------------
/messageupdate.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/diamondburned/discordgo"
5 | )
6 |
7 | func messageUpdate(s *discordgo.Session, u *discordgo.MessageUpdate) {
8 | if d == nil || Channel == nil {
9 | return
10 | }
11 |
12 | if Channel.ID != u.ChannelID {
13 | return
14 | }
15 |
16 | m, err := d.State.Message(Channel.ID, u.ID)
17 | if err != nil {
18 | Warn(err.Error())
19 | return
20 | }
21 |
22 | if rstore.Check(m.Author, RelationshipBlocked) && cfg.Prop.HideBlocked {
23 | return
24 | }
25 |
26 | messageRender <- u
27 | }
28 |
--------------------------------------------------------------------------------
/notify.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "html"
5 | "strings"
6 |
7 | "github.com/diamondburned/discordgo"
8 | "github.com/diamondburned/tview/v2"
9 | "github.com/gen2brain/beeep"
10 | )
11 |
12 | const pingHighlightNode = " [#ED2939](!)[-]"
13 |
14 | func mentionHandler(m *discordgo.MessageCreate) {
15 | // Crash-prevention
16 | if d.State.Settings == nil {
17 | return
18 | }
19 |
20 | var (
21 | submessage = "said in a heated channel"
22 | name = m.Author.Username
23 | pinged bool
24 | )
25 |
26 | if m.Author.ID != d.State.User.ID {
27 | if heatedChannelsExists(m.ChannelID) {
28 | goto Notify
29 | }
30 | }
31 |
32 | if !messagePingable(m.Message, m.GuildID) {
33 | return
34 | }
35 |
36 | pinged = true
37 |
38 | Notify:
39 | if c, err := d.State.Channel(m.ChannelID); err == nil {
40 | switch {
41 | case !pinged:
42 | submessage = "said in a heated channel"
43 | case len(c.Recipients) > 0:
44 | submessage = "messaged"
45 | default:
46 | submessage = "mentioned you"
47 | }
48 |
49 | if c.Name != "" {
50 | submessage += " in #" + c.Name
51 |
52 | m, err := d.State.Member(c.GuildID, m.Author.ID)
53 | if err == nil {
54 | if m.Nick != "" {
55 | name = m.Nick
56 | }
57 | }
58 |
59 | } else {
60 | if len(c.Recipients) > 1 {
61 | var names = make([]string, len(c.Recipients))
62 |
63 | for i, p := range c.Recipients {
64 | names[i] = p.Username
65 | }
66 |
67 | submessage += " in " + HumanizeStrings(names)
68 | }
69 | }
70 | }
71 |
72 | // Skip if user is busy
73 | if d.State.Settings.Status != discordgo.StatusDoNotDisturb || !pinged {
74 | // we ignore errors for users without dbus/notify-send
75 | beeep.Notify(
76 | name+" "+submessage,
77 | html.EscapeString(m.ContentWithMentionsReplaced()),
78 | "",
79 | )
80 |
81 | // if it's a heat signal
82 | if !pinged {
83 | return
84 | }
85 | }
86 |
87 | // Walk the tree for the sake of a (1)
88 |
89 | if Channel != nil && m.ChannelID == Channel.ID {
90 | return
91 | }
92 |
93 | root := guildView.GetRoot()
94 | if root == nil {
95 | return
96 | }
97 |
98 | root.Walk(func(node, parent *tview.TreeNode) bool {
99 | if parent == nil {
100 | return true
101 | }
102 |
103 | reference, ok := node.GetReference().(*discordgo.Channel)
104 | if !ok {
105 | return true
106 | }
107 |
108 | if reference.ID != m.ChannelID {
109 | return false
110 | }
111 |
112 | pingNode := tview.NewTreeNode(
113 | "[#ED2939]" + tview.Escape(name) + "[-] mentioned you",
114 | )
115 |
116 | pingNode.SetSelectable(false)
117 | pingNode.SetIndent(cfg.Prop.SidebarIndent - 1)
118 |
119 | node.AddChild(pingNode)
120 | node.Expand()
121 |
122 | if _, ok := parent.GetReference().(*discordgo.Guild); ok {
123 | if !strings.HasSuffix(parent.GetText(), pingHighlightNode) {
124 | parent.SetText(parent.GetText() + pingHighlightNode)
125 | }
126 | }
127 |
128 | return false
129 | })
130 |
131 | app.Draw()
132 | }
133 |
--------------------------------------------------------------------------------
/onready.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "sort"
6 |
7 | "github.com/diamondburned/discordgo"
8 | "github.com/diamondburned/tcell"
9 | "github.com/diamondburned/tview/v2"
10 | "gitlab.com/diamondburned/6cord/keyring"
11 | )
12 |
13 | func onReady(s *discordgo.Session, r *discordgo.Ready) {
14 | // Store the token in a keyring
15 | go func() {
16 | log.Println("Storing token inside keyring...")
17 | keyring.Set(d.Token)
18 | }()
19 |
20 | rstore.Relationships = r.Relationships
21 |
22 | // loadChannel()
23 |
24 | guildView.SetSingleClick(true)
25 |
26 | guildNode := tview.NewTreeNode("Guilds")
27 | guildNode.SetColor(tcell.Color(cfg.Prop.ForegroundColor))
28 | guildNode.SetSelectedColor(tcell.ColorBlack)
29 |
30 | pNode := tview.NewTreeNode("Direct Messages")
31 | pNode.SetReference("Direct Messages")
32 | pNode.Collapse()
33 | pNode.SetColor(tcell.Color(cfg.Prop.ForegroundColor))
34 | pNode.SetSelectedColor(tcell.ColorBlack)
35 |
36 | // https://github.com/Bios-Marcel/cordless
37 | sort.Slice(r.PrivateChannels, func(a, b int) bool {
38 | channelA := r.PrivateChannels[a]
39 | channelB := r.PrivateChannels[b]
40 |
41 | return channelA.LastMessageID > channelB.LastMessageID
42 | })
43 |
44 | for _, ch := range r.PrivateChannels {
45 | var display = generateName(ch) + "[-::-]"
46 |
47 | if isUnread(ch) {
48 | display = unreadChannelColorPrefix + display
49 | } else {
50 | display = readChannelColorPrefix + display
51 | }
52 |
53 | chNode := tview.NewTreeNode(display)
54 | chNode.SetReference(ch)
55 | chNode.SetColor(tcell.Color(cfg.Prop.ForegroundColor))
56 | chNode.SetSelectedColor(tcell.ColorBlack)
57 | chNode.SetIndent(cfg.Prop.SidebarIndent - 1)
58 |
59 | pNode.AddChild(chNode)
60 | }
61 |
62 | guildNode.AddChild(pNode)
63 | guildView.SetCurrentNode(pNode)
64 |
65 | // https://github.com/Bios-Marcel/cordless
66 | sort.Slice(r.Guilds, func(a, b int) bool {
67 | aFound := false
68 | for _, guild := range r.Settings.GuildPositions {
69 | if aFound {
70 | if guild == r.Guilds[b].ID {
71 | return true
72 | }
73 | } else {
74 | if guild == r.Guilds[a].ID {
75 | aFound = true
76 | }
77 | }
78 | }
79 |
80 | return false
81 | })
82 |
83 | for _, g := range r.Guilds {
84 | this := tview.NewTreeNode(readChannelColorPrefix + g.Name + "[-::-]")
85 | this.SetReference(g)
86 | this.Collapse()
87 | this.SetColor(tcell.Color(cfg.Prop.ForegroundColor))
88 | this.SetSelectedColor(tcell.ColorBlack)
89 |
90 | sorted := SortChannels(g.Channels)
91 |
92 | for _, ch := range sorted {
93 | if !isValidCh(ch.Type) {
94 | continue
95 | }
96 |
97 | perm, err := d.State.UserChannelPermissions(
98 | d.State.User.ID,
99 | ch.ID,
100 | )
101 |
102 | if err != nil {
103 | continue
104 | }
105 |
106 | if perm&discordgo.PermissionReadMessages == 0 {
107 | continue
108 | }
109 |
110 | var name = generateName(ch)
111 |
112 | switch ch.Type {
113 | case discordgo.ChannelTypeGuildCategory:
114 | chNode := tview.NewTreeNode(unreadChannelColorPrefix + name + "[::-]")
115 | chNode.SetSelectable(false)
116 | chNode.SetColor(tcell.Color(cfg.Prop.ForegroundColor))
117 | chNode.SetSelectedColor(tcell.ColorBlack)
118 | chNode.SetIndent(cfg.Prop.SidebarIndent - 1)
119 |
120 | this.AddChild(chNode)
121 |
122 | case discordgo.ChannelTypeGuildVoice:
123 | chNode := tview.NewTreeNode("[-::-]" + name + "[-::-]")
124 | chNode.SetReference(ch)
125 | chNode.SetColor(tcell.Color(cfg.Prop.ForegroundColor))
126 | chNode.SetSelectedColor(tcell.ColorBlack)
127 |
128 | if ch.ParentID == 0 {
129 | chNode.SetIndent(cfg.Prop.SidebarIndent - 1)
130 | } else {
131 | chNode.SetIndent(cfg.Prop.SidebarIndent*2 - 1)
132 | }
133 |
134 | this.AddChild(chNode)
135 |
136 | for _, vc := range getVoiceChannel(ch.GuildID, ch.ID) {
137 | vcNode := generateVoiceNode(vc)
138 | if vcNode == nil {
139 | continue
140 | }
141 |
142 | chNode.AddChild(vcNode)
143 | }
144 |
145 | default:
146 | chNode := tview.NewTreeNode(readChannelColorPrefix + name + "[-::-]")
147 | chNode.SetReference(ch)
148 | chNode.SetColor(tcell.Color(cfg.Prop.ForegroundColor))
149 | chNode.SetSelectedColor(tcell.ColorBlack)
150 |
151 | if ch.ParentID == 0 {
152 | chNode.SetIndent(cfg.Prop.SidebarIndent - 1)
153 | } else {
154 | chNode.SetIndent(cfg.Prop.SidebarIndent*2 - 1)
155 | }
156 |
157 | this.AddChild(chNode)
158 | }
159 | }
160 |
161 | checkGuildNode(g, this)
162 | guildNode.AddChild(this)
163 | }
164 |
165 | guildView.SetRoot(guildNode)
166 | guildView.SetMouseFunc(func(ev *tcell.EventMouse) bool {
167 | return false
168 | })
169 | guildView.SetSelectedFunc(func(node *tview.TreeNode) {
170 | reference := node.GetReference()
171 | if reference == nil {
172 | CollapseAll(guildNode)
173 | node.SetExpanded(!node.IsExpanded())
174 | return
175 | }
176 |
177 | switch r := reference.(type) {
178 | case nil:
179 |
180 | case *discordgo.Channel:
181 | node.SetChildren(nil)
182 | loadChannel(r.ID)
183 |
184 | case *discordgo.Guild:
185 | node.SetText(readChannelColorPrefix + r.Name + "[-::-]")
186 |
187 | if !node.IsExpanded() {
188 | CollapseAll(guildNode)
189 | node.SetExpanded(true)
190 | } else {
191 | node.SetExpanded(false)
192 | }
193 |
194 | checkGuildNode(r, node)
195 |
196 | default: // Private Channels
197 | children := pNode.GetChildren()
198 | n := make([]*tview.TreeNode, 0, len(children))
199 | for i, c := range children {
200 | if c == node {
201 | n = append(n, c)
202 | n = append(n, children[:i]...)
203 | n = append(n, children[i+1:]...)
204 |
205 | pNode.SetChildren(n)
206 | break
207 | }
208 | }
209 |
210 | if !node.IsExpanded() {
211 | CollapseAll(guildNode)
212 | node.SetExpanded(true)
213 | } else {
214 | node.SetExpanded(false)
215 | }
216 | }
217 | })
218 |
219 | guildView.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
220 | switch ev.Key() {
221 | case tcell.KeyRight, tcell.KeyCtrlL:
222 | app.SetFocus(input)
223 | return nil
224 | case tcell.KeyLeft:
225 | return nil
226 | case tcell.KeyTab:
227 | return nil
228 | case tcell.KeyCtrlD, tcell.KeyCtrlU:
229 | children := guildNode.GetChildren()
230 | _, _, _, height := messagesView.GetInnerRect()
231 |
232 | hop(children, func(i int) {
233 | // Change fn to up/down functions accordingly
234 | switch ev.Key() {
235 | case tcell.KeyCtrlU:
236 | // Make sure we can scroll up by the desired amount
237 | if i-height/2 >= 0 {
238 | guildView.SetCurrentNode(children[i-height/2])
239 | } else {
240 | guildView.SetCurrentNode(children[0])
241 | }
242 | case tcell.KeyCtrlD:
243 | // Make sure we can scroll down by the desired amount
244 | if i+height/2 < len(children) {
245 | guildView.SetCurrentNode(children[i+height/2])
246 | } else {
247 | guildView.SetCurrentNode(children[len(children)-1])
248 | }
249 | }
250 | })
251 |
252 | return nil
253 | case tcell.KeyCtrlJ, tcell.KeyCtrlK,
254 | tcell.KeyPgDn, tcell.KeyPgUp:
255 | children := guildNode.GetChildren()
256 |
257 | hop(children, func(i int) {
258 | // Change fn to up/down functions accordingly
259 | switch ev.Key() {
260 | case tcell.KeyCtrlK, tcell.KeyPgUp:
261 | // If we're not the first guild
262 | if i > 0 {
263 | // Collapse all nodes
264 | CollapseAll(guildNode)
265 | // Set the next node as expanded
266 | children[i-1].SetExpanded(true)
267 | // Set the current node focus
268 | guildView.SetCurrentNode(children[i-1])
269 | }
270 | case tcell.KeyCtrlJ, tcell.KeyPgDn:
271 | // If we're not the last guild
272 | if i != len(children)-1 {
273 | // Collapse all nodes
274 | CollapseAll(guildNode)
275 | // Set the previous node as expanded
276 | children[i+1].SetExpanded(true)
277 | // Set the current node focus
278 | guildView.SetCurrentNode(children[i+1])
279 | }
280 | }
281 | })
282 |
283 | return nil
284 | }
285 |
286 | if ev.Rune() == '/' {
287 | app.SetFocus(input)
288 | input.SetText("/")
289 |
290 | return nil
291 | }
292 |
293 | return ev
294 | })
295 |
296 | app.Draw()
297 | }
298 |
299 | func hop(children []*tview.TreeNode, fn func(i int)) {
300 | if n := guildView.GetCurrentNode(); n != nil {
301 | switch r := n.GetReference().(type) {
302 | // If the reference is a channel, we know the cursor is over a
303 | // guild's children
304 | case *discordgo.Channel:
305 | // Iterate over guild nodes
306 | for i, gNode := range children {
307 | // Get the dgo guild reference
308 | rg, ok := gNode.GetReference().(*discordgo.Guild)
309 | if !ok {
310 | // Probably not what we're looking for, next
311 | continue
312 | }
313 |
314 | // Not the guild we're in
315 | if rg.ID != r.GuildID {
316 | continue // next
317 | }
318 |
319 | fn(i)
320 | }
321 |
322 | // If the reference is a guild or the direct message thing
323 | case *discordgo.Guild, string:
324 | // Iterate over guild nodes
325 | for i, gNode := range children {
326 | // If the guild node is not the node we're on
327 | if gNode != n {
328 | continue // skip
329 | }
330 |
331 | fn(i)
332 | }
333 | }
334 | }
335 | }
336 |
337 | func isValidCh(t discordgo.ChannelType) bool {
338 | /**/ return t == discordgo.ChannelTypeGuildText ||
339 | /*****/ t == discordgo.ChannelTypeDM ||
340 | /*****/ t == discordgo.ChannelTypeGroupDM ||
341 | /*****/ t == discordgo.ChannelTypeGuildCategory ||
342 | /*****/ t == discordgo.ChannelTypeGuildVoice
343 | }
344 |
345 | func isSendCh(t discordgo.ChannelType) bool {
346 | /**/ return t == discordgo.ChannelTypeGuildText ||
347 | /*****/ t == discordgo.ChannelTypeDM ||
348 | /*****/ t == discordgo.ChannelTypeGroupDM
349 | }
350 |
351 | func generateName(ch *discordgo.Channel) string {
352 | switch ch.Type {
353 | case discordgo.ChannelTypeDM, discordgo.ChannelTypeGroupDM:
354 | return makeDMName(ch)
355 | case discordgo.ChannelTypeGuildVoice:
356 | return "v- " + ch.Name
357 | case discordgo.ChannelTypeGuildCategory:
358 | return ch.Name
359 | default:
360 | return "#" + ch.Name
361 | }
362 | }
363 |
364 | func makeDMName(ch *discordgo.Channel) string {
365 | if ch.Name != "" {
366 | return ch.Name
367 | }
368 |
369 | var names = make([]string, len(ch.Recipients))
370 | if len(ch.Recipients) == 1 {
371 | p := ch.Recipients[0]
372 | names[0] = p.Username + "#" + p.Discriminator
373 | } else {
374 | for i, p := range ch.Recipients {
375 | names[i] = p.Username
376 | }
377 | }
378 |
379 | return HumanizeStrings(names)
380 | }
381 |
--------------------------------------------------------------------------------
/ontyping.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/diamondburned/discordgo"
7 | )
8 |
9 | // TypingUsers is a store for all typing users
10 | type TypingUsers struct {
11 | Store []*typingEvent
12 | }
13 |
14 | type typingEvent struct {
15 | *discordgo.TypingStart
16 | Meta *typingMeta
17 | }
18 |
19 | type typingMeta struct {
20 | Name string
21 | Time time.Time
22 | }
23 |
24 | var (
25 | typing = &TypingUsers{}
26 | updateTyping = make(chan struct{})
27 | )
28 |
29 | func onTyping(s *discordgo.Session, ts *discordgo.TypingStart) {
30 | if Channel == nil {
31 | return
32 | }
33 |
34 | if ts.ChannelID != Channel.ID {
35 | return
36 | }
37 |
38 | if ts.UserID == d.State.User.ID {
39 | return
40 | }
41 |
42 | go typing.AddUser(ts)
43 | }
44 |
45 | func getTypingMeta(typing *discordgo.TypingStart) *typingMeta {
46 | if typing.GuildID != 0 {
47 | _, user := us.GetUser(
48 | typing.GuildID, typing.UserID,
49 | )
50 |
51 | m := &typingMeta{
52 | Time: time.Now(),
53 | }
54 |
55 | if user == nil {
56 | m, err := d.State.Member(typing.GuildID, typing.UserID)
57 | if err != nil {
58 | return nil
59 | }
60 |
61 | user = us.UpdateUser(
62 | m.GuildID,
63 | m.User.ID,
64 | m.User.Username,
65 | m.Nick,
66 | m.User.Discriminator,
67 | getUserColor(m.GuildID, m.Roles),
68 | )
69 | }
70 |
71 | if user.Nick == "" {
72 | m.Name = user.Name
73 | } else {
74 | m.Name = user.Nick
75 | }
76 |
77 | return m
78 | }
79 |
80 | ch, err := d.State.Channel(Channel.ID)
81 | if err != nil {
82 | return nil
83 | }
84 |
85 | for _, r := range ch.Recipients {
86 | if r.ID == typing.UserID {
87 | return &typingMeta{
88 | Name: r.Username,
89 | Time: time.Now(),
90 | }
91 | }
92 | }
93 |
94 | return nil
95 | }
96 |
97 | func renderCallback() {
98 | var (
99 | animation uint
100 | tick = time.Tick(time.Second)
101 | dots = [6]string{
102 | " ", "· ", "·· ",
103 | "···", " ··", " ·",
104 | }
105 | )
106 |
107 | for {
108 | var mems = make([]string, 0, len(typing.Store))
109 |
110 | select { // 500ms or instant
111 | case <-updateTyping:
112 | case <-tick:
113 | }
114 |
115 | if len(typing.Store) < 1 {
116 | animation = 0
117 | } else {
118 | animation++
119 | if animation > 5 {
120 | animation = 0
121 | }
122 | }
123 |
124 | for _, t := range typing.Store {
125 | if t.Meta != nil {
126 | mems = append(mems, t.Meta.Name)
127 | }
128 | }
129 |
130 | text := cfg.Prop.DefaultStatus
131 |
132 | switch {
133 | case len(mems) > 3:
134 | text = "Several people are typing" + dots[animation]
135 | case len(mems) == 1:
136 | text = HumanizeStrings(mems) + " is typing" + dots[animation]
137 | case len(mems) > 1:
138 | text = HumanizeStrings(mems) + " are typing" + dots[animation]
139 | }
140 |
141 | if text != input.GetPlaceholder() && !messagesView.HasFocus() {
142 | input.SetPlaceholder(text)
143 | app.Draw()
144 | }
145 | }
146 | }
147 |
148 | func getAnimation(i uint) string {
149 | switch i {
150 | case 0:
151 | return " "
152 | case 1:
153 | return "· "
154 | case 2:
155 | return "·· "
156 | case 3:
157 | return "···"
158 | case 4:
159 | return " ··"
160 | case 5:
161 | return " ·"
162 | }
163 |
164 | return " "
165 | }
166 |
167 | // Reset resets the store
168 | func (tu *TypingUsers) Reset() {
169 | tu.Store = []*typingEvent{}
170 | updateTyping <- struct{}{}
171 | }
172 |
173 | // AddUser this function needs to run in a goroutine
174 | func (tu *TypingUsers) AddUser(ts *discordgo.TypingStart) {
175 | defer func() {
176 | if r := recover(); r != nil {
177 | return
178 | }
179 | }()
180 |
181 | for _, s := range tu.Store {
182 | if s.UserID == ts.UserID && s.Meta != nil {
183 | s.Meta.Time = time.Now()
184 | return
185 | }
186 | }
187 |
188 | ev := &typingEvent{
189 | TypingStart: ts,
190 | Meta: getTypingMeta(ts),
191 | }
192 |
193 | tu.Store = append(tu.Store, ev)
194 |
195 | updateTyping <- struct{}{}
196 |
197 | time.Sleep(time.Second * 10)
198 |
199 | // should always pass UNLESS there's another AddUser call bumping the
200 | // time up
201 | for {
202 | t := ev.Meta.Time
203 | if t.Add(10 * time.Second).Before(time.Now()) {
204 | tu.RemoveUser(ts)
205 | break
206 | }
207 |
208 | time.Sleep(time.Second * 1)
209 | }
210 | }
211 |
212 | // RemoveUser removes a user from a store array
213 | // true is returned when a user is found and removed
214 | func (tu *TypingUsers) RemoveUser(ts *discordgo.TypingStart) bool {
215 | for i, d := range tu.Store {
216 | if d.UserID == ts.UserID {
217 | tu.Store = append(
218 | tu.Store[:i],
219 | tu.Store[i+1:]...,
220 | )
221 |
222 | updateTyping <- struct{}{}
223 | return true
224 | }
225 | }
226 |
227 | return false
228 | }
229 |
--------------------------------------------------------------------------------
/parsementions.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/diamondburned/discordgo"
10 | "github.com/diamondburned/tview/v2"
11 | "gitlab.com/diamondburned/6cord/md"
12 | )
13 |
14 | var (
15 | patternChannels = regexp.MustCompile("<#[^>]*>")
16 | )
17 |
18 | // ParseMentionsFallback parses mentions into strings without failing
19 | func ParseMentionsFallback(m *discordgo.Message) (content string) {
20 | content = md.Parse(m.Content)
21 |
22 | for _, user := range m.Mentions {
23 | var username = tview.Escape(user.Username)
24 |
25 | content = strings.NewReplacer(
26 | // <@ID>
27 | fmt.Sprintf("<@%d>", user.ID),
28 | "[:blue:]@"+username+"[:-:]",
29 | // <@!ID>
30 | fmt.Sprintf("<@!%d>", user.ID),
31 | "[:blue:]@"+username+"[:-:]",
32 | ).Replace(content)
33 | }
34 |
35 | return
36 | }
37 |
38 | func parseMessageContent(m *discordgo.Message) (content string, emojiMap map[string][]string) {
39 | channel, err := d.State.Channel(m.ChannelID)
40 | if err != nil {
41 | content = ParseMentionsFallback(m)
42 | return
43 | }
44 |
45 | _c, emojiMap := parseEmojis(m.Content)
46 | content = md.Parse(_c)
47 |
48 | for _, user := range m.Mentions {
49 | var username = tview.Escape(user.Username)
50 |
51 | member, err := d.State.Member(channel.GuildID, user.ID)
52 | if err == nil && member.Nick != "" {
53 | username = tview.Escape(member.Nick)
54 | }
55 |
56 | var color = "[-:" + cfg.Prop.MentionColor + ":-]"
57 | if user.ID == d.State.User.ID {
58 | color = "[-:" + cfg.Prop.MentionSelfColor + ":-]"
59 | }
60 |
61 | content = strings.NewReplacer(
62 | // <@ID>
63 | fmt.Sprintf("<@%d>", user.ID),
64 | color+"@"+username+"[:-:]",
65 | // <@!ID>
66 | fmt.Sprintf("<@!%d>", user.ID),
67 | color+"@"+username+"[:-:]",
68 | ).Replace(content)
69 | }
70 |
71 | var color = "[-:" + cfg.Prop.MentionColor + ":-]"
72 |
73 | for _, roleID := range m.MentionRoles {
74 | role, err := d.State.Role(channel.GuildID, roleID)
75 | if err != nil {
76 | continue
77 | }
78 |
79 | content = strings.Replace(
80 | content,
81 | fmt.Sprintf("<@&%d>", role.ID),
82 | color+"@"+role.Name+"[:-:]",
83 | 1,
84 | )
85 | }
86 |
87 | content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string {
88 | id, err := strconv.ParseInt(mention[2:len(mention)-1], 10, 64)
89 | if err != nil {
90 | return mention
91 | }
92 |
93 | channel, err := d.State.Channel(id)
94 | if err != nil || channel.Type == discordgo.ChannelTypeGuildVoice {
95 | return mention
96 | }
97 |
98 | return color + "#" + channel.Name + "[:-:]"
99 | })
100 |
101 | return
102 | }
103 |
--------------------------------------------------------------------------------
/reactionevents.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/diamondburned/discordgo"
4 |
5 | func reactionAdd(s *discordgo.Session, ra *discordgo.MessageReactionAdd) {
6 | if Channel == nil || ra.ChannelID != Channel.ID {
7 | return
8 | }
9 |
10 | m, err := d.State.Message(Channel.ID, ra.MessageID)
11 | if err != nil {
12 | return
13 | }
14 |
15 | d.State.Lock()
16 |
17 | for _, r := range m.Reactions {
18 | if !isSameEmoji(r, ra.MessageReaction) {
19 | continue
20 | }
21 |
22 | r.Count++
23 |
24 | if ra.UserID == d.State.User.ID {
25 | r.Me = true
26 | }
27 |
28 | goto Found
29 | }
30 |
31 | m.Reactions = append(
32 | m.Reactions,
33 | &discordgo.MessageReactions{
34 | Count: 1,
35 | Me: ra.UserID == d.State.User.ID,
36 | Emoji: &ra.Emoji,
37 | },
38 | )
39 |
40 | Found:
41 | d.State.Unlock()
42 | handleReactionEvent(m)
43 | }
44 |
45 | func reactionRemove(s *discordgo.Session, rm *discordgo.MessageReactionRemove) {
46 | if Channel == nil || rm.ChannelID != Channel.ID {
47 | return
48 | }
49 |
50 | m, err := d.State.Message(Channel.ID, rm.MessageID)
51 | if err != nil {
52 | return
53 | }
54 |
55 | d.State.Lock()
56 |
57 | for i, r := range m.Reactions {
58 | if !isSameEmoji(r, rm.MessageReaction) {
59 | continue
60 | }
61 |
62 | r.Count--
63 |
64 | if r.Count == 0 {
65 | m.Reactions = removeAllReactions(
66 | m.Reactions, i,
67 | )
68 |
69 | break
70 | }
71 |
72 | if rm.UserID == d.State.User.ID {
73 | r.Me = false
74 | }
75 | }
76 |
77 | d.State.Unlock()
78 | handleReactionEvent(m)
79 | }
80 |
81 | func reactionRemoveAll(s *discordgo.Session, rm *discordgo.MessageReactionRemoveAll) {
82 | if Channel == nil || rm.ChannelID != Channel.ID {
83 | return
84 | }
85 |
86 | m, err := d.State.Message(Channel.ID, rm.MessageID)
87 | if err != nil {
88 | return
89 | }
90 |
91 | d.State.Lock()
92 |
93 | m.Reactions = []*discordgo.MessageReactions{}
94 |
95 | d.State.Unlock()
96 |
97 | handleReactionEvent(m)
98 | }
99 |
--------------------------------------------------------------------------------
/reactions.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/diamondburned/discordgo"
9 | )
10 |
11 | const (
12 | formatReactionConstant = `[:%s:] %d %s [:-:] `
13 |
14 | unreactColor = "#383838"
15 | reactColor = "#2196F3"
16 | )
17 |
18 | func removeAllReactions(rs []*discordgo.MessageReactions, i int) []*discordgo.MessageReactions {
19 | if i > 0 {
20 | return append(rs[:i], rs[i+1:]...)
21 | }
22 |
23 | return rs[i+1:]
24 | }
25 |
26 | func isSameEmoji(rs *discordgo.MessageReactions, r *discordgo.MessageReaction) bool {
27 | if rs.Emoji.ID != 0 || r.Emoji.ID != 0 {
28 | return rs.Emoji.ID == r.Emoji.ID
29 | }
30 |
31 | return rs.Emoji.Name == r.Emoji.Name
32 | }
33 |
34 | func handleReactionEvent(m *discordgo.Message) {
35 | if rstore.Check(m.Author, RelationshipBlocked) && cfg.Prop.HideBlocked {
36 | return
37 | }
38 |
39 | id := strconv.FormatInt(m.ID, 10)
40 | for i, msg := range messageStore {
41 | if strings.HasPrefix(msg, messageRawFormat[:3]+id+"\"]") {
42 | msg := messageTmpl.ExecuteString(map[string]interface{}{
43 | "ID": strconv.FormatInt(m.ID, 10),
44 | "content": fmtMessage(m),
45 | })
46 |
47 | messageStore[i] = msg
48 |
49 | break
50 | }
51 | }
52 |
53 | messagesView.SetText(strings.Join(messageStore, ""))
54 |
55 | scrollChat()
56 | }
57 |
58 | func formatReactions(rs []*discordgo.MessageReactions) (f string, eM map[string][]string) {
59 | eM = make(map[string][]string)
60 |
61 | for _, r := range rs {
62 | f += formatReactionString(r)
63 |
64 | if r.Emoji.ID == 0 {
65 | continue
66 | }
67 |
68 | var format = "png"
69 | if r.Emoji.Animated {
70 | format = "gif"
71 | }
72 |
73 | IDstring := fmt.Sprintf("%d", r.Emoji.ID)
74 |
75 | eM[IDstring] = []string{
76 | r.Emoji.Name,
77 | `https://cdn.discordapp.com/emojis/` + IDstring + `.` + format,
78 | }
79 | }
80 |
81 | return
82 | }
83 |
84 | func formatReactionString(r *discordgo.MessageReactions) string {
85 | if r.Emoji == nil {
86 | return ""
87 | }
88 |
89 | var color = unreactColor
90 | if r.Me {
91 | color = reactColor
92 | }
93 |
94 | return fmt.Sprintf(
95 | formatReactionConstant,
96 | color, r.Count, strings.TrimSpace(r.Emoji.Name),
97 | )
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/readstate.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "strings"
6 | "sync"
7 |
8 | "github.com/diamondburned/discordgo"
9 | "github.com/diamondburned/tview/v2"
10 | )
11 |
12 | const readChannelColorPrefix = "[#808080::]"
13 | const unreadChannelColorPrefix = "[::b]"
14 |
15 | func messageAck(s *discordgo.Session, a *discordgo.MessageAck) {
16 | // Sets ReadState to the message you read
17 | for _, c := range d.State.ReadState {
18 | if c.ID == a.ChannelID && c.LastMessageID != 0 {
19 | c.LastMessageID = a.MessageID
20 | }
21 | }
22 |
23 | c, err := d.State.Channel(a.ChannelID)
24 | if err != nil {
25 | return
26 | }
27 |
28 | if c.GuildID == 0 {
29 | ackMeUI(c)
30 | } else {
31 | g, err := d.State.Guild(c.GuildID)
32 | if err != nil {
33 | return
34 | }
35 |
36 | checkGuild(g)
37 | }
38 | }
39 |
40 | // "[::b]actual string[::-]"
41 | func stripFormat(a string) string {
42 | if len(a) <= 10 {
43 | return a
44 | }
45 |
46 | if strings.HasPrefix(a, readChannelColorPrefix) {
47 | a = a[len(readChannelColorPrefix):]
48 | }
49 |
50 | return strings.TrimSuffix(a, "[-::-]")
51 | }
52 |
53 | var (
54 | guildSettingsMuted = map[int64]bool{}
55 | channelSettingsMuted = map[int64]bool{}
56 | settingsCacheMutex = &sync.Mutex{}
57 | )
58 |
59 | func guildMuted(g *discordgo.Guild) bool {
60 | if g == nil {
61 | return false
62 | }
63 |
64 | settingsCacheMutex.Lock()
65 | defer settingsCacheMutex.Unlock()
66 |
67 | guMuted, ok := guildSettingsMuted[g.ID]
68 | if !ok {
69 | gs := getGuildFromSettings(g.ID)
70 |
71 | guMuted = settingGuildIsMuted(gs)
72 | guildSettingsMuted[g.ID] = guMuted
73 | }
74 |
75 | return guMuted
76 | }
77 |
78 | // true if channelID has unread msgs
79 | func isUnread(ch *discordgo.Channel) bool {
80 | var gs *discordgo.UserGuildSettings
81 |
82 | settingsCacheMutex.Lock()
83 |
84 | chMuted, ok := channelSettingsMuted[ch.ID]
85 | if !ok {
86 | if gs == nil {
87 | gs = getGuildFromSettings(ch.GuildID)
88 | }
89 |
90 | cs := getChannelFromGuildSettings(ch.ID, gs)
91 |
92 | chMuted = settingChannelIsMuted(cs, gs)
93 | channelSettingsMuted[ch.ID] = chMuted
94 | }
95 |
96 | var guMuted = false
97 |
98 | if ch.GuildID != 0 {
99 | guMuted, ok = guildSettingsMuted[ch.GuildID]
100 | if !ok {
101 | if gs == nil {
102 | gs = getGuildFromSettings(ch.GuildID)
103 | }
104 |
105 | guMuted = settingGuildIsMuted(gs)
106 | guildSettingsMuted[ch.GuildID] = guMuted
107 | }
108 | }
109 |
110 | settingsCacheMutex.Unlock()
111 |
112 | if chMuted {
113 | return false
114 | }
115 |
116 | if ch.LastMessageID == 0 {
117 | return false
118 | }
119 |
120 | for _, c := range d.State.ReadState {
121 | if c.ID == ch.ID {
122 | return c.LastMessageID != ch.LastMessageID
123 | }
124 | }
125 |
126 | return false
127 | }
128 |
129 | func markUnread(m *discordgo.Message) {
130 | var unread bool
131 |
132 | c, err := d.State.Channel(m.ChannelID)
133 | if err != nil {
134 | return
135 | }
136 |
137 | if c.GuildID == 0 {
138 | // If the latest DM message is not the current message,
139 | // it's unread.
140 | for _, r := range d.State.ReadState {
141 | if r.ID == c.ID {
142 | unread = (m.ID != r.LastMessageID)
143 | break
144 | }
145 | }
146 | } else {
147 | // If neither the channel nor the guild is muted, it's
148 | // unread.
149 | gs := getGuildFromSettings(c.GuildID)
150 | chSettings := getChannelFromGuildSettings(c.ID, gs)
151 |
152 | var (
153 | chMuted = settingChannelIsMuted(chSettings, gs)
154 | guMuted = settingGuildIsMuted(gs)
155 | )
156 |
157 | unread = !(guMuted || chMuted)
158 | }
159 |
160 | if !unread {
161 | return
162 | }
163 |
164 | root := guildView.GetRoot()
165 | if root == nil {
166 | return
167 | }
168 |
169 | root.Walk(func(node, parent *tview.TreeNode) bool {
170 | if parent == nil {
171 | return true
172 | }
173 |
174 | switch reference := node.GetReference().(type) {
175 | case *discordgo.Guild:
176 | if reference.ID != m.GuildID {
177 | return false
178 | }
179 | case *discordgo.Channel:
180 | if reference.ID != m.ChannelID {
181 | return false
182 | }
183 |
184 | if g, ok := parent.GetReference().(*discordgo.Guild); ok {
185 | parent.SetText(unreadChannelColorPrefix + g.Name + "[-::-]")
186 | } else {
187 | parent.SetText(unreadChannelColorPrefix + "Direct Messages[-::-]")
188 | }
189 |
190 | var name = generateName(reference)
191 | node.SetText(unreadChannelColorPrefix + name + "[-::-]")
192 |
193 | return false
194 | default:
195 | return true
196 | }
197 |
198 | return true
199 | })
200 |
201 | app.Draw()
202 | }
203 |
204 | var lastAck string
205 |
206 | func ackMe(chID, ID int64) {
207 | c, err := d.State.Channel(chID)
208 | if err != nil {
209 | return
210 | }
211 |
212 | if isUnread(c) {
213 | // triggers messageAck
214 | a, err := d.ChannelMessageAck(c.ID, ID, lastAck)
215 |
216 | if err != nil {
217 | log.Println(err)
218 | return
219 | }
220 |
221 | lastAck = a.Token
222 | }
223 |
224 | if c.GuildID == 0 {
225 | ackMeUI(c)
226 | } else {
227 | g, err := d.State.Guild(c.GuildID)
228 | if err != nil {
229 | return
230 | }
231 |
232 | checkGuild(g)
233 | }
234 | }
235 |
236 | func ackMeUI(ch *discordgo.Channel) {
237 | root := guildView.GetRoot()
238 | if root == nil {
239 | return
240 | }
241 |
242 | root.Walk(func(node, parent *tview.TreeNode) bool {
243 | if parent == nil {
244 | return true
245 | }
246 |
247 | switch reference := node.GetReference().(type) {
248 | case *discordgo.Guild:
249 | if reference.ID != ch.GuildID {
250 | return false
251 | }
252 | case *discordgo.Channel:
253 | if reference.ID != ch.ID {
254 | return false
255 | }
256 |
257 | var name = generateName(reference)
258 | node.SetText(readChannelColorPrefix + name + "[-::-]")
259 | default:
260 | return true
261 | }
262 |
263 | return true
264 | })
265 |
266 | app.Draw()
267 | }
268 |
269 | func checkGuild(g *discordgo.Guild) {
270 | root := guildView.GetRoot()
271 | if root == nil {
272 | return
273 | }
274 |
275 | for _, n := range root.GetChildren() {
276 | gd, ok := n.GetReference().(*discordgo.Guild)
277 | if !ok {
278 | continue
279 | }
280 |
281 | if gd.ID != g.ID {
282 | continue
283 | }
284 |
285 | checkGuildNode(g, n)
286 |
287 | app.Draw()
288 | return
289 | }
290 | }
291 |
292 | func checkGuildNode(g *discordgo.Guild, n *tview.TreeNode) {
293 | var unreads = make([]*discordgo.Channel, 0, len(g.Channels))
294 | for _, c := range g.Channels {
295 | if isUnread(c) {
296 | unreads = append(unreads, c)
297 | }
298 | }
299 |
300 | if len(unreads) == 0 {
301 | n.SetText(readChannelColorPrefix + g.Name + "[-::-]")
302 | } else if !guildMuted(g) {
303 | n.SetText(unreadChannelColorPrefix + g.Name + "[-::-]")
304 | }
305 |
306 | Main:
307 | for _, node := range n.GetChildren() {
308 | reference := node.GetReference()
309 | if reference == nil {
310 | continue
311 | }
312 |
313 | ch, ok := reference.(*discordgo.Channel)
314 | if !ok {
315 | continue
316 | }
317 |
318 | var name = generateName(ch)
319 |
320 | for _, u := range unreads {
321 | if u.ID == ch.ID {
322 | node.SetText(unreadChannelColorPrefix + name + "[-::-]")
323 | continue Main
324 | }
325 | }
326 |
327 | node.SetText(readChannelColorPrefix + name + "[-::-]")
328 | }
329 | }
330 |
331 | // TODO: Check if guild has unread channel
332 |
--------------------------------------------------------------------------------
/relationships.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Taken from
4 | // https://gitlab.com/diamondburned/shittercord/blob/master/relationships.go
5 |
6 | import (
7 | "sync"
8 |
9 | "github.com/diamondburned/discordgo"
10 | )
11 |
12 | func relationshipAdd(s *discordgo.Session, ra *discordgo.RelationshipAdd) {
13 | for i, r := range d.State.Relationships {
14 | if r.ID == ra.ID {
15 | d.State.Relationships[i] = ra.Relationship
16 | return
17 | }
18 | }
19 |
20 | d.State.Relationships = append(
21 | d.State.Relationships,
22 | ra.Relationship,
23 | )
24 | }
25 |
26 | func relationshipRemove(s *discordgo.Session, rm *discordgo.RelationshipRemove) {
27 | rs := d.State.Relationships
28 |
29 | for i, r := range rs {
30 | if r.ID == rm.ID {
31 | rs = append(rs[:i], rs[i+1:]...)
32 | return
33 | }
34 | }
35 |
36 | d.State.Relationships = rs
37 | }
38 |
39 | // RStore contains Discord relationships
40 | type RStore struct {
41 | Relationships []*discordgo.Relationship
42 | lock sync.RWMutex
43 | }
44 |
45 | type Relationship int
46 |
47 | const (
48 | RelationshipNone Relationship = iota
49 |
50 | RelationshipFriend // friend
51 | RelationshipBlocked // blocked
52 | RelationshipIncomingFriendRequest // incoming friend request
53 | RelationshipSentFriendRequest // sent friend request
54 | )
55 |
56 | var (
57 | rstore = &RStore{}
58 | )
59 |
60 | // Check returns true if user is blocked
61 | func (rs *RStore) Check(u *discordgo.User, relationship Relationship) bool {
62 | if !cfg.Prop.HideBlocked && relationship == RelationshipBlocked {
63 | return false
64 | }
65 |
66 | rs.lock.RLock()
67 | defer rs.lock.RUnlock()
68 |
69 | for _, r := range rs.Relationships {
70 | if r == nil {
71 | continue
72 | }
73 |
74 | if r.User == nil {
75 | continue
76 | }
77 |
78 | if r.Type == int(relationship) && r.User.ID == u.ID {
79 | return true
80 | }
81 | }
82 |
83 | return false
84 | }
85 |
86 | // Get gets the relationship of a user
87 | func (rs *RStore) Get(u *discordgo.User) Relationship {
88 | rs.lock.RLock()
89 | defer rs.lock.RUnlock()
90 |
91 | for _, r := range rs.Relationships {
92 | if r.User.ID == u.ID {
93 | return parseInt(r.Type)
94 | }
95 | }
96 |
97 | return RelationshipNone
98 | }
99 |
100 | // Remove removes a relationship from the array
101 | func (rs *RStore) Remove(r *discordgo.Relationship) {
102 | rs.lock.Lock()
103 | defer rs.lock.Unlock()
104 |
105 | for i, rr := range rs.Relationships {
106 | if rr == r {
107 | // arr := *rs
108 | // arr[i] = arr[len(arr)-1]
109 | // *rs = arr[:len(arr)-1]
110 |
111 | rs.Relationships = append(rs.Relationships[:i], rs.Relationships[i+1:]...)
112 | }
113 | }
114 | }
115 |
116 | // Add adds a relationship to the store
117 | func (rs *RStore) Add(r *discordgo.Relationship) {
118 | rs.lock.Lock()
119 | defer rs.lock.Unlock()
120 |
121 | rs.Relationships = append(rs.Relationships, r)
122 | }
123 |
124 | func parseInt(i int) Relationship {
125 | switch i {
126 | case int(RelationshipFriend):
127 | return RelationshipFriend
128 | case int(RelationshipBlocked):
129 | return RelationshipBlocked
130 | case int(RelationshipIncomingFriendRequest):
131 | return RelationshipIncomingFriendRequest
132 | case int(RelationshipSentFriendRequest):
133 | return RelationshipSentFriendRequest
134 | default:
135 | return RelationshipNone
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/screenguild.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | func currentGuildScreen() {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/screenmiddleware.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/diamondburned/tcell"
4 |
5 | var drawingMiddlewareFn func()
6 |
7 | func onDrawingMiddleware(s *tcell.Screen) {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/scroll.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | func handleScroll() {
9 | current, lines := getLineStatus()
10 |
11 | if current == 0 {
12 | go loadMore()
13 | }
14 |
15 | input.SetPlaceholder(fmt.Sprintf(
16 | "%d/%d %d%%",
17 | current, lines, min(current*100/lines, 100),
18 | ))
19 |
20 | app.Draw()
21 | }
22 |
23 | func getLineStatus() (current, total int) {
24 | total = len(
25 | strings.Split(
26 | strings.Join(messageStore, ""),
27 | "\n",
28 | ),
29 | )
30 |
31 | if total <= 1 {
32 | total = len(messagesView.GetText(false))
33 | }
34 |
35 | var (
36 | toplinepos, _ = messagesView.GetScrollOffset()
37 | _, _, _, height = messagesView.GetInnerRect()
38 | )
39 |
40 | if toplinepos == 0 {
41 | height = 0
42 | }
43 |
44 | current = toplinepos + height
45 |
46 | return
47 | }
48 |
--------------------------------------------------------------------------------
/shortener/port.go:
--------------------------------------------------------------------------------
1 | package shortener
2 |
3 | import (
4 | "net"
5 | "net/http"
6 | "strconv"
7 | )
8 |
9 | // URL is the current URL
10 | var URL = ""
11 |
12 | // GetOpenPort finds an open port
13 | func GetOpenPort() (int, error) {
14 | addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
15 | if err != nil {
16 | return 0, err
17 | }
18 |
19 | l, err := net.ListenTCP("tcp", addr)
20 | if err != nil {
21 | return 0, err
22 | }
23 |
24 | defer l.Close()
25 | return l.Addr().(*net.TCPAddr).Port, nil
26 | }
27 |
28 | func StartHTTP(ip string) error {
29 | p, err := GetOpenPort()
30 | if err != nil {
31 | return err
32 | }
33 |
34 | URL = ip + ":" + strconv.Itoa(p)
35 |
36 | http.HandleFunc("/", Handler)
37 | go http.ListenAndServe(URL, nil)
38 |
39 | Enabled = true
40 |
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/shortener/shortener.go:
--------------------------------------------------------------------------------
1 | package shortener
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 | "strconv"
7 | "strings"
8 | "sync"
9 | )
10 |
11 | var (
12 | shortenerState = map[string]string{}
13 | shortenerMutex = &sync.RWMutex{}
14 |
15 | incr int
16 |
17 | // Enabled once StartHTTP is run
18 | Enabled = false
19 | )
20 |
21 | func Handler(w http.ResponseWriter, r *http.Request) {
22 | shortenerMutex.RLock()
23 | defer shortenerMutex.RUnlock()
24 |
25 | ou, ok := shortenerState[r.URL.Path]
26 | if !ok {
27 | http.NotFound(w, r)
28 | return
29 | }
30 |
31 | http.Redirect(w, r, ou, http.StatusTemporaryRedirect)
32 | }
33 |
34 | func ShortenURL(targetURL string) string {
35 | if !Enabled {
36 | return targetURL
37 | }
38 |
39 | shortenerMutex.Lock()
40 | defer shortenerMutex.Unlock()
41 |
42 | u, err := url.Parse(targetURL)
43 | if err != nil {
44 | return targetURL
45 | }
46 |
47 | var id string
48 | var ext = GetExtension(u.Path)
49 |
50 | var fileshards []string
51 | if u.Path != "" {
52 | fileshards = strings.Split(u.Path, "/")
53 | } else {
54 | fileshards = []string{u.Host}
55 | }
56 |
57 | filename := fileshards[len(fileshards)-1]
58 | filename = filename[:max(len(filename)-len(ext), 0)]
59 | filename = filename[:min(len(filename), 8)]
60 |
61 | slug := filename + "-" + increment()
62 | id = "/" + slug + ext
63 |
64 | shortenerState[id] = targetURL
65 | return "http://" + URL + id
66 | }
67 |
68 | func min(i, j int) int {
69 | if i < j {
70 | return i
71 | }
72 |
73 | return j
74 | }
75 |
76 | func max(i, j int) int {
77 | if i > j {
78 | return i
79 | }
80 |
81 | return j
82 | }
83 | func increment() string {
84 | incr++
85 | return strconv.Itoa(incr)
86 | }
87 |
--------------------------------------------------------------------------------
/shortener/url.go:
--------------------------------------------------------------------------------
1 | package shortener
2 |
3 | import "strings"
4 |
5 | func GetExtension(name string) string {
6 | parts := strings.Split(name, "/")
7 | ss := strings.Split(parts[len(parts)-1], ".")
8 |
9 | if len(ss) < 2 {
10 | return ""
11 | }
12 |
13 | return "." + ss[len(ss)-1]
14 | }
15 |
--------------------------------------------------------------------------------
/sixel.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "image"
4 |
5 | type sixelImage struct {
6 | source string
7 | image *image.Image
8 | sixel []byte
9 | }
10 |
11 | func newSixelImage(source string) *sixelImage {
12 | return nil
13 | }
14 |
--------------------------------------------------------------------------------
/sortchannels.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Source: https://gist.github.com/diamondburned/c9ebd9fb84a8955495d4fb7983345530
4 |
5 | import (
6 | "sort"
7 |
8 | "github.com/diamondburned/discordgo"
9 | )
10 |
11 | type ChannelGeneric struct {
12 | Underlying *discordgo.Channel
13 |
14 | Children []*discordgo.Channel
15 | }
16 |
17 | func SortChannels(cs []*discordgo.Channel) (out []*discordgo.Channel) {
18 | p := make(map[int64]*ChannelGeneric)
19 |
20 | for _, c := range cs {
21 | if c.Type != discordgo.ChannelTypeGuildCategory && c.ParentID != 0 {
22 | v, ok := p[c.ParentID]
23 | if ok {
24 | v.Children = append(v.Children, c)
25 | } else {
26 | p[c.ParentID] = &ChannelGeneric{
27 | Children: []*discordgo.Channel{c},
28 | }
29 | }
30 |
31 | continue
32 | }
33 |
34 | if c.Type == discordgo.ChannelTypeGuildCategory {
35 | v, ok := p[c.ID]
36 |
37 | if ok {
38 | v.Underlying = c
39 | } else {
40 | p[c.ID] = &ChannelGeneric{
41 | Underlying: c,
42 | }
43 | }
44 |
45 | continue
46 | }
47 |
48 | p[c.ID] = &ChannelGeneric{
49 | Underlying: c,
50 | }
51 | }
52 |
53 | a := make([]*ChannelGeneric, 0, len(p))
54 |
55 | for _, v := range p {
56 | if v.Children != nil {
57 | sort.Slice(v.Children, func(i, j int) bool {
58 | return v.Children[i].Position < v.Children[j].Position
59 | })
60 |
61 | sort.SliceStable(v.Children, func(i, j int) bool {
62 | if v.Children[j].Type == discordgo.ChannelTypeGuildVoice {
63 | return true
64 | }
65 |
66 | if v.Children[i].Type == discordgo.ChannelTypeGuildVoice {
67 | return true
68 | }
69 |
70 | return false
71 | })
72 | }
73 |
74 | a = append(a, v)
75 | }
76 |
77 | sort.Slice(a, func(i, j int) bool {
78 | return a[i].Underlying.Position < a[j].Underlying.Position
79 | })
80 |
81 | sort.SliceStable(a, func(i, j int) bool {
82 | return a[i].Children == nil
83 | })
84 |
85 | for _, v := range a {
86 | out = append(out, v.Underlying)
87 |
88 | if v.Children != nil {
89 | for _, k := range v.Children {
90 | out = append(out, k)
91 | }
92 | }
93 | }
94 |
95 | return
96 | }
97 |
--------------------------------------------------------------------------------
/syscall.go:
--------------------------------------------------------------------------------
1 | // +build !linux,!darwin,!freebsd,!windows
2 |
3 | package main
4 |
5 | import "os"
6 |
7 | func syscallSilenceStderr(f *os.File) {
8 | // since we're unsure if the platform has dup2, we can just
9 | // silent all errors
10 | d.Debug = false
11 | d.LogLevel = 0
12 | }
13 |
--------------------------------------------------------------------------------
/syscall_linux.go:
--------------------------------------------------------------------------------
1 | // +build linux
2 |
3 | package main
4 |
5 | import (
6 | "log"
7 | "os"
8 | "syscall"
9 | )
10 |
11 | func syscallSilenceStderr(f *os.File) {
12 | if err := syscall.Dup3(int(f.Fd()), 2, 0); err != nil {
13 | log.Println("Can't steal stderr, instabilities may occur")
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/syscall_unix.go:
--------------------------------------------------------------------------------
1 | // +build darwin freebsd
2 |
3 | package main
4 |
5 | import (
6 | "log"
7 | "os"
8 | "syscall"
9 | )
10 |
11 | func syscallSilenceStderr(f *os.File) {
12 | if err := syscall.Dup2(int(f.Fd()), 2); err != nil {
13 | log.Println("Can't steal stderr, instabilities may occur")
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/syscall_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package main
4 |
5 | import (
6 | "log"
7 | "os"
8 | "syscall"
9 | )
10 |
11 | var (
12 | kernel32 = syscall.MustLoadDLL("kernel32.dll")
13 | procSetStdHandle = kernel32.MustFindProc("SetStdHandle")
14 | )
15 |
16 | func setStdHandle(stdhandle int32, handle syscall.Handle) error {
17 | r0, _, e1 := syscall.Syscall(procSetStdHandle.Addr(), 2, uintptr(stdhandle), uintptr(handle), 0)
18 | if r0 == 0 {
19 | if e1 != 0 {
20 | return error(e1)
21 | }
22 |
23 | return syscall.EINVAL
24 | }
25 |
26 | return nil
27 | }
28 |
29 | func syscallSilenceStderr(f *os.File) {
30 | err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(f.Fd()))
31 | if err != nil {
32 | log.Fatalf("Failed to redirect stderr to file: %v", err)
33 | }
34 |
35 | os.Stderr = f
36 | }
37 |
--------------------------------------------------------------------------------
/treenodefns.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/diamondburned/tview/v2"
4 |
5 | // CollapseAll collapses all tree nodes
6 | func CollapseAll(gn *tview.TreeNode) {
7 | for _, c := range gn.GetChildren() {
8 | c.Collapse()
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/typing.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "time"
4 |
5 | var (
6 | typingDelay = time.Duration(time.Second * 8)
7 | typingTimer = time.NewTimer(typingDelay)
8 | )
9 |
10 | func typingTrigger() {
11 | select {
12 | case <-typingTimer.C:
13 | if cfg.Prop.TriggerTyping {
14 | if Channel == nil {
15 | return
16 | }
17 |
18 | go d.ChannelTyping(Channel.ID)
19 | }
20 | default:
21 | return
22 | }
23 |
24 | typingTimer.Reset(typingDelay)
25 | }
26 |
--------------------------------------------------------------------------------
/ui.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | var showChannels bool
4 |
5 | func toggleChannels() {
6 | if showChannels {
7 | wrapFrame.SetBorder(true)
8 | appflex.RemoveItem(guildView)
9 | appflex.RemoveItem(wrapFrame)
10 |
11 | wrapFrame.SetBorders(0, 0, 0, 0, 1, 1)
12 |
13 | appflex.AddItem(guildView, 0, 1, true)
14 | appflex.AddItem(wrapFrame, 0, cfg.Prop.SidebarRatio, true)
15 |
16 | app.SetFocus(guildView)
17 | } else {
18 | wrapFrame.SetBorder(false)
19 | appflex.RemoveItem(guildView)
20 |
21 | wrapFrame.SetBorders(0, 0, 0, 0, 0, 0)
22 |
23 | app.SetFocus(input)
24 | }
25 |
26 | showChannels = !showChannels
27 | }
28 |
--------------------------------------------------------------------------------
/user.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "sort"
6 |
7 | "github.com/diamondburned/discordgo"
8 | )
9 |
10 | func userSettingsUpdate(s *discordgo.Session, settings *discordgo.UserSettingsUpdate) {
11 | if settings == nil {
12 | return
13 | }
14 |
15 | _settings := *settings
16 |
17 | if status, ok := _settings["status"]; ok {
18 | if str, ok := status.(string); ok {
19 | st := discordgo.Status(str)
20 | d.State.Settings.Status = st
21 | }
22 | }
23 | }
24 |
25 | func safeAuthor(u *discordgo.User) (string, int64) {
26 | if u != nil {
27 | return u.Username, u.ID
28 | }
29 |
30 | return "invalid user", 0
31 | }
32 |
33 | func getUserData(u *discordgo.User, chID int64) (name string, color int) {
34 | var id int64
35 | color = defaultNameColor
36 | name, id = safeAuthor(u)
37 |
38 | if d == nil {
39 | return
40 | }
41 |
42 | channel, err := d.State.Channel(chID)
43 | if err != nil {
44 | if channel, err = d.Channel(chID); err != nil {
45 | log.Println(err)
46 | return
47 | }
48 | }
49 |
50 | if channel.GuildID == 0 {
51 | return
52 | }
53 |
54 | member, err := d.State.Member(channel.GuildID, id)
55 | if err != nil {
56 | if member, err = d.GuildMember(channel.GuildID, id); err != nil {
57 | log.Println(err)
58 | return
59 | }
60 | }
61 |
62 | name = member.Nick
63 | color = getUserColor(channel.GuildID, member.Roles)
64 |
65 | return
66 | }
67 |
68 | func getUserColor(guildID int64, rls discordgo.IDSlice) int {
69 | g, err := d.State.Guild(guildID)
70 | if err != nil {
71 | if g, err = d.Guild(guildID); err != nil {
72 | log.Println(err)
73 | return defaultNameColor
74 | }
75 | }
76 |
77 | roles := g.Roles
78 | sort.Slice(roles, func(i, j int) bool {
79 | return roles[i].Position > roles[j].Position
80 | })
81 |
82 | for _, role := range roles {
83 | for _, roleID := range rls {
84 | if role.ID == roleID && role.Color != 0 {
85 | return role.Color
86 | }
87 | }
88 | }
89 |
90 | return defaultNameColor
91 | }
92 |
93 | // ReflectStatusColor converts Discord status to HEX colors (#RRGGBB)
94 | func ReflectStatusColor(status discordgo.Status) string {
95 | switch status {
96 | case discordgo.StatusOnline:
97 | return "#43b581"
98 | case discordgo.StatusDoNotDisturb:
99 | return "#f04747"
100 | case discordgo.StatusIdle:
101 | return "#faa61a"
102 | default: // includes invisible and offline
103 | return "#747f8d"
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/user_store.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/diamondburned/discordgo"
7 | "github.com/diamondburned/tview/v2"
8 | )
9 |
10 | // User is used for one user
11 | type User struct {
12 | ID int64
13 | Discrim string
14 | Name string
15 | Nick string
16 | Color int
17 | Bot bool
18 | }
19 |
20 | // UserStore stores multiple users
21 | type UserStore struct {
22 | sync.RWMutex
23 | Guilds map[int64]UserStoreArray
24 | }
25 |
26 | // UserStoreArray is an array
27 | type UserStoreArray []*User
28 |
29 | var us = &UserStore{
30 | Guilds: map[int64]UserStoreArray{},
31 | }
32 |
33 | // Populated returns a bool on whether or not the array
34 | // alraedy is populated
35 | func (s *UserStore) Populated(guildID int64) bool {
36 | if s == nil {
37 | return false
38 | }
39 |
40 | if guildID == 0 {
41 | return true
42 | }
43 |
44 | return len(s.Guilds[guildID]) > 0
45 | }
46 |
47 | // InStore checks if a user is in the store
48 | func (s *UserStore) InStore(guildID, id int64) bool {
49 | if s == nil {
50 | return false
51 | }
52 |
53 | if _, u := s.GetUser(guildID, id); u != nil {
54 | return true
55 | }
56 |
57 | return false
58 | }
59 |
60 | // DiscordThis interfaces with DiscordGo
61 | func (s *UserStore) DiscordThis(m *discordgo.Message) (n string, c int) {
62 | n = "invalid user"
63 | c = defaultNameColor
64 |
65 | if m.Author == nil || s == nil {
66 | return
67 | }
68 |
69 | defer func() {
70 | if m.Author.Bot {
71 | n += " [#7289da][BOT[][-::-]"
72 | }
73 | }()
74 |
75 | if m.GuildID == 0 {
76 | channel, err := d.State.Channel(m.ChannelID)
77 | if err != nil {
78 | return
79 | }
80 |
81 | m.GuildID = channel.GuildID
82 | }
83 |
84 | _, user := s.GetUser(m.GuildID, m.Author.ID)
85 | if user != nil {
86 | n = user.Name
87 | c = user.Color
88 |
89 | if user.Nick != "" {
90 | n = user.Nick
91 | }
92 |
93 | return
94 | }
95 |
96 | nick, color := getUserData(m.Author, m.ChannelID)
97 |
98 | u := s.UpdateUser(
99 | m.GuildID,
100 | m.Author.ID,
101 | m.Author.Username,
102 | nick,
103 | m.Author.Discriminator,
104 | color,
105 | )
106 |
107 | n = u.Name
108 | c = u.Color
109 |
110 | if u.Nick != "" {
111 | n = u.Nick
112 | }
113 |
114 | return
115 | }
116 |
117 | // GetUser returns the index and user for that ID
118 | func (s *UserStore) GetUser(guildID, id int64) (int, *User) {
119 | s.RLock()
120 | defer s.RUnlock()
121 |
122 | if v, ok := s.Guilds[guildID]; ok {
123 | for i, u := range v {
124 | if u.ID == id {
125 | return i, u
126 | }
127 | }
128 | }
129 |
130 | return 0, nil
131 | }
132 |
133 | // RemoveUser removes the user from the store
134 | func (s *UserStore) RemoveUser(guildID, id int64) {
135 | var index int
136 |
137 | s.Lock()
138 | defer s.Unlock()
139 |
140 | if v, ok := s.Guilds[guildID]; ok {
141 | for i, u := range v {
142 | if u.ID == id {
143 | index = i
144 | goto Remove
145 | }
146 | }
147 | }
148 |
149 | return
150 |
151 | Remove:
152 | var st = s.Guilds[guildID]
153 |
154 | st[len(st)-1], st[index] = st[index], st[len(st)-1]
155 | s.Guilds[guildID] = st[:len(st)-1]
156 | }
157 |
158 | // UpdateUser updates an user
159 | func (s *UserStore) UpdateUser(guildID, id int64, name, nick, discrim string, color int) *User {
160 | if s == nil {
161 | return nil
162 | }
163 |
164 | if i, u := s.GetUser(guildID, id); u != nil {
165 | if name != "" {
166 | u.Name = tview.Escape(name)
167 | }
168 |
169 | if nick != "" {
170 | u.Nick = tview.Escape(nick)
171 | }
172 |
173 | if discrim != "" {
174 | u.Discrim = discrim
175 | }
176 |
177 | if color > 0 {
178 | u.Color = color
179 | }
180 |
181 | s.Lock()
182 | defer s.Unlock()
183 |
184 | s.Guilds[guildID][i] = u
185 | return u
186 | }
187 |
188 | s.Lock()
189 | defer s.Unlock()
190 |
191 | u := &User{
192 | ID: id,
193 | Discrim: discrim,
194 | Name: tview.Escape(name),
195 | Nick: tview.Escape(nick),
196 | Color: color,
197 | }
198 |
199 | s.Guilds[guildID] = append(s.Guilds[guildID], u)
200 | return u
201 | }
202 |
--------------------------------------------------------------------------------
/voice.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/diamondburned/discordgo"
7 | "github.com/diamondburned/tview/v2"
8 | )
9 |
10 | func voiceStateUpdate(s *discordgo.Session, vsu *discordgo.VoiceStateUpdate) {
11 | refreshVoiceStates(vsu.VoiceState)
12 | }
13 |
14 | func getVoiceChannel(guildID, channelID int64) (vcs []*discordgo.VoiceState) {
15 | g, err := d.State.Guild(guildID)
16 | if err != nil {
17 | return
18 | }
19 |
20 | for _, vc := range g.VoiceStates {
21 | if vc.ChannelID == channelID {
22 | vcs = append(vcs, vc)
23 | }
24 | }
25 |
26 | return
27 | }
28 |
29 | func canIhearthem(vc *discordgo.VoiceState) bool {
30 | return !(vc.SelfMute || vc.Mute || vc.Suppress)
31 | }
32 |
33 | func refreshVoiceStates(vc *discordgo.VoiceState) {
34 | defer func() {
35 | if r := recover(); r != nil {
36 | Warn(fmt.Sprintln(r))
37 | return
38 | }
39 | }()
40 |
41 | root := guildView.GetRoot()
42 | if root == nil {
43 | return
44 | }
45 |
46 | if vc == nil {
47 | return
48 | }
49 |
50 | root.Walk(func(node, parent *tview.TreeNode) bool {
51 | if parent == nil || node == nil {
52 | return true
53 | }
54 |
55 | reference := node.GetReference()
56 | if reference == nil {
57 | return true
58 | }
59 |
60 | id, ok := reference.(int64)
61 | if !ok {
62 | return true
63 | }
64 |
65 | // user left voice chat
66 | if vc.ChannelID == 0 {
67 | // checks for ID should match the userID instead,
68 | // as the user left voice chat
69 | if id == vc.UserID {
70 | // parent node at this point should be the voice
71 | // channel
72 | var nodes []*tview.TreeNode
73 | for _, ch := range parent.GetChildren() {
74 | if node != ch {
75 | // we add everything except for the user
76 | // that left by adding everything else back
77 | nodes = append(nodes, ch)
78 | }
79 | }
80 |
81 | parent.SetChildren(nodes)
82 | return false
83 | }
84 |
85 | return true
86 | }
87 |
88 | // user joined a voice channel
89 |
90 | if id != vc.ChannelID {
91 | return true
92 | }
93 |
94 | // checks should all pass to confirm this is
95 | // the right voice channel
96 |
97 | refreshVoiceTreeNode(node, vc.GuildID, vc.ChannelID)
98 | return false
99 | })
100 | }
101 |
102 | func refreshVoiceTreeNode(node *tview.TreeNode, guildID, channelID int64) {
103 | var (
104 | nodes []*tview.TreeNode
105 | vcs = getVoiceChannel(guildID, channelID)
106 | )
107 |
108 | for _, vc := range vcs {
109 | vcNode := generateVoiceNode(vc)
110 | if vcNode == nil {
111 | continue
112 | }
113 |
114 | nodes = append(nodes, vcNode)
115 | }
116 |
117 | node.SetChildren(nodes)
118 | }
119 |
120 | func generateVoiceNode(vc *discordgo.VoiceState) *tview.TreeNode {
121 | var color = "d"
122 |
123 | // Reserved for onSpeak
124 | //if !canIhearthem(vc) || vc.SelfDeaf || vc.Deaf {
125 | // color = "d"
126 | //}
127 |
128 | u, err := d.State.Member(vc.GuildID, vc.UserID)
129 | if err != nil {
130 | return nil
131 | }
132 |
133 | if u.User == nil {
134 | return nil
135 | }
136 |
137 | var name = u.User.Username
138 | if u.Nick != "" {
139 | name = u.Nick
140 | }
141 |
142 | var suffix string
143 |
144 | if vc.SelfMute || vc.Mute {
145 | suffix += " [gray][M[][-]"
146 | }
147 |
148 | if vc.SelfDeaf || vc.Deaf {
149 | suffix += " [gray][D[][-]"
150 | }
151 |
152 | if vc.Suppress {
153 | suffix += " [red][Suppressed[][-]"
154 | }
155 |
156 | vcNode := tview.NewTreeNode(
157 | "[::" + color + "]" + tview.Escape(name) + "[::-]" + suffix,
158 | )
159 |
160 | vcNode.SetSelectable(false)
161 | vcNode.SetReference(vc.UserID)
162 | vcNode.SetIndent(cfg.Prop.SidebarIndent - 1)
163 |
164 | return vcNode
165 | }
166 |
--------------------------------------------------------------------------------
/voiceaudio.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | //import (
4 | //"log"
5 |
6 | //"github.com/gordonklaus/portaudio"
7 | //"github.com/diamondburned/discordgo"
8 | //"gitlab.com/diamondburned/dgvoice"
9 | //)
10 |
11 | //var inVoice int64
12 |
13 | //func toggleVoiceJoin(chID int64) {
14 | //if inVoice != 0 {
15 | //d.GatewayManager.ChannelVoiceLeave(chID)
16 | //inVoice = 0
17 | //if chID != 0 {
18 | //Record(chID)
19 | //}
20 | //} else {
21 | //Record(chID)
22 | //}
23 | //}
24 |
25 | //// Record ..
26 | //func Record(chID int64) {
27 | //c, err := d.State.Channel(chID)
28 | //if err != nil {
29 | //Warn("Voice error: " + err.Error())
30 | //}
31 |
32 | //dgv, err := d.GatewayManager.ChannelVoiceJoin(
33 | //c.GuildID, c.ID, true, false,
34 | //)
35 |
36 | //if err != nil {
37 | //Warn(err.Error())
38 | //return
39 | //}
40 |
41 | //inVoice = chID
42 |
43 | //portaudio.Initialize()
44 | //defer portaudio.Terminate()
45 |
46 | //out := make([]int16, 960)
47 |
48 | //stream, err := portaudio.OpenDefaultStream(0, 2, 48000, len(out), &out)
49 | //if err != nil {
50 | //Warn(err.Error())
51 | //return
52 | //}
53 |
54 | //defer stream.Close()
55 |
56 | //if err := stream.Start(); err != nil {
57 | //Warn(err.Error())
58 | //return
59 | //}
60 |
61 | //defer stream.Stop()
62 |
63 | //recv := make(chan *discordgo.Packet, 2)
64 | //go dgvoice.ReceivePCM(dgv, recv)
65 |
66 | //for {
67 | //p, ok := <-recv
68 | //if !ok {
69 | //return
70 | //}
71 |
72 | //log.Println(len(p.PCM))
73 |
74 | //copy(out, p.PCM)
75 |
76 | //if err := stream.Write(); err != nil {
77 | //Warn(err.Error())
78 | //break
79 | //}
80 | //}
81 |
82 | ////portaudio.Initialize()
83 | ////defer portaudio.Terminate()
84 |
85 | ////in := make([]int32, 64)
86 | ////stream, err := portaudio.OpenDefaultStream(1, 0, 44100, len(in), in)
87 | ////if (err) != nil {
88 | ////Warn(err.Error())
89 | ////return
90 | ////}
91 |
92 | ////defer stream.Close()
93 |
94 | ////if err := stream.Start(); err != nil {
95 | ////Warn(err.Error())
96 | ////return
97 | ////}
98 |
99 | ////for {
100 | ////if err := stream.Read(); err != nil {
101 | ////Warn(err.Error())
102 | ////return
103 | ////}
104 |
105 | ////if err := binary.Write(f, binary.BigEndian, in); err != nil {
106 | ////Warn(err.Error())
107 | ////return
108 | ////}
109 |
110 | ////nSamples += len(in)
111 | ////select {
112 | ////case <-sig:
113 | ////return
114 | ////default:
115 | ////}
116 | ////}
117 | ////chk(stream.Stop())
118 | //}
119 |
--------------------------------------------------------------------------------
/w3m/locate.go:
--------------------------------------------------------------------------------
1 | package w3m
2 |
3 | import (
4 | "log"
5 | "os"
6 | "path/filepath"
7 | )
8 |
9 | // Thanks dylan
10 | var (
11 | paths = []string{
12 | "/usr/local/lib/w3m/w3mi*",
13 | "/usr/local/libexec/w3m/w3mi*",
14 | "/usr/local/lib64/w3m/w3mi*",
15 | "/usr/local/libexec64/w3m/w3mi*",
16 | "/usr/lib/w3m/w3mimgdisplay",
17 | "/usr/libexec/w3m/w3mi*",
18 | "/usr/lib64/w3m/w3mimgdisplay",
19 | "/usr/libexec64/w3m/w3mi*",
20 | }
21 | )
22 |
23 | // GetExecPath finds w3mimgdisplay
24 | func GetExecPath() string {
25 | // Todo: find a more performant way to do this
26 | for _, p := range paths {
27 | m, err := filepath.Glob(p)
28 | if err != nil {
29 | log.Println(err)
30 | continue
31 | }
32 |
33 | for _, path := range m {
34 | info, err := os.Stat(path)
35 | if err != nil {
36 | log.Println(err)
37 | continue
38 | }
39 |
40 | if info.Mode()&0111 != 0 {
41 | return path
42 | }
43 | }
44 | }
45 |
46 | return ""
47 | }
48 |
--------------------------------------------------------------------------------
/w3m/w3m.go:
--------------------------------------------------------------------------------
1 | package w3m
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os/exec"
7 | "strings"
8 | )
9 |
10 | var (
11 | w3mpath = GetExecPath()
12 |
13 | // ErrNotFound is returned when w3m can't be found
14 | ErrNotFound = errors.New("w3m not found")
15 | )
16 |
17 | // Arguments is the struct for w3m arguments
18 | // All fields are required
19 | type Arguments struct {
20 | Xoffset int // default: 0
21 | Yoffset int // default: 0
22 | Width int
23 | Height int
24 | Filename string
25 | }
26 |
27 | func Spawn(a Arguments) error {
28 | if w3mpath == "" {
29 | return ErrNotFound
30 | }
31 |
32 | var (
33 | cmd = exec.Command(w3mpath)
34 | args = fmt.Sprintf(
35 | "0;1;%d;%d;%d;%d;;;;;%s\n3;\n4\n",
36 | a.Xoffset, a.Yoffset,
37 | a.Width, a.Height,
38 | a.Filename,
39 | )
40 | )
41 |
42 | reader := strings.NewReader(args)
43 |
44 | cmd.Stdin = reader
45 |
46 | cmd.Run()
47 |
48 | return nil
49 | }
50 |
--------------------------------------------------------------------------------
/warn.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "runtime"
7 |
8 | "github.com/diamondburned/tview/v2"
9 | )
10 |
11 | // Warn ..
12 | func Warn(c string) {
13 | var content string
14 |
15 | {
16 | _, fn, line, ok := runtime.Caller(1)
17 | if ok {
18 | content += fmt.Sprintf("%s:%d ->", fn, line)
19 | }
20 | }
21 |
22 | {
23 | _, fn, line, ok := runtime.Caller(2)
24 | if ok {
25 | content += fmt.Sprintf(" %s:%d -> ", fn, line)
26 | }
27 | }
28 |
29 | log.Println(content + c)
30 |
31 | modal := tview.NewModal()
32 | modal.AddButtons([]string{"mkay"})
33 | modal.SetText(c)
34 | modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
35 | if buttonLabel == "mkay" {
36 | app.SetRoot(appflex, true)
37 | app.SetFocus(input)
38 | }
39 | })
40 |
41 | app.SetRoot(modal, false)
42 | app.SetFocus(modal)
43 | app.Draw()
44 | }
45 |
46 | // Message prints a system message
47 | func Message(m string) {
48 | messageRender <- m
49 | }
50 |
--------------------------------------------------------------------------------
/wraplines.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/diamondburned/tview/v2"
7 | "gitlab.com/diamondburned/6cord/md"
8 | )
9 |
10 | // 2nd arg ::-
11 | // 3rd arg -::
12 | func splitEmbedLine(e string, customMarkup ...string) (spl []string) {
13 | lines := strings.Split(e, "\n")
14 |
15 | // Todo: clean this up ETA never
16 |
17 | var (
18 | cm = ""
19 | ce = ""
20 | )
21 |
22 | if len(customMarkup) > 0 {
23 | cm = customMarkup[0]
24 | ce = "[::-]"
25 | }
26 |
27 | if len(customMarkup) > 1 {
28 | cm += customMarkup[1]
29 | ce += "[-::]"
30 | }
31 |
32 | _, _, col, _ := messagesView.GetInnerRect()
33 |
34 | for _, l := range lines {
35 | splwrap := strings.Split(
36 | md.Parse(tview.Escape(strings.Join(
37 | tview.WordWrap(l, min(col-5, 100)),
38 | "\n",
39 | ))),
40 | "\n",
41 | )
42 |
43 | for _, s := range splwrap {
44 | spl = append(spl, cm+s+ce)
45 | }
46 | }
47 |
48 | return
49 | }
50 |
--------------------------------------------------------------------------------