-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
384 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
.PHONY: all build clean help version | ||
|
||
.DEFAULT_GOAL: build | ||
|
||
# | ||
# Variables | ||
# | ||
|
||
ARCHITECTURES=386 amd64 | ||
BINARY=wikiipsum | ||
PLATFORMS=darwin linux windows | ||
VERSION = $(shell \ | ||
git -C . describe --tags 2> /dev/null || \ | ||
git -C . rev-parse --short HEAD 2> /dev/null || \ | ||
echo "unknown" \ | ||
) | ||
|
||
# | ||
# Targets | ||
# | ||
|
||
clean: ## Remove binary files. | ||
rm -fv $(BINARY)* | ||
|
||
## Compile binaries for all OS. | ||
all: | ||
$(foreach GOOS, $(PLATFORMS), $(foreach GOARCH, $(ARCHITECTURES), $(shell export GOOS=$(GOOS); export GOARCH=$(GOARCH); go build -o $(BINARY)-$(VERSION)-$(GOOS)-$(GOARCH) -ldflags="-X 'main.Version=$(VERSION)'"))) | ||
|
||
build: ## Compile a binary. | ||
go build -o $(BINARY) -ldflags="-X 'main.Version=$(VERSION)'" | ||
|
||
help: ## Show help. | ||
@echo | ||
@echo ' Usage:' | ||
@echo ' make <target>' | ||
@echo | ||
@echo ' Targets:' | ||
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' | ||
@echo | ||
|
||
version: ## Print version. | ||
@echo $(VERSION) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,57 @@ | ||
# wikiipsum | ||
Like Lorem Ipsum but using Wikipedia | ||
|
||
[Lorem Ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum) generator using content from [Wikipedia](https://wikipedia.org). | ||
|
||
--- | ||
|
||
A small program written in Go to generate random text in specified language using the [Wikimedia REST API](https://www.mediawiki.org/wiki/REST_API). | ||
|
||
## Compilation | ||
|
||
You need [Go](https://golang.org). Download and install it if you don't have it yet. | ||
|
||
Clone the repository. | ||
```bash | ||
git clone git@github.com:sepetrov/wikiipsum.git . | ||
``` | ||
|
||
Navigate to the directory with the source code. | ||
```bash | ||
cd wikiipsum | ||
``` | ||
|
||
Compile a binary. | ||
```bash | ||
go build -o wikiipsum . | ||
``` | ||
|
||
## Download | ||
|
||
To download a precompiled binary visit the [releases page](https://github.com/sepetrov/wikiipsum/releases). | ||
|
||
## Usage | ||
|
||
```bash | ||
Lorem Ipsum generates text using content from Wikipedia and prints it to the standard output. | ||
For more information visit https://github.com/sepetrov/wikiipsum. | ||
|
||
Example: | ||
|
||
./wikiipsum -user-agent="admin@example.com" -lang="en" -length="500" | ||
|
||
Usage of ./wikiipsum: | ||
-lang string | ||
Language code, e.g. 'en' | ||
-length string | ||
Length of generated text, e.g. '500' for 500 bytes, '100 bytes', '100 Kb', '1.5 MB' etc. | ||
-rate float | ||
Request rate limit in req/s | ||
-user-agent string | ||
User agent header for API calls to Wikipedia. It should provide information how to contact you, e.g. admin@example.com | ||
-verbose | ||
Verbose | ||
``` | ||
|
||
## License | ||
|
||
See [LICENSE](LICENSE). |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module github.com/sepetrov/wikiipsum | ||
|
||
go 1.13 | ||
|
||
require ( | ||
github.com/cenkalti/backoff v2.2.1+incompatible | ||
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 | ||
) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= | ||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= | ||
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 h1:xQwXv67TxFo9nC1GJFyab5eq/5B590r6RlnL/G8Sz7w= | ||
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"flag" | ||
"fmt" | ||
"math" | ||
"mime" | ||
"net/http" | ||
"os" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
"github.com/cenkalti/backoff" | ||
"golang.org/x/time/rate" | ||
) | ||
|
||
var usage = `Lorem Ipsum generates text using content from Wikipedia and prints it to the standard output. | ||
For more information visit https://github.com/sepetrov/wikiipsum. | ||
Example: | ||
%s -user-agent="admin@example.com" -lang="en" -length="500" | ||
Usage of %s: | ||
` | ||
|
||
// Version is the program version. | ||
// See https://github.com/golang/go/wiki/GcToolchainTricks#including-build-information-in-the-executable. | ||
var Version string | ||
|
||
// See https://en.wikipedia.org/api/rest_v1/. | ||
func main() { | ||
var ( | ||
userAgent string | ||
lang string | ||
lengthStr string | ||
rateLimit float64 | ||
verbose bool | ||
version bool | ||
) | ||
|
||
flag.StringVar(&userAgent, "user-agent", "", "User agent header for API calls to Wikipedia. It should provide information how to contact you, e.g. admin@example.com") | ||
flag.StringVar(&lang, "lang", "", "Language code, e.g. 'en'") | ||
flag.StringVar(&lengthStr, "length", "", "Length of generated text, e.g. '500' for 500 bytes, '100 bytes', '100 Kb', '1.5 MB' etc.") | ||
flag.Float64Var(&rateLimit, "rate", 0, "Request rate limit in req/s") | ||
flag.BoolVar(&verbose, "verbose", false, "Verbose") | ||
flag.BoolVar(&version, "version", false, "Print version") | ||
|
||
flag.Usage = func() { | ||
fmt.Fprintf(flag.CommandLine.Output(), usage, os.Args[0], os.Args[0]) | ||
flag.PrintDefaults() | ||
} | ||
|
||
flag.Parse() | ||
|
||
if version { | ||
fmt.Fprintln(os.Stdout, Version) | ||
os.Exit(0) | ||
} | ||
|
||
if userAgent == "" { | ||
fmt.Println("'-user-agent' is required") | ||
os.Exit(1) | ||
} | ||
if lang == "" { | ||
fmt.Println("'-lang' is required") | ||
os.Exit(1) | ||
} | ||
length, err := str2bytes(lengthStr) | ||
if err != nil { | ||
fmt.Println("'-length' is invalid: " + err.Error()) | ||
os.Exit(1) | ||
} | ||
if rateLimit <= 0 || rateLimit > maxRateLimit { | ||
rateLimit = maxRateLimit | ||
} | ||
|
||
if verbose { | ||
fmt.Fprintf(os.Stderr, "Rate limit: %f\n", rateLimit) | ||
} | ||
|
||
wiki := wikiClient{ | ||
url: fmt.Sprintf(randomSummaryURL, lang), | ||
userAgent: userAgent, | ||
client: http.Client{Timeout: 5 * time.Second}, | ||
} | ||
|
||
ctx, cancel := context.WithCancel(context.Background()) | ||
txtch := make(chan []byte) // Channel to send random text. | ||
errch := make(chan error) // Channel to send errors. | ||
|
||
go func() { | ||
sleepch := make(chan time.Duration) // Channel to pause creating go routines. | ||
lim := rate.NewLimiter(rate.Limit(rateLimit), 1) | ||
for { | ||
|
||
// Pause when we need to back off due to errors. | ||
select { | ||
case d := <-sleepch: | ||
time.Sleep(d) | ||
default: | ||
} | ||
|
||
// Wait for the next available event so we don't exceed the rate limit. | ||
if err := lim.Wait(ctx); err != nil { | ||
errch <- err | ||
return | ||
} | ||
|
||
go func() { | ||
op := func() error { | ||
if verbose { | ||
fmt.Fprint(os.Stderr, ".") | ||
} | ||
b, err := wiki.RandomSummary(ctx) | ||
if errors.Is(err, errTooManyRequests) { | ||
errch <- err | ||
return err // Back off when we have 429 Too Many Requests response. | ||
} | ||
if err != nil { | ||
errch <- err | ||
return nil | ||
} | ||
txtch <- b | ||
return nil | ||
} | ||
notify := func(_ error, next time.Duration) { | ||
sleepch <- next | ||
} | ||
|
||
if err := backoff.RetryNotify(op, backoff.NewExponentialBackOff(), notify); err != nil { | ||
errch <- err | ||
} | ||
}() | ||
} | ||
}() | ||
|
||
sigch := make(chan os.Signal) // Channel to send OS signal to terminate this program. | ||
l := 0 | ||
for { | ||
select { | ||
case txt := <-txtch: | ||
n, _ := fmt.Fprintln(os.Stdout, string(txt)) | ||
if length > 0 { | ||
l += n | ||
if l >= length { | ||
goto End | ||
} | ||
} | ||
case err := <-errch: | ||
var ignore bool | ||
var timeoutErr interface { | ||
Timeout() bool | ||
} | ||
if errors.Is(err, errTooManyRequests) || | ||
errors.Is(err, context.Canceled) || | ||
errors.Is(err, context.DeadlineExceeded) || | ||
errors.As(err, &timeoutErr) && timeoutErr.Timeout() { | ||
ignore = true | ||
} | ||
if verbose || !ignore { | ||
fmt.Fprintln(os.Stderr, err) | ||
} | ||
case sig := <-sigch: | ||
if sig == os.Interrupt || sig == os.Kill { | ||
goto End | ||
} | ||
} | ||
} | ||
|
||
End: | ||
cancel() | ||
} | ||
|
||
// byteMap contains a map of regular expressions for conversion of strings to bytes. | ||
var byteMap = map[*regexp.Regexp]float64{ | ||
regexp.MustCompile(`^(\d+)$`): 1, | ||
regexp.MustCompile(`^(\d+)\s?(byte)$`): 1, | ||
regexp.MustCompile(`^(\d+)\s?(byte)$`): 1, | ||
regexp.MustCompile(`^(\d+)\s?(bytes)$`): 1, | ||
regexp.MustCompile(`^(\d+(\.\d+)?)\s?(Kb)$`): 1024, | ||
regexp.MustCompile(`^(\d+(\.\d+)?)\s?(MB)$`): 1024 * 1024, | ||
} | ||
|
||
// str2bytes parses s and tries to returns the corresponding size in bytes. | ||
func str2bytes(s string) (int, error) { | ||
for r, f := range byteMap { | ||
match := r.FindStringSubmatch(s) | ||
if len(match) == 0 { | ||
continue | ||
} | ||
val, err := strconv.ParseFloat(match[1], 10) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return int(math.Round(val * f)), nil | ||
} | ||
|
||
return 0, errors.New("cannot parse string") | ||
} | ||
|
||
// randomSummaryURL is the fmt.Sprintf pattern of the Wikipedia API URL | ||
// for random page summary. Use two character language code as an argument. | ||
// | ||
// fmt.Sprintf(randomSummaryURL, "en") | ||
const randomSummaryURL = "https://%s.wikipedia.org/api/rest_v1/page/random/summary" | ||
|
||
// maxRateLimit is the maximum rate limit for API calls to Wikipedia. | ||
// See https://en.wikipedia.org/api/rest_v1/. | ||
const maxRateLimit float64 = 200 | ||
|
||
type wikiClient struct { | ||
url string | ||
userAgent string | ||
client http.Client | ||
} | ||
|
||
var errTooManyRequests = errors.New("too many requests") | ||
|
||
// RandomText returns text from a random Wikipedia page. | ||
func (w *wikiClient) RandomSummary(ctx context.Context) ([]byte, error) { | ||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, w.url, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
req.Header.Add("Accept", "application/problem+json") | ||
req.Header.Add("User-Agent", w.userAgent) | ||
|
||
resp, err := w.client.Do(req) | ||
|
||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { | ||
return nil, err | ||
} | ||
var timeoutErr interface { | ||
Timeout() bool | ||
} | ||
if errors.As(err, &timeoutErr) && timeoutErr.Timeout() { | ||
return nil, err | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if resp.StatusCode == http.StatusTooManyRequests { | ||
return nil, errTooManyRequests | ||
} | ||
|
||
fail := func(format string, a ...interface{}) ([]byte, error) { | ||
a = append(a, req, resp) | ||
return nil, fmt.Errorf(format+"\n\nrequest:\n%v\n\nresponse:\n%v\n", a...) | ||
} | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return fail("response status %s", resp.Status) | ||
} | ||
|
||
if ctype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil { | ||
return nil, err | ||
} else if ctype != "application/json" { | ||
return fail("response content type %q", ctype) | ||
} | ||
|
||
var body struct{ Extract string `json:"extract"` } | ||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { | ||
return fail("%w", err) | ||
} | ||
|
||
return []byte(strings.TrimSpace(body.Extract)), nil | ||
} |