new wipe subcommand

This commit is contained in:
gutmet 2020-09-27 11:46:52 +02:00
parent 2c11d2102c
commit dae7c11433
2 changed files with 186 additions and 63 deletions

View File

@ -55,8 +55,14 @@ To lookup tweets with specific IDs:
drivel lookup TWEET_ID1 [TWEET_ID2 TWEET_ID3 ...] drivel lookup TWEET_ID1 [TWEET_ID2 TWEET_ID3 ...]
``` ```
To update your status with optional media upload: To wipe your timeline and likes (keepDays defaults to 10, can only reach back as far as the result of the timeline):
```
drivel wipe [--keepDays=N]
```
To update your status with optional media upload:
``` ```
drivel status STATUS [FILE1 FILE2 ...] drivel status STATUS [FILE1 FILE2 ...]

217
drivel.go
View File

@ -4,6 +4,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"flag"
"fmt" "fmt"
"git.gutmet.org/goutil.git" "git.gutmet.org/goutil.git"
"html" "html"
@ -21,14 +22,18 @@ const (
MAX_BYTES = 50 * 1024 * 1024 MAX_BYTES = 50 * 1024 * 1024
CHUNK_SIZE = 1024 * 1024 CHUNK_SIZE = 1024 * 1024
CHARACTER_LIMIT = 280 CHARACTER_LIMIT = 280
WIPE_KEEP_DAYS = 10
UPLOAD_ENDPOINT = "https://upload.twitter.com/1.1/media/upload.json" UPLOAD_ENDPOINT = "https://upload.twitter.com/1.1/media/upload.json"
STATUS_ENDPOINT = "https://api.twitter.com/1.1/statuses/update.json" STATUS_ENDPOINT = "https://api.twitter.com/1.1/statuses/update.json"
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"
LIKES_TIMELINE_ENDPOINT = "https://api.twitter.com/1.1/favorites/list.json?tweet_mode=extended&count=200"
LOOKUP_ENDPOINT = "https://api.twitter.com/1.1/statuses/lookup.json?tweet_mode=extended" LOOKUP_ENDPOINT = "https://api.twitter.com/1.1/statuses/lookup.json?tweet_mode=extended"
RETWEET_ENDPOINT = "https://api.twitter.com/1.1/statuses/retweet/" RETWEET_ENDPOINT = "https://api.twitter.com/1.1/statuses/retweet/"
LIKE_ENDPOINT = "https://api.twitter.com/1.1/favorites/create.json" LIKE_ENDPOINT = "https://api.twitter.com/1.1/favorites/create.json"
DESTROY_STATUS_ENDPOINT = "https://api.twitter.com/1.1/statuses/destroy/"
DESTROY_LIKE_ENDPOINT = "https://api.twitter.com/1.1/favorites/destroy.json"
) )
func optLogFatal(decorum string, err error) { func optLogFatal(decorum string, err error) {
@ -155,15 +160,15 @@ func getMimetype(file string) string {
} }
} }
func get(client *http.Client, url string) []byte { func get(url string) []byte {
return _send(client, url, nil, false) return _send(url, nil, false)
} }
func send(client *http.Client, url string, vals url.Values) []byte { func send(url string, vals url.Values) []byte {
return _send(client, url, vals, true) return _send(url, vals, true)
} }
func _send(client *http.Client, url string, vals url.Values, usePost bool) []byte { func _send(url string, vals url.Values, usePost bool) []byte {
log := func(err error) { log := func(err error) {
v, _ := json.Marshal(vals) v, _ := json.Marshal(vals)
optLogFatal("get/post "+url+" "+string(v), err) optLogFatal("get/post "+url+" "+string(v), err)
@ -187,10 +192,10 @@ func _send(client *http.Client, url string, vals url.Values, usePost bool) []byt
return body return body
} }
func initFileUpload(client *http.Client, file string, mediaData []byte) ObjectID { func initFileUpload(file string, mediaData []byte) ObjectID {
log := func(err error) { optLogFatal("initFileUpload "+file, err) } log := func(err error) { optLogFatal("initFileUpload "+file, err) }
initRequest := InitRequest(getMimetype(file), len(mediaData)) initRequest := InitRequest(getMimetype(file), len(mediaData))
body := send(client, UPLOAD_ENDPOINT, initRequest) body := send(UPLOAD_ENDPOINT, initRequest)
var initResponse InitResponse var initResponse InitResponse
err := json.Unmarshal(body, &initResponse) err := json.Unmarshal(body, &initResponse)
log(err) log(err)
@ -198,7 +203,7 @@ func initFileUpload(client *http.Client, file string, mediaData []byte) ObjectID
return ObjectID(initResponse.Media_id_string) return ObjectID(initResponse.Media_id_string)
} }
func appendFileChunks(client *http.Client, file string, media string, mediaId ObjectID) { func appendFileChunks(file string, media string, mediaId ObjectID) {
log := func(err error) { optLogFatal("appendFileChunks", err) } log := func(err error) { optLogFatal("appendFileChunks", err) }
info := func(v ...interface{}) { info := func(v ...interface{}) {
if len(media) > CHUNK_SIZE { if len(media) > CHUNK_SIZE {
@ -218,7 +223,7 @@ func appendFileChunks(client *http.Client, file string, media string, mediaId Ob
var body []byte var body []byte
for try := 0; try < 3 && !appended; try++ { for try := 0; try < 3 && !appended; try++ {
appRequest := AppendRequest(mediaId, media[start:end], i) appRequest := AppendRequest(mediaId, media[start:end], i)
body = send(client, UPLOAD_ENDPOINT, appRequest) body = send(UPLOAD_ENDPOINT, appRequest)
if string(body) == "" { if string(body) == "" {
appended = true appended = true
} }
@ -229,9 +234,9 @@ func appendFileChunks(client *http.Client, file string, media string, mediaId Ob
} }
} }
func finalizeFileUpload(client *http.Client, file string, mediaId ObjectID) int64 { func finalizeFileUpload(file string, mediaId ObjectID) int64 {
log := func(err error) { optLogFatal("finalizeFileUpload", err) } log := func(err error) { optLogFatal("finalizeFileUpload", err) }
body := send(client, UPLOAD_ENDPOINT, FinalizeRequest(mediaId)) body := send(UPLOAD_ENDPOINT, FinalizeRequest(mediaId))
var finalizeResponse FinalizeResponse var finalizeResponse FinalizeResponse
err := json.Unmarshal(body, &finalizeResponse) err := json.Unmarshal(body, &finalizeResponse)
log(err) log(err)
@ -251,12 +256,12 @@ func wait(seconds int64) {
time.Sleep(time.Duration(seconds) * time.Second) time.Sleep(time.Duration(seconds) * time.Second)
} }
func pollStatus(client *http.Client, mediaId ObjectID) { func pollStatus(mediaId ObjectID) {
log := func(err error) { optLogFatal("pollStatus "+string(mediaId), err) } log := func(err error) { optLogFatal("pollStatus "+string(mediaId), err) }
succeeded := false succeeded := false
var error TwitterError var error TwitterError
for try := 0; try < 6; try = try + 1 { for try := 0; try < 6; try = try + 1 {
body := get(client, UPLOAD_ENDPOINT+PollStatusParameters(mediaId)) body := get(UPLOAD_ENDPOINT + PollStatusParameters(mediaId))
var response PollStatusResponse var response PollStatusResponse
err := json.Unmarshal(body, &response) err := json.Unmarshal(body, &response)
log(err) log(err)
@ -282,26 +287,26 @@ func pollStatus(client *http.Client, mediaId ObjectID) {
} }
} }
func uploadFile(client *http.Client, file string) ObjectID { func uploadFile(file string) ObjectID {
log := func(err error) { optLogFatal("uploadFile "+file, err) } log := func(err error) { optLogFatal("uploadFile "+file, err) }
tmpMedia, err := ioutil.ReadFile(file) tmpMedia, err := ioutil.ReadFile(file)
log(err) log(err)
media := base64.RawURLEncoding.EncodeToString(tmpMedia) media := base64.RawURLEncoding.EncodeToString(tmpMedia)
mediaId := initFileUpload(client, file, tmpMedia) mediaId := initFileUpload(file, tmpMedia)
appendFileChunks(client, file, media, mediaId) appendFileChunks(file, media, mediaId)
seconds := finalizeFileUpload(client, file, mediaId) seconds := finalizeFileUpload(file, mediaId)
if seconds > 0 { if seconds > 0 {
wait(seconds) wait(seconds)
pollStatus(client, mediaId) pollStatus(mediaId)
} }
return mediaId return mediaId
} }
func uploadAll(client *http.Client, files []string) []ObjectID { func uploadAll(files []string) []ObjectID {
ids := []ObjectID{} ids := []ObjectID{}
for _, f := range files { for _, f := range files {
if f != "" { if f != "" {
id := uploadFile(client, f) id := uploadFile(f)
ids = append(ids, id) ids = append(ids, id)
} }
} }
@ -391,10 +396,10 @@ func splitArguments(args []string) data {
return d return d
} }
func tweet(client *http.Client, status string, mediaIDs []ObjectID, previousID ObjectID) ObjectID { func tweet(status string, mediaIDs []ObjectID, previousID ObjectID) ObjectID {
log := func(err error) { optLogFatal("tweet "+status, err) } log := func(err error) { optLogFatal("tweet "+status, err) }
request := UpdateStatusRequest(status, mediaIDs, previousID) request := UpdateStatusRequest(status, mediaIDs, previousID)
body := send(client, STATUS_ENDPOINT, request) body := send(STATUS_ENDPOINT, request)
var sr UpdateStatusResponse var sr UpdateStatusResponse
err := json.Unmarshal(body, &sr) err := json.Unmarshal(body, &sr)
log(err) log(err)
@ -424,22 +429,22 @@ func (d *data) getVideo(i int) string {
return goutil.StrSliceAt(d.videos, i) return goutil.StrSliceAt(d.videos, i)
} }
func (d *data) uploadPics(client *http.Client, from, to int) []ObjectID { func (d *data) uploadPics(from, to int) []ObjectID {
pics := goutil.StrSlice(d.pics, from, to) pics := goutil.StrSlice(d.pics, from, to)
return uploadAll(client, pics) return uploadAll(pics)
} }
func (d *data) uploadGif(client *http.Client, i int) []ObjectID { func (d *data) uploadGif(i int) []ObjectID {
gif := d.getGif(i) gif := d.getGif(i)
return uploadAll(client, []string{gif}) return uploadAll([]string{gif})
} }
func (d *data) uploadVideo(client *http.Client, i int) []ObjectID { func (d *data) uploadVideo(i int) []ObjectID {
vid := d.getVideo(i) vid := d.getVideo(i)
return uploadAll(client, []string{vid}) return uploadAll([]string{vid})
} }
func (d *data) push(client *http.Client, previous ObjectID) { func (d *data) push(previous ObjectID) {
if d == nil { if d == nil {
return return
} }
@ -454,20 +459,20 @@ func (d *data) push(client *http.Client, previous ObjectID) {
} }
from := i * 4 from := i * 4
to := (i + 1) * 4 to := (i + 1) * 4
mediaIDs = d.uploadPics(client, from, to) mediaIDs = d.uploadPics(from, to)
if len(mediaIDs) == 0 { if len(mediaIDs) == 0 {
mediaIDs = d.uploadGif(client, g) mediaIDs = d.uploadGif(g)
g++ g++
} }
if len(mediaIDs) == 0 { if len(mediaIDs) == 0 {
mediaIDs = d.uploadVideo(client, v) mediaIDs = d.uploadVideo(v)
v++ v++
} }
if len(mediaIDs) > 0 { if len(mediaIDs) > 0 {
empty = false empty = false
} }
if !empty { if !empty {
previous = tweet(client, status, mediaIDs, previous) previous = tweet(status, mediaIDs, previous)
i++ i++
} }
} }
@ -475,14 +480,13 @@ func (d *data) push(client *http.Client, previous ObjectID) {
func updateStatus(args []string, previous ObjectID, embedTweet ObjectID) { func updateStatus(args []string, previous ObjectID, embedTweet ObjectID) {
d := splitArguments(args) d := splitArguments(args)
httpClient := getClient()
if embedTweet != "" { if embedTweet != "" {
tweets := _lookup(httpClient, []string{string(embedTweet)}) tweets := _lookup([]string{string(embedTweet)})
if len(tweets) == 1 { if len(tweets) == 1 {
d.status[0] += " " + tweets[0].URL() d.status[0] += " " + tweets[0].URL()
} }
} }
d.push(httpClient, previous) d.push(previous)
} }
func status(args []string) error { func status(args []string) error {
@ -508,9 +512,21 @@ func quote(args []string) error {
return nil return nil
} }
type TwitterTime struct {
time.Time
}
func (twt *TwitterTime) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), "\"")
var err error
twt.Time, err = time.Parse(time.RubyDate, s)
return err
}
type Status struct { type Status struct {
Full_text string Full_text string
Id_str string Id_str string
Created_at TwitterTime
In_reply_to_screen_name string In_reply_to_screen_name string
In_reply_to_status_id_str string In_reply_to_status_id_str string
User StatusUser User StatusUser
@ -519,6 +535,10 @@ type Status struct {
Extended_entities Entities Extended_entities Entities
} }
func (t Status) equals(t2 Status) bool {
return t.Id_str == t2.Id_str
}
type Entities struct { type Entities struct {
Media []Media Media []Media
} }
@ -573,9 +593,9 @@ type StatusUser struct {
Screen_name string Screen_name string
} }
func _lookup(client *http.Client, ids []string) []Status { func _lookup(ids []string) []Status {
log := func(err error) { optLogFatal("lookup "+strings.Join(ids, ","), err) } log := func(err error) { optLogFatal("lookup "+strings.Join(ids, ","), err) }
body := get(client, LOOKUP_ENDPOINT+LookupParameters(ids)) body := get(LOOKUP_ENDPOINT + LookupParameters(ids))
var tweets []Status var tweets []Status
err := json.Unmarshal(body, &tweets) err := json.Unmarshal(body, &tweets)
log(err) log(err)
@ -587,28 +607,30 @@ func lookup(args []string) error {
fmt.Fprintln(os.Stderr, "USAGE: drivel lookup TWEET_ID1 [TWEET_ID2 TWEET_ID3 ...]") fmt.Fprintln(os.Stderr, "USAGE: drivel lookup TWEET_ID1 [TWEET_ID2 TWEET_ID3 ...]")
os.Exit(-1) os.Exit(-1)
} }
tweets := _lookup(getClient(), args) tweets := _lookup(args)
PrintTweets(tweets) PrintTweets(tweets)
return nil return nil
} }
func timeline(endpoint string) { func timeline(endpoint string, quiet bool) []Status {
log := func(err error) { optLogFatal("timeline", err) } log := func(err error) { optLogFatal("timeline", err) }
client := getClient() body := get(endpoint)
body := get(client, endpoint)
var tweets []Status var tweets []Status
err := json.Unmarshal(body, &tweets) err := json.Unmarshal(body, &tweets)
log(err) log(err)
if !quiet {
PrintTweets(tweets) PrintTweets(tweets)
}
return tweets
} }
func mentions(args []string) error { func mentions(args []string) error {
timeline(MENTIONS_ENDPOINT) timeline(MENTIONS_ENDPOINT, false)
return nil return nil
} }
func home(args []string) error { func home(args []string) error {
timeline(HOME_ENDPOINT) timeline(HOME_ENDPOINT, false)
return nil return nil
} }
@ -617,7 +639,7 @@ func UserTimelineParameters(screenName string) string {
} }
func userTimeline(args []string) error { func userTimeline(args []string) error {
timeline(TIMELINE_ENDPOINT + UserTimelineParameters(args[0])) timeline(TIMELINE_ENDPOINT+UserTimelineParameters(args[0]), false)
return nil return nil
} }
@ -631,13 +653,12 @@ func retweet(args []string) error {
fmt.Fprintln(os.Stderr, "USAGE: drivel retweet TWEET_ID") fmt.Fprintln(os.Stderr, "USAGE: drivel retweet TWEET_ID")
os.Exit(-1) os.Exit(-1)
} }
client := getClient()
id := args[0] id := args[0]
tweets := _lookup(client, []string{id}) tweets := _lookup([]string{id})
if len(tweets) != 1 { if len(tweets) != 1 {
log(errors.New("Could not find tweet " + id)) log(errors.New("Could not find tweet " + id))
} }
body := send(client, RETWEET_ENDPOINT+RetweetParameters(id), nil) body := send(RETWEET_ENDPOINT+RetweetParameters(id), nil)
var retweet Status var retweet Status
err := json.Unmarshal(body, &retweet) err := json.Unmarshal(body, &retweet)
log(err) log(err)
@ -658,8 +679,7 @@ func like(args []string) error {
fmt.Fprintln(os.Stderr, "USAGE: drivel like TWEET_ID") fmt.Fprintln(os.Stderr, "USAGE: drivel like TWEET_ID")
os.Exit(-1) os.Exit(-1)
} }
client := getClient() body := send(LIKE_ENDPOINT, LikeRequest(args[0]))
body := send(client, LIKE_ENDPOINT, LikeRequest(args[0]))
var tweet Status var tweet Status
err := json.Unmarshal(body, &tweet) err := json.Unmarshal(body, &tweet)
log(err) log(err)
@ -667,7 +687,103 @@ func like(args []string) error {
return nil return nil
} }
func equals(t1 []Status, t2 []Status) bool {
if len(t1) != len(t2) {
return false
}
for i := range t1 {
if !t1[i].equals(t2[i]) {
return false
}
}
return true
}
func UnlikeRequest(id string) url.Values {
return map[string][]string{
"id": {id},
}
}
func unlike(id string) {
log := func(err error) { optLogFatal("unlike", err) }
body := send(DESTROY_LIKE_ENDPOINT, UnlikeRequest(id))
var tweet Status
err := json.Unmarshal(body, &tweet)
log(err)
fmt.Println("Unliked", tweet.Id_str)
}
func DestroyParameters(id string) string {
return id + ".json"
}
func destroyStatus(id string) {
log := func(err error) { optLogFatal("destroy", err) }
body := send(DESTROY_STATUS_ENDPOINT+DestroyParameters(id), nil)
var tweet Status
err := json.Unmarshal(body, &tweet)
log(err)
fmt.Println("Destroyed", tweet.Id_str)
}
func wipeTimeline(likes bool, keepDays int) {
var endpoint string
if likes {
endpoint = LIKES_TIMELINE_ENDPOINT
} else {
endpoint = TIMELINE_ENDPOINT
}
n := 0
now := time.Now()
tweets := timeline(endpoint, true)
for {
for _, tweet := range tweets {
daysSince := now.Sub(tweet.Created_at.Time).Hours() / 24
if tweet.Created_at != (TwitterTime{}) && daysSince >= float64(keepDays) {
if likes {
unlike(tweet.Id_str)
} else {
destroyStatus(tweet.Id_str)
}
n++
if n >= 200 {
fmt.Println("reached limit of 200")
return
}
}
}
newTweets := timeline(endpoint, true)
if !equals(newTweets, tweets) {
tweets = newTweets
} else {
return
}
}
}
func wipe(flags wipeFlags) error {
wipeTimeline(true, flags.keepDays)
wipeTimeline(false, flags.keepDays)
return nil
}
type wipeFlags struct {
keepDays int
}
func wipeCommand() (goutil.CommandFlagsInit, goutil.CommandFunc) {
f := wipeFlags{}
flagsInit := func(s *flag.FlagSet) {
s.IntVar(&f.keepDays, "keepDays", WIPE_KEEP_DAYS, "don't wipe the last N days")
}
return flagsInit, func([]string) error { return wipe(f) }
}
var client *http.Client
func main() { func main() {
client = getClient()
commands := []goutil.Command{ commands := []goutil.Command{
goutil.NewCommand("status", status, "post a status with message and/or media"), goutil.NewCommand("status", status, "post a status with message and/or media"),
goutil.NewCommand("home", home, "get your home timeline"), goutil.NewCommand("home", home, "get your home timeline"),
@ -678,6 +794,7 @@ func main() {
goutil.NewCommand("quote", quote, "quote retweet a tweet with a specific ID"), goutil.NewCommand("quote", quote, "quote retweet a tweet with a specific ID"),
goutil.NewCommand("retweet", retweet, "retweet a tweet with a specific ID"), goutil.NewCommand("retweet", retweet, "retweet a tweet with a specific ID"),
goutil.NewCommand("like", like, "like a tweet with a specific ID"), goutil.NewCommand("like", like, "like a tweet with a specific ID"),
goutil.NewCommandWithFlags("wipe", wipeCommand, "wipe your timeline and likes"),
} }
err := goutil.Execute(commands) err := goutil.Execute(commands)
if err != nil { if err != nil {