Browse Source

Initial version of levyraati bot

Toni Fadjukoff 7 years ago
commit
735f178f31
5 changed files with 510 additions and 0 deletions
  1. 1 0
      .gitignore
  2. 235 0
      bot.go
  3. 224 0
      mediawiki.go
  4. 47 0
      songs.go
  5. 3 0
      songs.json

+ 1 - 0
.gitignore View File

@@ -0,0 +1 @@
1
+credentials.json

+ 235 - 0
bot.go View File

@@ -0,0 +1,235 @@
1
+package main
2
+
3
+import (
4
+	"container/heap"
5
+	"encoding/json"
6
+	"errors"
7
+	"fmt"
8
+	"github.com/antonholmquist/jason"
9
+	"github.com/jasonlvhit/gocron"
10
+	"io"
11
+	"io/ioutil"
12
+	"log"
13
+	"os"
14
+	"regexp"
15
+	"strconv"
16
+	"strings"
17
+	"time"
18
+)
19
+
20
+var songs SongPriorityQueue
21
+
22
+var credentials struct {
23
+	APIURL   string
24
+	UserName string
25
+	Password string
26
+}
27
+
28
+func appendSong(wikiText string, author string, newSong string) string {
29
+	const AUTHOR_MARK = "<!-- Lisääjä -->"
30
+	const SONG_MARK = "<!-- Kappale -->"
31
+	lines := strings.Split(wikiText, "\n")
32
+	authorPrevIndex := -2
33
+
34
+	changedLines := make([]string, 0, len(lines))
35
+	for index, line := range lines {
36
+		if strings.Index(line, AUTHOR_MARK) != -1 && strings.Index(line, author) != -1 {
37
+			authorPrevIndex = index
38
+		}
39
+		if authorPrevIndex == (index-1) && strings.Index(line, SONG_MARK) != -1 {
40
+			changedLines = append(changedLines, "| "+SONG_MARK+" "+newSong)
41
+		} else {
42
+			changedLines = append(changedLines, line)
43
+		}
44
+	}
45
+	return strings.Join(changedLines, "\n")
46
+}
47
+
48
+func addSong(title string, week int, author string, song string) (bool, error) {
49
+	wiki := CreateWikiClient(credentials.APIURL, credentials.UserName, credentials.Password)
50
+
51
+	sections, err := wiki.GetWikiPageSections(title)
52
+	if err != nil {
53
+		return false, err
54
+	}
55
+
56
+	numberReg, _ := regexp.Compile("\\d+")
57
+	for _, section := range sections {
58
+		weekStr := numberReg.FindString(section.title)
59
+		if weekStr != "" {
60
+			weekNumber, _ := strconv.Atoi(weekStr)
61
+			if weekNumber == week {
62
+				wikiText, err := wiki.GetWikiPageSectionText(title, section.index)
63
+				if err != nil {
64
+					return false, err
65
+				}
66
+				changedWikiText := appendSong(wikiText, author, song)
67
+
68
+				return wiki.EditWikiPageSection(title, section.index, changedWikiText,
69
+					fmt.Sprintf("Added week %d song for %s", week, author))
70
+			}
71
+		}
72
+	}
73
+	return false, errors.New("Could not find matching section")
74
+}
75
+
76
+type SongsFile struct {
77
+	Songs []*struct {
78
+		Week   int
79
+		Title  string
80
+		Artist string
81
+		URL    string
82
+		Sync   bool
83
+	}
84
+}
85
+
86
+func songSynced(syncedWeek int) error {
87
+	var v SongsFile
88
+	f, err := os.Open("songs.json")
89
+	if err != nil {
90
+		return err
91
+	}
92
+	defer f.Close()
93
+
94
+	if err != nil {
95
+		log.Fatal(err)
96
+		return nil
97
+	}
98
+	dec := json.NewDecoder(f)
99
+	dec.UseNumber()
100
+	for {
101
+		if err := dec.Decode(&v); err == io.EOF {
102
+			break
103
+		} else if err != nil {
104
+			log.Fatal(err)
105
+			return err
106
+		}
107
+	}
108
+
109
+	synced := false
110
+	for _, song := range v.Songs {
111
+		if song.Week == syncedWeek {
112
+			song.Sync = true
113
+			synced = true
114
+			jsonValue, err := json.Marshal(v)
115
+			if err != nil {
116
+				log.Fatal(err)
117
+				return err
118
+			}
119
+			err = ioutil.WriteFile("songs.json", jsonValue, 0644)
120
+			return err
121
+		}
122
+	}
123
+	if !synced {
124
+		return errors.New("No week matched from JSON for synced song")
125
+	}
126
+	return errors.New("Week not found")
127
+}
128
+
129
+func getSongs() (SongPriorityQueue, error) {
130
+	f, err := os.Open("songs.json")
131
+	if err != nil {
132
+		return nil, err
133
+	}
134
+	defer f.Close()
135
+	v, err := jason.NewObjectFromReader(f)
136
+	if err != nil {
137
+		return nil, err
138
+	}
139
+	songsArr, err := v.GetObjectArray("Songs")
140
+	if err != nil {
141
+		return nil, err
142
+	}
143
+
144
+	yearStart, _ := time.Parse(time.RFC3339, "2018-01-01T00:00:00+02:00")
145
+
146
+	songs := make(SongPriorityQueue, len(songsArr))
147
+	for index, songObj := range songsArr {
148
+		title, err := songObj.GetString("Title")
149
+		if err != nil {
150
+			return nil, err
151
+		}
152
+		artist, err := songObj.GetString("Artist")
153
+		if err != nil {
154
+			return nil, err
155
+		}
156
+		url, err := songObj.GetString("URL")
157
+		if err != nil {
158
+			return nil, err
159
+		}
160
+		week64, err := songObj.GetInt64("Week")
161
+		if err != nil {
162
+			return nil, err
163
+		}
164
+		week := int(week64)
165
+		sync, _ := songObj.GetBoolean("Sync")
166
+
167
+		target := yearStart.AddDate(0, 0, (week-1)*7)
168
+		songs[index] = &Song{
169
+			time:  target,
170
+			song:  "[" + url + " " + artist + " - " + title + "]",
171
+			week:  week,
172
+			sync:  sync,
173
+			index: index,
174
+		}
175
+	}
176
+	heap.Init(&songs)
177
+	for len(songs) > 0 && songs[0].sync {
178
+		heap.Pop(&songs)
179
+	}
180
+	return songs, nil
181
+}
182
+
183
+func task() {
184
+	now := time.Now()
185
+	if len(songs) > 0 && songs[0].time.Before(now) {
186
+		fmt.Println("Time has passed for " + songs[0].song)
187
+		success, err := addSong("Levyraati 2018", songs[0].week, "Lamperi", songs[0].song)
188
+		if err != nil {
189
+			log.Fatal(err)
190
+		}
191
+		if success {
192
+			err := songSynced(songs[0].week)
193
+			if err == nil {
194
+				heap.Pop(&songs)
195
+			} else {
196
+				fmt.Println("Error received:", err)
197
+			}
198
+		}
199
+	}
200
+}
201
+
202
+func initCreds() error {
203
+	f, err := os.Open("credentials.json")
204
+	if err != nil {
205
+		return err
206
+	}
207
+	defer f.Close()
208
+
209
+	if err != nil {
210
+		log.Fatal(err)
211
+		return err
212
+	}
213
+	dec := json.NewDecoder(f)
214
+	for {
215
+		if err := dec.Decode(&credentials); err == io.EOF {
216
+			break
217
+		} else if err != nil {
218
+			log.Fatal(err)
219
+		}
220
+	}
221
+	return nil
222
+}
223
+
224
+func main() {
225
+	err := initCreds()
226
+	if err != nil {
227
+		panic(err)
228
+	}
229
+	songs, err = getSongs()
230
+	if err != nil {
231
+		panic(err)
232
+	}
233
+	gocron.Every(1).Second().Do(task)
234
+	<-gocron.Start()
235
+}

