Skip to content

Commit 38ebac9

Browse files
committed
first
0 parents  commit 38ebac9

File tree

6 files changed

+391
-0
lines changed

6 files changed

+391
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
build
2+
scrap
3+
go.sum

Procfile

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
worker: bin/main

go.mod

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/Pyorot/streams
2+
3+
go 1.14
4+
5+
require (
6+
github.com/bwmarrin/discordgo v0.20.2
7+
github.com/joho/godotenv v1.3.0
8+
github.com/nicklaw5/helix v0.5.7
9+
)

main/main.go

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"os"
7+
"strings"
8+
"time"
9+
10+
"github.com/bwmarrin/discordgo"
11+
"github.com/joho/godotenv"
12+
"github.com/nicklaw5/helix"
13+
)
14+
15+
var err error // placeholder error
16+
var env = make(map[string]string) // environment variables
17+
var twitch *helix.Client // Twitch client
18+
var discord *discordgo.Session // Discord client
19+
var twicord = make(map[string]string) // map: twitch username -> Discord user ID
20+
var getStreamsParams helix.StreamsParams // const argument for getStreams calls
21+
22+
type stream helix.Stream
23+
24+
func exitIfError(err error) {
25+
if err != nil {
26+
panic(err)
27+
}
28+
}
29+
30+
func init() {
31+
// load env vars from .env file
32+
if _, exists := os.LookupEnv("TWITCH_KEY"); !exists {
33+
exitIfError(godotenv.Load())
34+
}
35+
36+
// register env vars; assert existence
37+
for _, key := range []string{
38+
"TWITCH_KEY", "DISCORD_TOKEN", "GAME_ID",
39+
"CHANNEL_ID", "ROLE_SERVER_ID", "ROLE_ID",
40+
"TWICORD_CHANNEL_ID",
41+
} {
42+
val, exists := os.LookupEnv(key)
43+
if exists {
44+
env[key] = val
45+
} else {
46+
panic(fmt.Sprintf("Env var '%s' not found.", key))
47+
}
48+
}
49+
50+
// init clients + constants
51+
twitch, err = helix.NewClient(&helix.Options{ClientID: env["TWITCH_KEY"]})
52+
exitIfError(err)
53+
discord, err = discordgo.New("Bot " + env["DISCORD_TOKEN"])
54+
exitIfError(err)
55+
getStreamsParams = helix.StreamsParams{GameIDs: []string{env["GAME_ID"]}, First: 60}
56+
57+
// start threads
58+
go msg()
59+
}
60+
61+
func main() {
62+
// async state init
63+
task1, task2, task3 := msgInit(), roleInit(), twicordInit()
64+
_, _, _ = <-task1, <-task2, <-task3
65+
fmt.Printf(". | initialised\n")
66+
67+
// loop
68+
for {
69+
new, err := fetch()
70+
if err == nil {
71+
fmt.Printf("\n< | %s\n", time.Now().Format("15:04:05"))
72+
msgCh <- new
73+
go role(*new)
74+
time.Sleep(60 * time.Second)
75+
}
76+
}
77+
}
78+
79+
func fetch() (*map[string]*stream, error) {
80+
dict := make(map[string]*stream)
81+
res, err := twitch.GetStreams(&getStreamsParams)
82+
if err == nil && res.StatusCode != 200 { // reinterpret HTTP error as actual error
83+
err = fmt.Errorf("HTTP %d", res.StatusCode)
84+
}
85+
if err == nil {
86+
list := res.Data.Streams
87+
for i := range list {
88+
s := stream(list[i])
89+
dict[strings.ToLower(list[i].UserName)] = &s
90+
}
91+
} else {
92+
fmt.Printf("\nx | < : %s\n", err)
93+
}
94+
// convert to map: Username -> *Stream
95+
return &dict, err
96+
}
97+
98+
func twicordInit() chan (bool) {
99+
res := make(chan (bool), 1)
100+
go func() {
101+
history, err := discord.ChannelMessages(env["TWICORD_CHANNEL_ID"], 20, "", "", "")
102+
exitIfError(err)
103+
for _, msg := range history {
104+
if len(msg.Content) >= 8 && msg.Content[:7] == "twicord" {
105+
scanner := bufio.NewScanner(strings.NewReader(msg.Content))
106+
scanner.Scan()
107+
for scanner.Scan() {
108+
line := scanner.Text()
109+
splitIndex := strings.IndexByte(line, ' ')
110+
twicord[line[splitIndex+1:]] = line[:splitIndex]
111+
}
112+
exitIfError(scanner.Err())
113+
}
114+
}
115+
fmt.Printf(". | twicord loaded [%d]\n", len(twicord))
116+
res <- true
117+
}()
118+
return res
119+
}

