diff options
| author | Xe Iaso <me@xeiaso.net> | 2024-09-02 09:33:04 -0400 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2024-09-02 09:33:04 -0400 |
| commit | 4628a6e4ba4920925bef6b5dbb6dfd15c7b08a73 (patch) | |
| tree | 34aefa02776bec947c3cb6f38b7c1fd69f7a9d2f /cmd | |
| parent | a2cf1050866bc902ad014ab21767531ff64a337a (diff) | |
| download | x-4628a6e4ba4920925bef6b5dbb6dfd15c7b08a73.tar.xz x-4628a6e4ba4920925bef6b5dbb6dfd15c7b08a73.zip | |
import cmd/aerial
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/aerial/.gitignore | 1 | ||||
| -rw-r--r-- | cmd/aerial/derpi/derpi.go | 87 | ||||
| -rw-r--r-- | cmd/aerial/embed.go | 236 | ||||
| -rw-r--r-- | cmd/aerial/hipster.go | 50 | ||||
| -rw-r--r-- | cmd/aerial/main.go | 88 | ||||
| -rw-r--r-- | cmd/aerial/pvfm.go | 260 |
6 files changed, 722 insertions, 0 deletions
diff --git a/cmd/aerial/.gitignore b/cmd/aerial/.gitignore new file mode 100644 index 0000000..caed212 --- /dev/null +++ b/cmd/aerial/.gitignore @@ -0,0 +1 @@ +aerial diff --git a/cmd/aerial/derpi/derpi.go b/cmd/aerial/derpi/derpi.go new file mode 100644 index 0000000..eab910a --- /dev/null +++ b/cmd/aerial/derpi/derpi.go @@ -0,0 +1,87 @@ +package derpi + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" +) + +// DerpiResults is a struct to contain Derpibooru search results +type DerpiResults struct { + Search []struct { + ID string `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DuplicateReports []interface{} `json:"duplicate_reports"` + FirstSeenAt time.Time `json:"first_seen_at"` + UploaderID string `json:"uploader_id"` + Score int `json:"score"` + CommentCount int `json:"comment_count"` + Width int `json:"width"` + Height int `json:"height"` + FileName string `json:"file_name"` + Description string `json:"description"` + Uploader string `json:"uploader"` + Image string `json:"image"` + Upvotes int `json:"upvotes"` + Downvotes int `json:"downvotes"` + Faves int `json:"faves"` + Tags string `json:"tags"` + TagIds []string `json:"tag_ids"` + AspectRatio float64 `json:"aspect_ratio"` + OriginalFormat string `json:"original_format"` + MimeType string `json:"mime_type"` + Sha512Hash string `json:"sha512_hash"` + OrigSha512Hash string `json:"orig_sha512_hash"` + SourceURL string `json:"source_url"` + Representations struct { + ThumbTiny string `json:"thumb_tiny"` + ThumbSmall string `json:"thumb_small"` + Thumb string `json:"thumb"` + Small string `json:"small"` + Medium string `json:"medium"` + Large string `json:"large"` + Tall string `json:"tall"` + Full string `json:"full"` + } `json:"representations"` + IsRendered bool `json:"is_rendered"` + IsOptimized bool `json:"is_optimized"` + } `json:"search"` + Total int `json:"total"` + Interactions []interface{} `json:"interactions"` +} + +// Perform a Derpibooru search query with a given string of tags and an API key +func SearchDerpi(tags string) (DerpiResults, error) { + + // format for URL query + tags += ",safe" // Enforce the safe tag for PG rating + derpiTags := strings.Replace(tags, " ", "+", -1) + + // make URL query + urlQuery := "https://derpibooru.org/search.json?q=" + derpiTags + resp, err := http.Get(urlQuery) + if err != nil { + return DerpiResults{}, fmt.Errorf("Failed with HTTP error.") + } + + // read response body + defer resp.Body.Close() + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return DerpiResults{}, fmt.Errorf("Failed with error reading response body.") + } + + // parse json + results := DerpiResults{} + err = json.Unmarshal(respBody, &results) + if err != nil { + return DerpiResults{}, fmt.Errorf("Failed with JSON parsing error.") + } + + return results, nil + +} diff --git a/cmd/aerial/embed.go b/cmd/aerial/embed.go new file mode 100644 index 0000000..b6f7d19 --- /dev/null +++ b/cmd/aerial/embed.go @@ -0,0 +1,236 @@ +package main + +import "github.com/bwmarrin/discordgo" + +//Embed ... +type Embed struct { + *discordgo.MessageEmbed +} + +// Constants for message embed character limits +const ( + EmbedLimitTitle = 256 + EmbedLimitDescription = 2048 + EmbedLimitFieldValue = 1024 + EmbedLimitFieldName = 256 + EmbedLimitField = 25 + EmbedLimitFooter = 2048 + EmbedLimit = 4000 +) + +//NewEmbed returns a new embed object +func NewEmbed() *Embed { + return &Embed{&discordgo.MessageEmbed{}} +} + +//SetTitle ... +func (e *Embed) SetTitle(name string) *Embed { + e.Title = name + return e +} + +//SetDescription [desc] +func (e *Embed) SetDescription(description string) *Embed { + if len(description) > 2048 { + description = description[:2048] + } + e.Description = description + return e +} + +//AddField [name] [value] +func (e *Embed) AddField(name, value string) *Embed { + if len(value) > 1024 { + value = value[:1024] + } + + if len(name) > 1024 { + name = name[:1024] + } + + e.Fields = append(e.Fields, &discordgo.MessageEmbedField{ + Name: name, + Value: value, + }) + + return e + +} + +//SetFooter [Text] [iconURL] +func (e *Embed) SetFooter(args ...string) *Embed { + iconURL := "" + text := "" + proxyURL := "" + + switch { + case len(args) > 2: + proxyURL = args[2] + fallthrough + case len(args) > 1: + iconURL = args[1] + fallthrough + case len(args) > 0: + text = args[0] + case len(args) == 0: + return e + } + + e.Footer = &discordgo.MessageEmbedFooter{ + IconURL: iconURL, + Text: text, + ProxyIconURL: proxyURL, + } + + return e +} + +//SetImage ... +func (e *Embed) SetImage(args ...string) *Embed { + var URL string + var proxyURL string + + if len(args) == 0 { + return e + } + if len(args) > 0 { + URL = args[0] + } + if len(args) > 1 { + proxyURL = args[1] + } + e.Image = &discordgo.MessageEmbedImage{ + URL: URL, + ProxyURL: proxyURL, + } + return e +} + +//SetThumbnail ... +func (e *Embed) SetThumbnail(args ...string) *Embed { + var URL string + var proxyURL string + + if len(args) == 0 { + return e + } + if len(args) > 0 { + URL = args[0] + } + if len(args) > 1 { + proxyURL = args[1] + } + e.Thumbnail = &discordgo.MessageEmbedThumbnail{ + URL: URL, + ProxyURL: proxyURL, + } + return e +} + +//SetAuthor ... +func (e *Embed) SetAuthor(args ...string) *Embed { + var ( + name string + iconURL string + URL string + proxyURL string + ) + + if len(args) == 0 { + return e + } + if len(args) > 0 { + name = args[0] + } + if len(args) > 1 { + iconURL = args[1] + } + if len(args) > 2 { + URL = args[2] + } + if len(args) > 3 { + proxyURL = args[3] + } + + e.Author = &discordgo.MessageEmbedAuthor{ + Name: name, + IconURL: iconURL, + URL: URL, + ProxyIconURL: proxyURL, + } + + return e +} + +//SetURL ... +func (e *Embed) SetURL(URL string) *Embed { + e.URL = URL + return e +} + +//SetColor ... +func (e *Embed) SetColor(clr int) *Embed { + e.Color = clr + return e +} + +// InlineAllFields sets all fields in the embed to be inline +func (e *Embed) InlineAllFields() *Embed { + for _, v := range e.Fields { + v.Inline = true + } + return e +} + +// Truncate truncates any embed value over the character limit. +func (e *Embed) Truncate() *Embed { + e.TruncateDescription() + e.TruncateFields() + e.TruncateFooter() + e.TruncateTitle() + return e +} + +// TruncateFields truncates fields that are too long +func (e *Embed) TruncateFields() *Embed { + if len(e.Fields) > 25 { + e.Fields = e.Fields[:EmbedLimitField] + } + + for _, v := range e.Fields { + + if len(v.Name) > EmbedLimitFieldName { + v.Name = v.Name[:EmbedLimitFieldName] + } + + if len(v.Value) > EmbedLimitFieldValue { + v.Value = v.Value[:EmbedLimitFieldValue] + } + + } + return e +} + +// TruncateDescription ... +func (e *Embed) TruncateDescription() *Embed { + if len(e.Description) > EmbedLimitDescription { + e.Description = e.Description[:EmbedLimitDescription] + } + return e +} + +// TruncateTitle ... +func (e *Embed) TruncateTitle() *Embed { + if len(e.Title) > EmbedLimitTitle { + e.Title = e.Title[:EmbedLimitTitle] + } + return e +} + +// TruncateFooter ... +func (e *Embed) TruncateFooter() *Embed { + if e.Footer != nil && len(e.Footer.Text) > EmbedLimitFooter { + e.Footer.Text = e.Footer.Text[:EmbedLimitFooter] + } + return e +}
\ No newline at end of file diff --git a/cmd/aerial/hipster.go b/cmd/aerial/hipster.go new file mode 100644 index 0000000..e6728c5 --- /dev/null +++ b/cmd/aerial/hipster.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "strings" + + "github.com/bwmarrin/discordgo" +) + +func hipster(s *discordgo.Session, m *discordgo.Message, parv []string) error { + msg, err := getHipsterText() + if err != nil { + return err + } + + s.ChannelMessageSend(m.ChannelID, msg) + return nil +} + +func getHipsterText() (string, error) { + resp, err := http.Get("http://hipsterjesus.com/api/?type=hipster-centric&html=false") // paras parameter no longer respected, need to re-implement locally + if err != nil { + return "", err + } + + textStruct := &struct { + Text string `json:"text"` + }{} + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + json.Unmarshal(body, textStruct) + + text := strings.Split(textStruct.Text, ". ")[0] + textSlice := strings.Split(text, " ") // Separate each word into an array + truncatedText := "" + + wordCount := 5 // change this to adjust word count + + for i := 0; i < wordCount; i++ { + truncatedText += textSlice[i] + " " + } + + return truncatedText, nil +} diff --git a/cmd/aerial/main.go b/cmd/aerial/main.go new file mode 100644 index 0000000..e62e977 --- /dev/null +++ b/cmd/aerial/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "regexp" + "time" + + "github.com/bwmarrin/discordgo" + _ "github.com/joho/godotenv/autoload" + "within.website/x/internal/pvfm/bot" + "within.website/x/internal/pvfm/commands/source" +) + +type aerial struct { + cs *bot.CommandSet + s *discordgo.Session +} + +const ( + djonHelp = `` + djoffHelp = `` + setupHelp = `` +) + +func (a *aerial) Handle(s *discordgo.Session, m *discordgo.MessageCreate) { + err := a.cs.Run(s, m.Message) + if err != nil { + log.Println(err) + } +} + +var ( + token = os.Getenv("TOKEN") + youtubeSpamRoomID = os.Getenv("DISCORD_YOUTUBESPAM_ROOMID") + gClientID = os.Getenv("GOOGLE_CLIENT_ID") + gClientSecret = os.Getenv("GOOGLE_CLIENT_SECRET") + + musicLinkRegex = regexp.MustCompile(`(.*)((http(s?):\/\/(www\.)?soundcloud.com\/.*)|(http(s?):\/\/(www\.)?youtube.com\/.*)|(http(s?):\/\/(www\.)?youtu.be\/.*))(.*)|(.*)http(s?):\/\/(www\.)?mixcloud.com\/.*`) +) + +func main() { + flag.Parse() + dg, err := discordgo.New("Bot " + token) + if err != nil { + log.Fatal(err) + } + + a := &aerial{ + cs: bot.NewCommandSet(), + s: dg, + } + + a.cs.Prefix = ";" + a.cs.AddCmd("np", "shows radio station statistics for Ponyville FM", bot.NoPermissions, stats) + a.cs.AddCmd("stats", "shows radio station statistics for Ponyville FM", bot.NoPermissions, stats) + a.cs.AddCmd("dj", "shows which DJ is up next on Ponyville FM", bot.NoPermissions, stats) + a.cs.AddCmd("schedule", "shows the future radio schedule for Ponyville FM", bot.NoPermissions, schedule) + a.cs.AddCmd("hipster", "hip me up fam", bot.NoPermissions, hipster) + a.cs.AddCmd("source", "source code information", bot.NoPermissions, source.Source) + a.cs.AddCmd("time", "shows the current bot time", bot.NoPermissions, curTime) + a.cs.AddCmd("streams", "shows the different Ponyville FM stream links", bot.NoPermissions, streams) + a.cs.AddCmd("servers", "shows the different Ponyville FM stream links", bot.NoPermissions, streams) + a.cs.AddCmd("derpi", "grabs a random **__safe__** image from Derpibooru with the given search results", bot.NoPermissions, derpi) + a.cs.AddCmd("weather", "how's the weather right now?", bot.NoPermissions, weather) + + dg.AddHandler(a.Handle) + dg.AddHandler(pesterLink) + dg.AddHandler(messageCreate) + + err = dg.Open() + if err != nil { + log.Fatal(err) + } + + log.Println("ready") + + <-make(chan struct{}) +} + +// This function will be called (due to AddHandler above) every time a new +// message is created on any channel that the autenticated bot has access to. +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + // Print message to stdout. + fmt.Printf("%20s %20s %20s %20s > %s\n", m.Author.ID, m.ChannelID, time.Now().Format(time.Stamp), m.Author.Username, m.Content) +} diff --git a/cmd/aerial/pvfm.go b/cmd/aerial/pvfm.go new file mode 100644 index 0000000..97f13bb --- /dev/null +++ b/cmd/aerial/pvfm.go @@ -0,0 +1,260 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/bwmarrin/discordgo" + derpiSearch "within.website/x/cmd/aerial/derpi" + "within.website/x/internal/pvfm" + pvfmschedule "within.website/x/internal/pvfm/schedule" + "within.website/x/internal/pvfm/station" +) + +func init() { + rand.Seed(time.Now().Unix()) +} + +// randomRange gives a random whole integer between the given integers [min, max) +func randomRange(min, max int) int { + return rand.Intn(max-min) + min +} + +func pesterLink(s *discordgo.Session, m *discordgo.MessageCreate) { + if musicLinkRegex.Match([]byte(m.Content)) { + i, err := pvfm.GetStats() + if err != nil { + log.Println(err) + return + } + + if i.IsDJLive() && m.ChannelID == youtubeSpamRoomID { + s.ChannelMessageSend(m.ChannelID, "Please be mindful sharing links to music when a DJ is performing. Thanks!") + } + } +} + +func stats(s *discordgo.Session, m *discordgo.Message, parv []string) error { + i, err := pvfm.GetStats() + if err != nil { + log.Printf("Error getting the station info: %v, falling back to plan b", err) + return doStatsFromStation(s, m, parv) + } + + st, err := station.GetStats() + if err != nil { + return err + } + + var l int + var peak int + + for _, source := range st.Icestats.Source { + l = l + source.Listeners + peak = peak + source.ListenerPeak + } + + // checks if the event is currently happening + outputEmbed := NewEmbed(). + SetTitle("Listener Statistics"). + SetDescription("Use `;streams` if you need a link to the radio!\nTotal listeners across all stations: " + strconv.Itoa(i.Listeners.Listeners) + " with a maximum of " + strconv.Itoa(peak) + ".") + + outputEmbed.AddField("🎵 Main", strconv.Itoa(i.Main.Listeners)+" listeners.\n"+i.Main.Nowplaying) + outputEmbed.AddField("🎵 Chill", strconv.Itoa(i.Secondary.Listeners)+" listeners.\n"+i.Secondary.Nowplaying) + outputEmbed.AddField("🎵 Free! (no DJ sets)", strconv.Itoa(i.MusicOnly.Listeners)+" listeners.\n"+i.MusicOnly.Nowplaying) + + outputEmbed.InlineAllFields() + + s.ChannelMessageSendEmbed(m.ChannelID, outputEmbed.MessageEmbed) + + return nil +} + +func schedule(s *discordgo.Session, m *discordgo.Message, parv []string) error { + schEntries, err := pvfmschedule.Get() + if err != nil { + return err + } + + // Create embed object + outputEmbed := NewEmbed(). + SetTitle("Upcoming Shows"). + SetDescription("These are the upcoming shows and events airing soon on PVFM 1.\n[Convert to your timezone](https://www.worldtimebuddy.com/?pl=1&lid=100&h=100)") + + for _, entry := range schEntries { + + // Format countdown timer + startTimeUnix := time.Unix(int64(entry.StartUnix), 0) + nowWithoutNanoseconds := time.Unix(time.Now().Unix(), 0) + dur := startTimeUnix.Sub(nowWithoutNanoseconds) + + // Show "Live Now!" if the timer is less than 0h0m0s + if dur > 0 { + outputEmbed.AddField(":musical_note: "+entry.Host+" - "+entry.Name, entry.StartTime+" "+entry.Timezone+"\nAirs in "+dur.String()) + } else { + outputEmbed.AddField(":musical_note: "+entry.Host+" - "+entry.Name, "Live now!") + } + } + + s.ChannelMessageSendEmbed(m.ChannelID, outputEmbed.MessageEmbed) + return nil +} + +func doStationRequest(s *discordgo.Session, m *discordgo.Message, parv []string) error { + stats, err := station.GetStats() + if err != nil { + return err + } + + result := fmt.Sprintf( + "Now playing: %s - %s on Ponyville FM!", + stats.Icestats.Source[0].Title, + stats.Icestats.Source[0].Artist, + ) + + s.ChannelMessageSend(m.ChannelID, result) + return nil +} + +func doStatsFromStation(s *discordgo.Session, m *discordgo.Message, parv []string) error { + st, err := station.GetStats() + if err != nil { + return err + } + + var l int + var peak int + + for _, source := range st.Icestats.Source { + l = l + source.Listeners + peak = peak + source.ListenerPeak + } + + result := []string{ + fmt.Sprintf("Current listeners: %d with a maximum of %d!", l, peak), + } + + s.ChannelMessageSend(m.ChannelID, strings.Join(result, "\n")) + return nil +} + +func curTime(s *discordgo.Session, m *discordgo.Message, parv []string) error { + s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("The time currently is %s\nUse <https://www.worldtimebuddy.com/?pl=1&lid=100&h=100> to convert UTC to your local timezone.", time.Now().UTC().Format("2006-01-02 15:04:05 UTC"))) + + return nil +} + +const pvfmList = `SSL SAFE Streams +PonyvilleFM Europe OGG Stream: +https://dj.bronyradio.com/pvfm1.ogg +PVFM AAC+ 3G/4G Mobile Stream: +https://dj.bronyradio.com/pvfm1mobile.aac +PonyvilleFM Free MP3 24/7 Pony Stream: +https://dj.bronyradio.com/pvfmfree.mp3 +PonyvilleFM Free OGG 24/7 Pony Stream: +https://dj.bronyradio.com/pvfmfree.ogg +PVFM OPUS Stream: +https://dj.bronyradio.com/pvfmopus.ogg +PonyvilleFM Europe Stream: +https://dj.bronyradio.com/stream.mp3 +PonyvilleFM High Quality Europe Stream: +https://dj.bronyradio.com/streamhq.mp3 + +Legacy Streams (non https) +PonyvilleFM Europe OGG Stream: +http://dj.bronyradio.com:8000/pvfm1.ogg +PonyvilleFM Europe Stream: +http://dj.bronyradio.com:8000/stream.mp3 +PonyvilleFM Free MP3 24/7 Pony Stream: +http://dj.bronyradio.com:8000/pvfmfree.mp3 +PonyvilleFM Free OGG 24/7 Pony Stream: +http://dj.bronyradio.com:8000/pvfmfree.ogg +PVFM AAC+ 3G/4G Mobile Stream: +http://dj.bronyradio.com:8000/pvfm1mobile.aac` + +func streams(s *discordgo.Session, m *discordgo.Message, parv []string) error { + // start building custom embed + outputEmbed := NewEmbed(). + SetTitle("Stream Links"). + SetDescription("These are direct feeds of the live streams; most browsers and media players can play them!") + + // PVFM + outputEmbed.AddField(":musical_note: PVFM Servers", pvfmList) + // Luna Radio + outputEmbed.AddField(":musical_note: Luna Radio Servers", "Luna Radio MP3 128Kbps Stream:\n<http://radio.ponyvillelive.com:8002/stream.mp3>\nLuna Radio Mobile MP3 64Kbps Stream:\n<http://radio.ponyvillelive.com:8002/mobile?;stream.mp3>\n") + // Recordings + outputEmbed.AddField(":cd: DJ Recordings", "Archive\n<https://pvfm.within.lgbt/var/93252527679639552/>\nLegacy Archive\n<https://pvfm.within.lgbt/BronyRadio/>") + + s.ChannelMessageSendEmbed(m.ChannelID, outputEmbed.MessageEmbed) + + // no errors yay!!!! + return nil +} + +func derpi(s *discordgo.Session, m *discordgo.Message, parv []string) error { + if m.ChannelID == "292755043684450304" { + + searchResults, err := derpiSearch.SearchDerpi(m.Content[7:len(m.Content)]) // Safe tag will be added in derpi/derpi.go + if err != nil { + s.ChannelMessageSend(m.ChannelID, "An error occured.") + return err + } + if len(searchResults.Search) < 1 { + s.ChannelMessageSend(m.ChannelID, "Error: No results") + return nil + } + derpiImage := searchResults.Search[randomRange(0, len(searchResults.Search))] + + tags := strings.Split(derpiImage.Tags, ", ") // because this isn't an array for some reason + + // Check for artist tag + artist := "" + for _, tag := range tags { + if strings.Contains(tag, "artist:") { + artist = tag[7:] + } + } + + outputEmbed := NewEmbed(). + SetTitle("Derpibooru Image"). + SetURL("https://derpibooru.org/" + derpiImage.ID). + SetDescription(derpiImage.Description). + SetImage("http:" + derpiImage.Image). + SetFooter("Image score: " + strconv.Itoa(derpiImage.Score) + " | Uploaded: " + derpiImage.CreatedAt.String()) + + // Credit the artist! + if artist == "" { + outputEmbed.SetAuthor("No artist") + } else { + outputEmbed.SetAuthor("Artist: " + artist) + } + + s.ChannelMessageSendEmbed(m.ChannelID, outputEmbed.MessageEmbed) + } else { + s.ChannelMessageSend(m.ChannelID, "Please use this command in <#292755043684450304> only.") + } + return nil +} + +func weather(s *discordgo.Session, m *discordgo.Message, parv []string) error { + responses := []string{ + "Cloudy with a chance of meatballs.", + "It's currently pouring down even more than Pinkie.", + "It's the most overcast I've ever seen. In other words, same as always.", + "Do you have a better conversation starter than that?", + "There's at least 5 or 6 weather right now, my dude.", + "It's soggy enough for Rainbow Dash to get fired, if she didn't have a literal deity keeping her in charge.", + "Surprisingly, the weather is pretty alright.", + "You'd be happy to know that it's hot enough to make a phoenix sweat.", + "The weather right now is like you took London and stuck it in a dishwasher.", + "The Crystal Empire is warmer than this weather.", + } + + s.ChannelMessageSend(m.ChannelID, responses[randomRange(0, len(responses))]) + + return nil +} |