+ 224 - 0
mediawiki.go View File

@@ -0,0 +1,224 @@
1
+package main
2
+
3
+import (
4
+	"cgt.name/pkg/go-mwclient"
5
+	"cgt.name/pkg/go-mwclient/params"
6
+	"errors"
7
+	"fmt"
8
+	"strings"
9
+)
10
+
11
+func clientLogin(w *mwclient.Client, username, password string) error {
12
+	token, err := w.GetToken("login")
13
+	if err != nil {
14
+		return err
15
+	}
16
+	v := params.Values{
17
+		"action":     "login",
18
+		"lgname":     username,
19
+		"lgpassword": password,
20
+		"lgtoken":    token,
21
+	}
22
+	resp, err := w.Post(v)
23
+	if err != nil {
24
+		if strings.Index(err.Error(), "see action=clientlogin.") == -1 {
25
+			return err
26
+		}
27
+	}
28
+	lgResult, err := resp.GetString("login", "result")
29
+	if err != nil {
30
+		return fmt.Errorf("invalid API response: unable to assert login result to string")
31
+	}
32
+	if lgResult != "Success" {
33
+		apierr := mwclient.APIError{Code: lgResult}
34
+		if reason, err := resp.GetString("login", "reason"); err == nil {
35
+			apierr.Info = reason
36
+		}
37
+		return apierr
38
+	}
39
+	return nil
40
+}
41
+
42
+type WikiClient struct {
43
+	apiUrl   string
44
+	userName string
45
+	password string
46
+}
47
+
48
+func CreateWikiClient(apiUrl string, userName string, password string) *WikiClient {
49
+	return &WikiClient{apiUrl, userName, password}
50
+}
51
+
52
+func (wc *WikiClient) GetWikiPageContent(title string) (content string, err error) {
53
+	w, err := mwclient.New(wc.apiUrl, "lamperiWikibot")
54
+	if err != nil {
55
+		return "", err
56
+	}
57
+	// Log in.
58
+	err = clientLogin(w, wc.userName, wc.password)
59
+	if err != nil {
60
+		return "", err
61
+	}
62
+
63
+	// Specify parameters to send.
64
+	parameters := map[string]string{
65
+		"action": "query",
66
+		"format": "json",
67
+		"titles": title,
68
+		"prop":   "revisions",
69
+		"rvprop": "content",
70
+	}
71
+
72
+	// Make the request.
73
+	resp, err := w.Get(parameters)
74
+	if err != nil {
75
+		return "", err
76
+	}
77
+
78
+	pages, err := resp.GetObjectArray("query", "pages")
79
+	if err != nil {
80
+		return "", err
81
+	}
82
+	if len(pages) == 0 {
83
+		return "", errors.New("Not pages in page JSON")
84
+	}
85
+	title, err = pages[0].GetString("title")
86
+	if err != nil {
87
+		return "", err
88
+	}
89
+	revisions, err := pages[0].GetObjectArray("revisions")
90
+	if err != nil {
91
+		return "", err
92
+	}
93
+	content, err = revisions[0].GetString("content")
94
+	if err != nil {
95
+		return "", err
96
+	}
97
+
98
+	return content, nil
99
+}
100
+
101
+func (wc *WikiClient) GetWikiPageSections(title string) ([]struct {
102
+	title string
103
+	index string
104
+}, error) {
105
+	w, err := mwclient.New(wc.apiUrl, "lamperiWikibot")
106
+	if err != nil {
107
+		return nil, err
108
+	}
109
+	// Log in.
110
+	err = clientLogin(w, wc.userName, wc.password)
111
+	if err != nil {
112
+		return nil, err
113
+	}
114
+
115
+	// Specify parameters to send.
116
+	parameters := map[string]string{
117
+		"action": "parse",
118
+		"format": "json",
119
+		"page":   title,
120
+		"prop":   "sections",
121
+	}
122
+
123
+	// Make the request.
124
+	resp, err := w.Get(parameters)
125
+	if err != nil {
126
+		return nil, err
127
+	}
128
+
129
+	sections, err := resp.GetObjectArray("parse", "sections")
130
+	if err != nil {
131
+		return nil, err
132
+	}
133
+	ret := make([]struct {
134
+		title string
135
+		index string
136
+	}, 0, len(sections))
137
+	for _, section := range sections {
138
+		title, err := section.GetString("line")
139
+		if err != nil {
140
+			return nil, err
141
+		}
142
+		index, err := section.GetString("index")
143
+		if err != nil {
144
+			return nil, err
145
+		}
146
+		ret = append(ret, struct {
147
+			title string
148
+			index string
149
+		}{title, index})
150
+	}
151
+	return ret, nil
152
+}
153
+
154
+func (wc *WikiClient) GetWikiPageSectionText(title string, sectionIndex string) (string, error) {
155
+	w, err := mwclient.New(wc.apiUrl, "lamperiWikibot")
156
+	if err != nil {
157
+		return "", err
158
+	}
159
+	// Log in.
160
+	err = clientLogin(w, wc.userName, wc.password)
161
+	if err != nil {
162
+		return "", err
163
+	}
164
+
165
+	// Specify parameters to send.
166
+	parameters := map[string]string{
167
+		"action":  "parse",
168
+		"format":  "json",
169
+		"page":    title,
170
+		"section": sectionIndex,
171
+		"prop":    "wikitext",
172
+	}
173
+
174
+	// Make the request.
175
+	resp, err := w.Get(parameters)
176
+	if err != nil {
177
+		return "", err
178
+	}
179
+
180
+	wikiText, err := resp.GetString("parse", "wikitext")
181
+	if err != nil {
182
+		return "", err
183
+	}
184
+	return wikiText, nil
185
+}
186
+
187
+func (wc *WikiClient) EditWikiPageSection(title string, sectionIndex string, content string, summary string) (bool, error) {
188
+	w, err := mwclient.New(wc.apiUrl, "lamperiWikibot")
189
+	if err != nil {
190
+		return false, err
191
+	}
192
+	// Log in.
193
+	err = clientLogin(w, wc.userName, wc.password)
194
+	if err != nil {
195
+		return false, err
196
+	}
197
+
198
+	editToken, err := w.GetToken("csrf")
199
+
200
+	// Specify parameters to send.
201
+	parameters := map[string]string{
202
+		"action":   "edit",
203
+		"title":    title,
204
+		"section":  sectionIndex,
205
+		"summary":  summary,
206
+		"text":     content,
207
+		"nocreate": "true",
208
+		"bot":      "true",
209
+		"token":    editToken,
210
+	}
211
+
212
+	// Make the request.
213
+	resp, err := w.Post(parameters)
214
+	if err != nil {
215
+		return false, err
216
+	}
217
+
218
+	result, err := resp.GetString("edit", "result")
219
+	if err != nil {
220
+		return false, err
221
+	}
222
+
223
+	return result == "Success", nil
224
+}