main/msg.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/bwmarrin/discordgo"
9+
)
10+
11+
var msgs = make(map[string]*msgsEntry, 60)
12+
var msgCh = make(chan (*map[string]*stream))
13+
14+
type msgsEntry struct {
15+
stream *stream
16+
msgID string
17+
}
18+
19+
type command struct {
20+
action rune
21+
user string
22+
stream *stream
23+
}
24+
25+
func msgInit() chan (bool) {
26+
res := make(chan (bool), 1)
27+
go func() {
28+
// load message history
29+
history, err := discord.ChannelMessages(env["CHANNEL_ID"], 50, "", "", "")
30+
exitIfError(err)
31+
// decode + register green messages
32+
for _, msg := range history {
33+
if len(msg.Embeds) == 1 && msg.Embeds[0].Color == 0x00ff00 {
34+
user := msg.Embeds[0].Title[:strings.IndexByte(msg.Embeds[0].Title, ' ')]
35+
startTime, err := time.Parse("2006-01-02T15:04:05-07:00", msg.Embeds[0].Timestamp)
36+
exitIfError(err)
37+
stream := stream{
38+
UserName: user,
39+
Title: msg.Embeds[0].Description,
40+
StartedAt: startTime,
41+
}
42+
msgs[strings.ToLower(user)] = &msgsEntry{&stream, msg.ID}
43+
}
44+
}
45+
fmt.Printf("m | init [%d]\n", len(msgs))
46+
res <- true
47+
}()
48+
return res
49+
}
50+
51+
func msg() {
52+
for {
53+
new := *<-msgCh
54+
commands := make([]command, 0)
55+
56+
// generate command queue from new data
57+
for user := range msgs {
58+
_, isInNew := new[user]
59+
if !isInNew { // remove
60+
commands = append(commands, command{'r', user, nil})
61+
fmt.Printf("m | - %s\n", user)
62+
}
63+
}
64+
for user := range new {
65+
streamNew := new[user]
66+
_, isInOld := msgs[user]
67+
if isInOld && streamNew.Title != msgs[user].stream.Title { // update
68+
commands = append(commands, command{'e', user, streamNew})
69+
fmt.Printf("m | ~ %s\n", user)
70+
} else if !isInOld { // add
71+
commands = append(commands, command{'a', user, streamNew})
72+
fmt.Printf("m | + %s\n", user)
73+
}
74+
}
75+
76+
// process command queue
77+
for _, cmd := range commands {
78+
switch cmd.action {
79+
case 'a':
80+
msgID := msgAdd()
81+
msgEdit(msgID, cmd.stream, true)
82+
msgs[cmd.user] = &msgsEntry{cmd.stream, msgID}
83+
case 'e':
84+
msgEdit(msgs[cmd.user].msgID, cmd.stream, true)
85+
msgs[cmd.user].stream = cmd.stream
86+
case 'r':
87+
// calc swap ID of closing msg with oldest open msg (so open msgs stay grouped at bottom)
88+
user, msgID := cmd.user, msgs[cmd.user].msgID
89+
minUser, minID := user, msgID
90+
for userTest := range msgs {
91+
if strings.Compare(msgs[userTest].msgID, minID) == -1 {
92+
minUser, minID = userTest, msgs[userTest].msgID
93+
}
94+
}
95+
if minID != msgID {
96+
fmt.Printf("m | %s ↔ %s\n", user, minUser)
97+
msgs[user].msgID, msgs[minUser].msgID = minID, msgID
98+
msgEdit(msgs[minUser].msgID, msgs[minUser].stream, true)
99+
}
100+
msgEdit(msgs[user].msgID, msgs[user].stream, false)
101+
// update messages at new IDs
102+
delete(msgs, user)
103+
}
104+
}
105+
fmt.Printf("m | ok [%d]\n", len(msgs))
106+
}
107+
}
108+
109+
func msgAdd() (msgID string) {
110+
for {
111+
msgOut, err := discord.ChannelMessageSendComplex(
112+
env["CHANNEL_ID"],
113+
&discordgo.MessageSend{Embed: &discordgo.MessageEmbed{Color: 0xffff00}},
114+
)
115+
time.Sleep(time.Second)
116+
if err != nil {
117+
fmt.Printf("x | m+: %s\n", err)
118+
} else {
119+
return msgOut.ID
120+
}
121+
}
122+
}
123+
124+
func msgEdit(msgID string, stream *stream, active bool) {
125+
for {
126+
_, err := discord.ChannelMessageEditComplex(&discordgo.MessageEdit{
127+
Channel: env["CHANNEL_ID"],
128+
ID: msgID,
129+
Embed: generateMsg(stream, active),
130+
})
131+
time.Sleep(time.Second)
132+
if err != nil {
133+
fmt.Printf("x | m~: %s\n", err)
134+
} else {
135+
return
136+
}
137+
}
138+
}
139+
140+
func generateMsg(s *stream, live bool) *discordgo.MessageEmbed {
141+
var colour int
142+
var postText, thumbnail string
143+
if live {
144+
colour = 0x00ff00
145+
postText = " is live"
146+
if len(s.ThumbnailURL) >= 20 {
147+
thumbnail = s.ThumbnailURL[:len(s.ThumbnailURL)-20] + "440x248.jpg"
148+
}
149+
} else {
150+
colour = 0xff0000
151+
postText = " was live"
152+
}
153+
return &discordgo.MessageEmbed{
154+
Title: s.UserName + postText,
155+
Description: s.Title,
156+
URL: "https://twitch.tv/" + s.UserName,
157+
Color: colour,
158+
Thumbnail: &discordgo.MessageEmbedThumbnail{URL: thumbnail},
159+
Timestamp: s.StartedAt.Format("2006-01-02T15:04:05Z"),
160+
Footer: &discordgo.MessageEmbedFooter{IconURL: "https://www.mariowiki.com/images/thumb/b/be/SMS_Shine_Sprite_Artwork.png/805px-SMS_Shine_Sprite_Artwork.png"},
161+
}
162+
}

