commit a22502f299c32a193c56f4fa49378fdf80db280c Author: overflowerror Date: Mon Nov 22 17:51:03 2021 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c14c8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +config.json +access.json + +.idea diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..1fb4f98 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "log" + . "randomarticle/internal/config" + "randomarticle/internal/twitter" + "randomarticle/internal/wikipedia" + "time" +) + +const configPath = "config.json" +const accessConfigPath = "access.json" + +func getAccessConfig(config Config) AccessConfig { + accessConfig, err := ReadAccessConfig(accessConfigPath) + if err == nil { + return accessConfig + } + + log.Println("no access config found") + + accessConfig, err = twitter.Login(config) + if err != nil { + log.Fatalln(err) + } + + err = WriteAccessConfig(accessConfigPath, accessConfig) + if err != nil { + log.Fatalln(err) + } + + accessConfig, err = ReadAccessConfig(accessConfigPath) + if err != nil { + log.Fatalln(err) + } + + return accessConfig +} + +func main() { + config, err := ReadConfig(configPath) + if err != nil { + log.Fatalln(err) + } + + access := getAccessConfig(config) + twitter.Init(config, access) + + for range time.Tick(time.Minute * 1) { + log.Println("tick") + + page, err := wikipedia.Get() + if err != nil { + log.Println(err) + continue + } + + err = twitter.Tweet(wikipedia.Format(page)) + if err != nil { + log.Println(err) + continue + } + } +} \ No newline at end of file diff --git a/config.json.templ b/config.json.templ new file mode 100644 index 0000000..8b5f25d --- /dev/null +++ b/config.json.templ @@ -0,0 +1,4 @@ +{ + "consumer_key": "", + "consumer_secret": "" +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..caf6e7c --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module randomarticle + +go 1.16 + +require ( + github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb + github.com/dghubble/oauth1 v0.7.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dec552f --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= +github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb h1:7ENzkH+O3juL+yj2undESLTaAeRllHwCs/b8z6aWSfc= +github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb/go.mod h1:qhZBgV9e4WyB1JNjHpcXVkUe3knWUwYuAPB1hITdm50= +github.com/dghubble/oauth1 v0.7.0 h1:AlpZdbRiJM4XGHIlQ8BuJ/wlpGwFEJNnB4Mc+78tA/w= +github.com/dghubble/oauth1 v0.7.0/go.mod h1:8pFdfPkv/jr8mkChVbNVuJ0suiHe278BtWI4Tk1ujxk= +github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY= +github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oNzkMoM8= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..2fea9fb --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,55 @@ +package config + +import ( + "encoding/json" + "io/ioutil" +) + +type Config struct { + ConsumerKey string `json:"consumer_key"` + ConsumerSecret string `json:"consumer_secret"` +} + +type AccessConfig struct { + AccessToken string `json:"access_token"` + AccessSecret string `json:"access_secret"` +} + +func ReadConfig(path string) (Config, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return Config{}, err + } + + var config Config + err = json.Unmarshal(content, &config) + if err != nil { + return Config{}, err + } + + return config, nil +} + +func ReadAccessConfig(path string) (AccessConfig, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return AccessConfig{}, err + } + + var config AccessConfig + err = json.Unmarshal(content, &config) + if err != nil { + return AccessConfig{}, err + } + + return config, nil +} + +func WriteAccessConfig(path string, config AccessConfig) error { + content, err := json.Marshal(config) + if err != nil { + return err + } + + return ioutil.WriteFile(path, content, 0660) +} \ No newline at end of file diff --git a/internal/twitter/init.go b/internal/twitter/init.go new file mode 100644 index 0000000..4e7a10c --- /dev/null +++ b/internal/twitter/init.go @@ -0,0 +1,17 @@ +package twitter + +import ( + "github.com/dghubble/go-twitter/twitter" + appConfig "randomarticle/internal/config" +) +import "github.com/dghubble/oauth1" + +var client *twitter.Client + +func Init(appConfig appConfig.Config, access appConfig.AccessConfig) { + config := oauth1.NewConfig(appConfig.ConsumerKey, appConfig.ConsumerSecret) + token := oauth1.NewToken(access.AccessToken, access.AccessSecret) + httpClient := config.Client(oauth1.NoContext, token) + + client = twitter.NewClient(httpClient) +} \ No newline at end of file diff --git a/internal/twitter/login.go b/internal/twitter/login.go new file mode 100644 index 0000000..d1a6c3a --- /dev/null +++ b/internal/twitter/login.go @@ -0,0 +1,47 @@ +package twitter + +import ( + "fmt" + "github.com/dghubble/oauth1" + twauth "github.com/dghubble/oauth1/twitter" + . "randomarticle/internal/config" +) + +const outOfBand = "oob" + +func Login(config Config) (AccessConfig, error) { + oauthConfig := oauth1.Config{ + ConsumerKey: config.ConsumerKey, + ConsumerSecret: config.ConsumerSecret, + CallbackURL: outOfBand, + Endpoint: twauth.AuthorizeEndpoint, + } + + requestToken, _, err := oauthConfig.RequestToken() + if err != nil { + return AccessConfig{}, fmt.Errorf("could not get request token: %w", err) + } + + authorizationURL, err := oauthConfig.AuthorizationURL(requestToken) + if err != nil { + return AccessConfig{}, fmt.Errorf("could not create authorization url: %w", err) + } + fmt.Printf("Open this URL in your browser:\n%s\n", authorizationURL.String()) + + fmt.Printf("Paste your PIN here: ") + var verifier string + _, err = fmt.Scanf("%s", &verifier) + if err != nil { + return AccessConfig{}, err + } + + accessToken, accessSecret, err := oauthConfig.AccessToken(requestToken, "secret does not matter", verifier) + if err != nil { + return AccessConfig{}, err + } + + return AccessConfig{ + AccessToken: accessToken, + AccessSecret: accessSecret, + }, nil +} \ No newline at end of file diff --git a/internal/twitter/tweet.go b/internal/twitter/tweet.go new file mode 100644 index 0000000..44b322e --- /dev/null +++ b/internal/twitter/tweet.go @@ -0,0 +1,6 @@ +package twitter + +func Tweet(content string) error { + _, _, err := client.Statuses.Update(content, nil) + return err +} diff --git a/internal/wikipedia/api.go b/internal/wikipedia/api.go new file mode 100644 index 0000000..d4adf69 --- /dev/null +++ b/internal/wikipedia/api.go @@ -0,0 +1,133 @@ +package wikipedia + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "strconv" + "strings" +) + +type PageInfo struct { + Title string + Description string + URL string +} + +type infoResponse struct { + Query struct { + Pages map[string]struct{ + Title string `json:"title"` + FullURL string `json:"fullurl"` + Terms struct { + Description []string `json:"description"` + } `json:"terms"` + } `json:"pages"` + } `json:"query"` +} + +type randomReponse struct { + Query struct { + Random []struct { + ID int64 `json:"id"` + } `json:"random"` + } `json:"query"` +} + +const baseURL = "https://de.wikipedia.org" + +var noDescription = errors.New("no description found") +var noPageInfo = errors.New("no page info found") + +func request(params map[string]string) ([]byte, error) { + builder := strings.Builder{} + builder.WriteString(baseURL) + builder.WriteString("/w/api.php?") + + for key, value := range params { + builder.WriteString(key) + builder.WriteString("=") + builder.WriteString(value) + builder.WriteString("&") + } + + builder.WriteString("format=json") + + response, err := http.Get(builder.String()) + if err != nil { + return nil, err + } + + defer response.Body.Close() + + return ioutil.ReadAll(response.Body) +} + +func responseToPageInfo(response infoResponse) (PageInfo, error) { + for _, page := range response.Query.Pages { + if len(page.Terms.Description) < 1 { + return PageInfo{}, noDescription + } + return PageInfo{ + Title: page.Title, + Description: page.Terms.Description[0], + URL: page.FullURL, + }, nil + } + + return PageInfo{}, noPageInfo +} + +func queryInfo(id int64) (PageInfo, error) { + params := map[string]string { + "action": "query", + "pageids": strconv.FormatInt(id, 10), + "prop": "info|pageterms", + "inprop": "url", + } + + content, err := request(params) + if err != nil { + return PageInfo{}, err + } + + var response infoResponse + err = json.Unmarshal(content, &response) + if err != nil { + return PageInfo{}, err + } + + return responseToPageInfo(response) +} + +func queryRandom() (int64, error) { + params := map[string]string { + "action": "query", + "list": "random", + "rnnamespace": "0", + } + + content, err := request(params) + if err != nil { + return 0, err + } + + var response randomReponse + err = json.Unmarshal(content, &response) + if err != nil { + return 0, err + } + + if len(response.Query.Random) < 1 { + return 0, errors.New("could not get random result") + } + + id := response.Query.Random[0].ID + + if id == 0 { + return 0, errors.New("no page id in result") + } + + return id, nil +} diff --git a/internal/wikipedia/wikipedia.go b/internal/wikipedia/wikipedia.go new file mode 100644 index 0000000..80db7c8 --- /dev/null +++ b/internal/wikipedia/wikipedia.go @@ -0,0 +1,53 @@ +package wikipedia + +import ( + "errors" + "fmt" + "strings" +) + +const retries = 5 + +var illegalDescriptionParts = []string{ + "Begriffsklärungsseite", +} + +func Get() (PageInfo, error){ + retryLoop: + for i := 0; i < retries; i++ { + id, err := queryRandom() + if err != nil { + return PageInfo{}, err + } + + info, err := queryInfo(id) + + if err != nil { + fmt.Println(err) + fmt.Println("Retrying...") + fmt.Println() + continue + } + + for _, part := range illegalDescriptionParts { + if strings.Contains(info.Description, part) { + fmt.Println("illegal description: " + info.Description) + i-- // illegal descriptions don't count towards retry limit + continue retryLoop + } + } + + return info, err + } + return PageInfo{}, errors.New("retries exceeded") +} + +func Format(info PageInfo) string { + var builder strings.Builder + builder.WriteString(info.Title) + builder.WriteString(":\n") + builder.WriteString(info.Description) + builder.WriteString("\n\n") + builder.WriteString(info.URL) + return builder.String() +} \ No newline at end of file