+ 47 - 0
songs.go View File

@@ -0,0 +1,47 @@
1
+package main
2
+
3
+import (
4
+	"time"
5
+)
6
+
7
+// From https://golang.org/pkg/container/heap/
8
+
9
+type Song struct {
10
+	time  time.Time
11
+	song  string
12
+	week  int
13
+	sync  bool
14
+	index int
15
+}
16
+
17
+type SongPriorityQueue []*Song
18
+
19
+func (pq SongPriorityQueue) Len() int {
20
+	return len(pq)
21
+}
22
+
23
+func (pq SongPriorityQueue) Less(i, j int) bool {
24
+	return pq[i].time.Before(pq[j].time)
25
+
26
+}
27
+func (pq SongPriorityQueue) Swap(i, j int) {
28
+	pq[i], pq[j] = pq[j], pq[i]
29
+	pq[i].index = i
30
+	pq[j].index = j
31
+}
32
+
33
+func (pq *SongPriorityQueue) Push(x interface{}) {
34
+	n := len(*pq)
35
+	item := x.(*Song)
36
+	item.index = n
37
+	*pq = append(*pq, item)
38
+}
39
+
40
+func (pq *SongPriorityQueue) Pop() interface{} {
41
+	old := *pq
42
+	n := len(old)
43
+	item := old[n-1]
44
+	item.index = -1 // for safety
45
+	*pq = old[0 : n-1]
46
+	return item
47
+}

+ 3 - 0
songs.json View File

@@ -0,0 +1,3 @@
1
+{"Songs":[{"Week":1,"Title":"Party (papiidipaadi)","Artist":"Antti Tuisku feat. Nikke Ankara","URL":"https://open.spotify.com/album/4T5cveHFQ5qZkRfZAJPcP7?si=ll_ktjrpQeW-U9KBOAcgvQ","Sync":true},
2
+    {"Week":2,"Title":"Poro","Artist":"Eläkeläiset","URL":"https://open.spotify.com/track/7BGxxc7n5rGkFhiV50pVC6","Sync":true},
3
+    {"Week":26,"Title":"Summer of '69","Artist":"Bryan Adams","URL":"https://open.spotify.com/track/3bYCGWnZdLQjndiKogqA3G","Sync":false}]}