main/role.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"time"
6+
)
7+
8+
var roles = make(map[string]string)
9+
10+
func roleInit() chan (bool) {
11+
res := make(chan (bool), 1)
12+
go func() {
13+
next := ""
14+
userCount := 0
15+
for {
16+
users, err := discord.GuildMembers(env["ROLE_SERVER_ID"], next, 300)
17+
exitIfError(err)
18+
if len(users) == 0 {
19+
break
20+
} else {
21+
next = users[len(users)-1].User.ID
22+
userCount += len(users)
23+
for _, user := range users {
24+
for _, role := range user.Roles {
25+
if role == env["ROLE_ID"] {
26+
roles[user.User.ID] = user.User.ID
27+
break
28+
}
29+
}
30+
}
31+
}
32+
}
33+
fmt.Printf("r | init [%d/%d]\n", len(roles), userCount)
34+
res <- true
35+
}()
36+
return res
37+
}
38+
39+
func role(new map[string]*stream) {
40+
addsCh := make(map[string]chan (bool))
41+
removesCh := make(map[string]chan (bool))
42+
43+
for user := range roles {
44+
_, isInNew := new[user]
45+
if !isInNew { // remove
46+
removesCh[user] = roleRemove(roles[user])
47+
time.Sleep(2 * time.Second)
48+
fmt.Printf("r | - %s\n", user)
49+
}
50+
}
51+
for user := range new {
52+
_, isInOld := roles[user]
53+
userID, isReg := twicord[user]
54+
if !isInOld && isReg { // add
55+
addsCh[user] = roleAdd(userID)
56+
time.Sleep(2 * time.Second)
57+
fmt.Printf("r | + %s\n", user)
58+
}
59+
}
60+
61+
for user, ch := range removesCh {
62+
if <-ch {
63+
delete(roles, user)
64+
}
65+
}
66+
for user, ch := range addsCh {
67+
if <-ch {
68+
roles[user] = twicord[user]
69+
}
70+
}
71+
72+
fmt.Printf("r | ok [%d]\n", len(roles))
73+
}
74+
75+
func roleAdd(userID string) chan (bool) {
76+
res := make(chan (bool), 1)
77+
go func() {
78+
err := discord.GuildMemberRoleAdd(env["ROLE_SERVER_ID"], userID, env["ROLE_ID"])
79+
if err != nil {
80+
fmt.Printf("x | r+ | %s – %s\n", userID, err)
81+
}
82+
res <- err == nil
83+
}()
84+
return res
85+
}
86+
87+
func roleRemove(userID string) chan (bool) {
88+
res := make(chan (bool), 1)
89+
go func() {
90+
err := discord.GuildMemberRoleRemove(env["ROLE_SERVER_ID"], userID, env["ROLE_ID"])
91+
if err != nil {
92+
fmt.Printf("x | r- | %s – %s\n", userID, err)
93+
}
94+
res <- err == nil
95+
}()
96+
return res
97+
}

0 commit comments

Comments
 (0)