implement cursoring for timelines

This commit is contained in:
gutmet 2020-10-21 12:19:19 +02:00
parent f40cc15af7
commit 3cf9966ec5

View File

@ -11,6 +11,7 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"text/template" "text/template"
"time" "time"
@ -20,6 +21,8 @@ const (
CHARACTER_LIMIT = 280 CHARACTER_LIMIT = 280
WIPE_KEEP_DAYS = 10 WIPE_KEEP_DAYS = 10
STATUS_ENDPOINT = "https://api.twitter.com/1.1/statuses/update.json" STATUS_ENDPOINT = "https://api.twitter.com/1.1/statuses/update.json"
MAX_TIMELINE_REQUESTS = 15
DEFAULT_COUNT = 200
MENTIONS_ENDPOINT = "https://api.twitter.com/1.1/statuses/mentions_timeline.json?tweet_mode=extended&count=200" MENTIONS_ENDPOINT = "https://api.twitter.com/1.1/statuses/mentions_timeline.json?tweet_mode=extended&count=200"
HOME_ENDPOINT = "https://api.twitter.com/1.1/statuses/home_timeline.json?tweet_mode=extended&count=200" HOME_ENDPOINT = "https://api.twitter.com/1.1/statuses/home_timeline.json?tweet_mode=extended&count=200"
TIMELINE_ENDPOINT = "https://api.twitter.com/1.1/statuses/user_timeline.json?tweet_mode=extended&count=200" TIMELINE_ENDPOINT = "https://api.twitter.com/1.1/statuses/user_timeline.json?tweet_mode=extended&count=200"
@ -33,8 +36,7 @@ const (
func optLogFatal(decorum string, err error) { func optLogFatal(decorum string, err error) {
if err != nil && err.Error() != "" { if err != nil && err.Error() != "" {
fmt.Fprintln(os.Stderr, "drivel: "+decorum+": "+err.Error()) panic("drivel: " + decorum + ": " + err.Error())
os.Exit(-1)
} }
} }
@ -70,9 +72,7 @@ func _send(url string, vals url.Values, usePost bool) []byte {
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode > 299 { if resp.StatusCode < 200 || resp.StatusCode > 299 {
fmt.Fprintln(os.Stderr, "response:", resp, "\n") log(errors.New(fmt.Sprintf("HTTP status %d\n\nresponse: %v\n\nbody: %s", resp.StatusCode, resp, string(body))))
fmt.Fprintln(os.Stderr, "body:", string(body), "\n")
log(errors.New("HTTP status " + fmt.Sprint(resp.StatusCode)))
} }
log(err) log(err)
return body return body
@ -289,8 +289,11 @@ func lookup(args []string) error {
return nil return nil
} }
func timeline(endpoint string) []Status { func timeline(endpoint string, maxID string) []Status {
log := func(err error) { optLogFatal("timeline", err) } log := func(err error) { optLogFatal("timeline", err) }
if maxID != "" {
endpoint += "&max_id=" + maxID
}
body := get(endpoint) body := get(endpoint)
var tweets []Status var tweets []Status
err := json.Unmarshal(body, &tweets) err := json.Unmarshal(body, &tweets)
@ -298,34 +301,86 @@ func timeline(endpoint string) []Status {
return tweets return tweets
} }
func mentions(args []string) error { func timelineLoop(endpoint string, flags timelineFlags) (tweets []Status) {
defer func() {
if r := recover(); r != nil {
fmt.Fprintln(os.Stderr, "INFO:", r)
}
}()
requestCount := 0
maxID := ""
for len(tweets) < flags.count && requestCount < flags.maxRequests {
tmp := timeline(endpoint, maxID)
if len(tmp) == 0 {
break
}
lowestSoFar, _ := strconv.Atoi(tmp[len(tmp)-1].Id_str)
maxID = strconv.Itoa(lowestSoFar - 1)
tweets = append(tweets, tmp...)
if len(tweets) > flags.count {
tweets = tweets[:flags.count]
}
requestCount++
}
return
}
type timelineFlags struct {
count int
maxRequests int
}
func timelineFlagsVars(s *flag.FlagSet, f *timelineFlags) {
s.IntVar(&f.count, "count", DEFAULT_COUNT, "try to get up to N tweets")
s.IntVar(&f.maxRequests, "max-requests", MAX_TIMELINE_REQUESTS, "try to achieve count with a maximum of N requests")
}
func mentions(flags timelineFlags, args []string) error {
checkUsage(args, 0, 0, "mentions") checkUsage(args, 0, 0, "mentions")
tweets := timeline(MENTIONS_ENDPOINT) tweets := timelineLoop(MENTIONS_ENDPOINT, flags)
PrintTweets(tweets, mentionsFilter) PrintTweets(tweets, mentionsFilter)
return nil return nil
} }
func home(args []string) error { func mentionsCommand() (goutil.CommandFlagsInit, goutil.CommandFunc) {
f := timelineFlags{}
flagsInit := func(s *flag.FlagSet) {
timelineFlagsVars(s, &f)
}
return flagsInit, func(args []string) error { return mentions(f, args) }
}
func home(flags timelineFlags, args []string) error {
checkUsage(args, 0, 0, "home") checkUsage(args, 0, 0, "home")
tweets := timeline(HOME_ENDPOINT) tweets := timelineLoop(HOME_ENDPOINT, flags)
PrintTweets(tweets, homeFilter) PrintTweets(tweets, homeFilter)
return nil return nil
} }
func homeCommand() (goutil.CommandFlagsInit, goutil.CommandFunc) {
f := timelineFlags{}
flagsInit := func(s *flag.FlagSet) {
timelineFlagsVars(s, &f)
}
return flagsInit, func(args []string) error { return home(f, args) }
}
func userTimeline(flags userTimelineFlags, args []string) error { func userTimeline(flags userTimelineFlags, args []string) error {
checkUsage(args, 1, 1, "timeline USER") checkUsage(args, 1, 1, "timeline USER")
tweets := timeline(TIMELINE_ENDPOINT + UserTimelineParameters(flags, args[0])) tweets := timelineLoop(TIMELINE_ENDPOINT+UserTimelineParameters(flags, args[0]), flags.timelineFlags)
PrintTweets(tweets, nil) PrintTweets(tweets, nil)
return nil return nil
} }
type userTimelineFlags struct { type userTimelineFlags struct {
timelineFlags
withReplies bool withReplies bool
} }
func userTimelineCommand() (goutil.CommandFlagsInit, goutil.CommandFunc) { func userTimelineCommand() (goutil.CommandFlagsInit, goutil.CommandFunc) {
f := userTimelineFlags{} f := userTimelineFlags{}
flagsInit := func(s *flag.FlagSet) { flagsInit := func(s *flag.FlagSet) {
timelineFlagsVars(s, &f.timelineFlags)
s.BoolVar(&f.withReplies, "with-replies", false, "include replies in timeline") s.BoolVar(&f.withReplies, "with-replies", false, "include replies in timeline")
} }
return flagsInit, func(args []string) error { return userTimeline(f, args) } return flagsInit, func(args []string) error { return userTimeline(f, args) }
@ -385,7 +440,8 @@ func wipeTimeline(likes bool, keepDays int) {
} }
n := 0 n := 0
now := time.Now() now := time.Now()
tweets := timeline(endpoint) maxID := ""
tweets := timeline(endpoint, maxID)
for { for {
for _, tweet := range tweets { for _, tweet := range tweets {
daysSince := now.Sub(tweet.Created_at.Time).Hours() / 24 daysSince := now.Sub(tweet.Created_at.Time).Hours() / 24
@ -401,8 +457,9 @@ func wipeTimeline(likes bool, keepDays int) {
return return
} }
} }
maxID = tweet.Id_str
} }
newTweets := timeline(endpoint) newTweets := timeline(endpoint, maxID)
if !equals(newTweets, tweets) { if !equals(newTweets, tweets) {
tweets = newTweets tweets = newTweets
} else { } else {
@ -504,12 +561,18 @@ var mentionsFilter hashset
var formatTemplate *template.Template var formatTemplate *template.Template
func main() { func main() {
defer func() {
if r := recover(); r != nil {
fmt.Fprintln(os.Stderr, r)
os.Exit(-1)
}
}()
client = getClient() client = getClient()
setFilters(appDir()) setFilters(appDir())
commands := []goutil.Command{ commands := []goutil.Command{
goutil.NewCommandWithFlags("status", wrapCommand(status), "post a status with message and/or media"), goutil.NewCommandWithFlags("status", wrapCommand(status), "post a status with message and/or media"),
goutil.NewCommandWithFlags("home", wrapCommand(home), "get your home timeline"), goutil.NewCommandWithFlags("home", wrapCommandFl(homeCommand), "get your home timeline"),
goutil.NewCommandWithFlags("mentions", wrapCommand(mentions), "get your mention timeline"), goutil.NewCommandWithFlags("mentions", wrapCommandFl(mentionsCommand), "get your mention timeline"),
goutil.NewCommandWithFlags("timeline", wrapCommandFl(userTimelineCommand), "get timeline of a specific user"), goutil.NewCommandWithFlags("timeline", wrapCommandFl(userTimelineCommand), "get timeline of a specific user"),
goutil.NewCommandWithFlags("lookup", wrapCommand(lookup), "lookup tweets with specific IDs"), goutil.NewCommandWithFlags("lookup", wrapCommand(lookup), "lookup tweets with specific IDs"),
goutil.NewCommandWithFlags("reply", wrapCommand(reply), "reply to a tweet with a specific ID"), goutil.NewCommandWithFlags("reply", wrapCommand(reply), "reply to a tweet with a specific ID"),
@ -518,9 +581,5 @@ func main() {
goutil.NewCommandWithFlags("like", wrapCommand(like), "like a tweet with a specific ID"), goutil.NewCommandWithFlags("like", wrapCommand(like), "like a tweet with a specific ID"),
goutil.NewCommandWithFlags("wipe", wipeCommand, "wipe your timeline and likes"), goutil.NewCommandWithFlags("wipe", wipeCommand, "wipe your timeline and likes"),
} }
err := goutil.Execute(commands) _ = goutil.Execute(commands)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
} }