mirror of
https://github.com/sigmasternchen/wikitil
synced 2025-03-15 08:09:00 +00:00
initial commit
This commit is contained in:
commit
a22502f299
11 changed files with 415 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
config.json
|
||||||
|
access.json
|
||||||
|
|
||||||
|
.idea
|
64
cmd/main.go
Normal file
64
cmd/main.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
config.json.templ
Normal file
4
config.json.templ
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"consumer_key": "",
|
||||||
|
"consumer_secret": ""
|
||||||
|
}
|
8
go.mod
Normal file
8
go.mod
Normal file
|
@ -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
|
||||||
|
)
|
24
go.sum
Normal file
24
go.sum
Normal file
|
@ -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=
|
55
internal/config/config.go
Normal file
55
internal/config/config.go
Normal file
|
@ -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)
|
||||||
|
}
|
17
internal/twitter/init.go
Normal file
17
internal/twitter/init.go
Normal file
|
@ -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)
|
||||||
|
}
|
47
internal/twitter/login.go
Normal file
47
internal/twitter/login.go
Normal file
|
@ -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
|
||||||
|
}
|
6
internal/twitter/tweet.go
Normal file
6
internal/twitter/tweet.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package twitter
|
||||||
|
|
||||||
|
func Tweet(content string) error {
|
||||||
|
_, _, err := client.Statuses.Update(content, nil)
|
||||||
|
return err
|
||||||
|
}
|
133
internal/wikipedia/api.go
Normal file
133
internal/wikipedia/api.go
Normal file
|
@ -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
|
||||||
|
}
|
53
internal/wikipedia/wikipedia.go
Normal file
53
internal/wikipedia/wikipedia.go
Normal file
|
@ -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()
|
||||||
|
}
|
Loading…
Reference in a new issue