Last active
February 4, 2017 03:26
-
-
Save 1lann/147edc662da628539f3f09d02c1a8ec6 to your computer and use it in GitHub Desktop.
A bot that posts notifications about new comments and posts on the Riot API Developer Forums.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"log" | |
"net/http" | |
"net/url" | |
"strconv" | |
"strings" | |
"sync" | |
"time" | |
"github.com/PuerkitoBio/goquery" | |
"github.com/bwmarrin/discordgo" | |
) | |
// Forum notifications | |
const channelID = "276779706941177856" | |
const commentColor = 0x2196F3 | |
const discussionColor = 0x00E676 | |
const developerSite = "https://developer.riotgames.com" | |
const apolloPath = "https://apollo.developer.leagueoflegends.com/apollo/applications/" | |
const maxBodyLength = 2000 | |
var thresholdTime = time.Minute * 10 | |
var utcLocation *time.Location | |
var currentVersion string | |
type threadState struct { | |
lastComment time.Time | |
numComments int | |
} | |
var threadStates = make(map[string]threadState) | |
var threadStatesMutex = new(sync.Mutex) | |
var session *discordgo.Session | |
var pages = []string{ | |
"https://developer.riotgames.com/discussion/index", | |
"https://developer.riotgames.com/discussion/announcements", | |
"https://developer.riotgames.com/discussion/bugs-feedback", | |
"https://developer.riotgames.com/discussion/technical-help", | |
"https://developer.riotgames.com/discussion/tutorials-libraries", | |
} | |
// Comment represents a comment posted in a discussion. | |
type Comment struct { | |
Message string `json:"message"` | |
User struct { | |
Name string `json:"name"` | |
Realm string `json:"realm"` | |
ProfileIcon string `json:"lolProfileIcon"` | |
} `json:"user"` | |
Replies struct { | |
Comments []Comment `json:"comments"` | |
} `json:"replies"` | |
CreatedAt ApolloTime `json:"createdAt"` | |
ModifiedAt ApolloTime `json:"modifiedAt"` | |
} | |
// Thread represents the information of a discussion (thread). | |
type Thread struct { | |
Discussion struct { | |
ID string `json:"id"` | |
Title string `json:"title"` | |
User struct { | |
Name string `json:"name"` | |
Realm string `json:"realm"` | |
ProfileIcon string `json:"lolProfileIcon"` | |
} `json:"user"` | |
SoftComments int `json:"softComments"` | |
Comments struct { | |
Comments []Comment `json:"comments"` | |
} `json:"comments"` | |
CreatedAt ApolloTime `json:"createdAt"` | |
ModifiedAt ApolloTime `json:"modifiedAt"` | |
LastCommentedAt ApolloTime `json:"lastCommentedAt"` | |
Content struct { | |
Body string `json:"body"` | |
} `json:"content"` | |
Application struct { | |
Name string `json:"name"` | |
} `json:"application"` | |
} `json:"discussion"` | |
} | |
// ApolloTime represents the time inside Apollo responses | |
type ApolloTime struct { | |
time.Time | |
} | |
// UnmarshalJSON is the JSON unmarshaller for ApolloTime | |
func (t *ApolloTime) UnmarshalJSON(b []byte) error { | |
if string(b) == "null" { | |
return nil | |
} | |
str, err := strconv.Unquote(string(b)) | |
if err != nil { | |
return err | |
} | |
t.Time, err = time.Parse("2006-01-02T15:04:05.999999999-0700", str) | |
return err | |
} | |
func janitor() { | |
t := time.Tick(time.Hour * 24) | |
for _ = range t { | |
threadStatesMutex.Lock() | |
for key, state := range threadStates { | |
if time.Since(state.lastComment) > time.Hour*48 { | |
delete(threadStates, key) | |
} | |
} | |
threadStatesMutex.Unlock() | |
updateVersion() | |
} | |
} | |
func init() { | |
utcLocation, _ = time.LoadLocation("UTC") | |
updateVersion() | |
} | |
func updateVersion() { | |
resp, err := http.Get("https://ddragon.leagueoflegends.com/realms/oce.json") | |
if err != nil { | |
log.Println("version checker: failed to get version:", err) | |
return | |
} | |
defer resp.Body.Close() | |
if resp.StatusCode != http.StatusOK { | |
log.Println("version checker: version not OK:", resp.Status) | |
return | |
} | |
dec := json.NewDecoder(resp.Body) | |
var version struct { | |
Dd string `json:"dd"` | |
} | |
if err := dec.Decode(&version); err != nil { | |
log.Println("version checker: version decode error:", err) | |
return | |
} | |
if currentVersion == version.Dd { | |
return | |
} | |
currentVersion = version.Dd | |
log.Println("version checker: using version " + currentVersion) | |
return | |
} | |
func main() { | |
var err error | |
session, err = discordgo.New(loginEmail, loginPassword) | |
if err != nil { | |
panic(err) | |
} | |
err = session.Open() | |
if err != nil { | |
panic(err) | |
} | |
fmt.Println("Running...") | |
go janitor() | |
t := time.Tick(time.Second * 10) | |
for _ = range t { | |
log.Println("checking...") | |
for _, page := range pages { | |
checkNewUpdates(page) | |
} | |
log.Println("done checking") | |
} | |
} | |
func checkNewUpdates(page string) { | |
doc, err := goquery.NewDocument(page) | |
if err != nil { | |
log.Println("goquery get:", err) | |
return | |
} | |
doc.Find(".discussion-list .discussion-list-item").Each(func(num int, | |
thread *goquery.Selection) { | |
applicationID, _ := thread.Find(".riot-apollo").Attr("data-apollo-application-id") | |
discussionID, _ := thread.Find(".riot-apollo").Attr("data-apollo-discussion-id") | |
page, _ := thread.Find(".footer .total-comments-count").Attr("href") | |
numComments, err := strconv.Atoi(strings.Split( | |
thread.Find(".footer .total-comments-count").Text(), " ")[0]) | |
if err != nil { | |
panic(err) | |
} | |
threadStatesMutex.Lock() | |
state, found := threadStates[applicationID+":"+discussionID] | |
if !found { | |
threadStatesMutex.Unlock() | |
checkThread(applicationID, discussionID, page, | |
time.Now().Add(-thresholdTime)) | |
return | |
} | |
if numComments != state.numComments { | |
threadStatesMutex.Unlock() | |
checkThread(applicationID, discussionID, page, | |
state.lastComment) | |
return | |
} | |
threadStatesMutex.Unlock() | |
}) | |
} | |
// truncates messages nicely so I don't go above Discord's limit. | |
func truncateMessage(message string) string { | |
message = strings.Replace(message, "\r", "", -1) | |
newMessage := "" | |
messageParts := strings.Split(message, " ") | |
for i, part := range messageParts { | |
if len(newMessage+" "+part) > maxBodyLength { | |
return newMessage + "..." | |
} | |
if i == 0 { | |
newMessage = part | |
} else { | |
newMessage = newMessage + " " + part | |
} | |
} | |
return newMessage | |
} | |
func checkThread(applicationID, discussionID, page string, laterThan time.Time) { | |
log.Println("checking thread:", developerSite+page) | |
resp, err := http.Get(apolloPath + applicationID + "/discussions/" + | |
discussionID + "?sort_type=recent&page_size=100") | |
if err != nil { | |
log.Println("failed to check thread:", page, "error:", err) | |
return | |
} | |
defer resp.Body.Close() | |
if resp.StatusCode != http.StatusOK { | |
log.Println("not OK status code:", resp.Status, "for:", page) | |
return | |
} | |
dec := json.NewDecoder(resp.Body) | |
var thread Thread | |
err = dec.Decode(&thread) | |
if err != nil { | |
log.Println("failed to decode thread:", apolloPath+applicationID+"/discussions/"+ | |
discussionID+"?sort_type=recent&page_size=100", "error:", err) | |
return | |
} | |
if thread.Discussion.CreatedAt.After(laterThan) { | |
// This thread is new | |
title, err := url.QueryUnescape(thread.Discussion.Title) | |
if err != nil { | |
log.Println("failed to unescape title:"+ | |
thread.Discussion.Title, "error:", err) | |
title = "Error getting title" | |
} | |
body, err := url.QueryUnescape(thread.Discussion.Content.Body) | |
if err != nil { | |
log.Println("failed to unescape body:"+ | |
thread.Discussion.Content.Body, "error:", err) | |
body = "Error getting body" | |
} | |
body = truncateMessage(body) | |
message := &discordgo.MessageEmbed{ | |
Title: title, | |
Description: body, | |
URL: developerSite + page, | |
Color: discussionColor, | |
Author: &discordgo.MessageEmbedAuthor{ | |
Name: thread.Discussion.User.Name + | |
" (" + thread.Discussion.User.Realm + | |
") posted a new discussion", | |
IconURL: "https://ddragon.leagueoflegends.com/cdn/" + | |
currentVersion + "/img/profileicon/" + | |
thread.Discussion.User.ProfileIcon + ".png", | |
}, | |
Timestamp: thread.Discussion.CreatedAt. | |
In(utcLocation).Format(time.RFC3339), | |
} | |
_, err = session.ChannelMessageSendEmbed(channelID, message) | |
if err != nil { | |
log.Println("error sending message:", err) | |
} | |
} | |
checkComments(page, thread, thread.Discussion.Comments.Comments, laterThan) | |
threadStatesMutex.Lock() | |
lastComment := thread.Discussion.LastCommentedAt.Time | |
if lastComment.IsZero() { | |
lastComment = thread.Discussion.CreatedAt.Time | |
} | |
threadStates[applicationID+":"+discussionID] = threadState{ | |
numComments: thread.Discussion.SoftComments, | |
lastComment: lastComment, | |
} | |
threadStatesMutex.Unlock() | |
} | |
func checkComments(page string, thread Thread, comments []Comment, laterThan time.Time) { | |
for _, comment := range comments { | |
if comment.CreatedAt.After(laterThan) { | |
title, err := url.QueryUnescape(thread.Discussion.Title) | |
if err != nil { | |
log.Println("failed to unescape title:"+ | |
thread.Discussion.Title, "error:", err) | |
title = "Error getting title" | |
} | |
message := &discordgo.MessageEmbed{ | |
Title: title, | |
Description: comment.Message, | |
URL: developerSite + page, | |
Color: commentColor, | |
Author: &discordgo.MessageEmbedAuthor{ | |
Name: comment.User.Name + | |
" (" + comment.User.Realm + | |
") posted a comment", | |
IconURL: "https://ddragon.leagueoflegends.com/cdn/" + | |
currentVersion + "/img/profileicon/" + | |
comment.User.ProfileIcon + ".png", | |
}, | |
Timestamp: comment.CreatedAt. | |
In(utcLocation).Format(time.RFC3339), | |
} | |
_, err = session.ChannelMessageSendEmbed(channelID, message) | |
if err != nil { | |
log.Println("error sending message:", err) | |
} | |
} | |
checkComments(page, thread, comment.Replies.Comments, laterThan) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment