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 ...]
```
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 ...]

217
drivel.go
View File

@ -4,6 +4,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"git.gutmet.org/goutil.git"
"html"
@ -21,14 +22,18 @@ const (
MAX_BYTES = 50 * 1024 * 1024
CHUNK_SIZE = 1024 * 1024
CHARACTER_LIMIT = 280
WIPE_KEEP_DAYS = 10
UPLOAD_ENDPOINT = "https://upload.twitter.com/1.1/media/upload.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"
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"
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"
RETWEET_ENDPOINT = "https://api.twitter.com/1.1/statuses/retweet/"
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) {
@ -155,15 +160,15 @@ func getMimetype(file string) string {
}
}
func get(client *http.Client, url string) []byte {
return _send(client, url, nil, false)
func get(url string) []byte {
return _send(url, nil, false)
}
func send(client *http.Client, url string, vals url.Values) []byte {
return _send(client, url, vals, true)
func send(url string, vals url.Values) []byte {
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) {
v, _ := json.Marshal(vals)
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
}
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) }
initRequest := InitRequest(getMimetype(file), len(mediaData))
body := send(client, UPLOAD_ENDPOINT, initRequest)
body := send(UPLOAD_ENDPOINT, initRequest)
var initResponse InitResponse
err := json.Unmarshal(body, &initResponse)
log(err)
@ -198,7 +203,7 @@ func initFileUpload(client *http.Client, file string, mediaData []byte) ObjectID
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) }
info := func(v ...interface{}) {
if len(media) > CHUNK_SIZE {
@ -218,7 +223,7 @@ func appendFileChunks(client *http.Client, file string, media string, mediaId Ob
var body []byte
for try := 0; try < 3 && !appended; try++ {
appRequest := AppendRequest(mediaId, media[start:end], i)
body = send(client, UPLOAD_ENDPOINT, appRequest)
body = send(UPLOAD_ENDPOINT, appRequest)
if string(body) == "" {
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) }
body := send(client, UPLOAD_ENDPOINT, FinalizeRequest(mediaId))
body := send(UPLOAD_ENDPOINT, FinalizeRequest(mediaId))
var finalizeResponse FinalizeResponse
err := json.Unmarshal(body, &finalizeResponse)
log(err)
@ -251,12 +256,12 @@ func wait(seconds int64) {
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) }
succeeded := false
var error TwitterError
for try := 0; try < 6; try = try + 1 {
body := get(client, UPLOAD_ENDPOINT+PollStatusParameters(mediaId))
body := get(UPLOAD_ENDPOINT + PollStatusParameters(mediaId))
var response PollStatusResponse
err := json.Unmarshal(body, &response)
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) }
tmpMedia, err := ioutil.ReadFile(file)
log(err)
media := base64.RawURLEncoding.EncodeToString(tmpMedia)
mediaId := initFileUpload(client, file, tmpMedia)
appendFileChunks(client, file, media, mediaId)
seconds := finalizeFileUpload(client, file, mediaId)
mediaId := initFileUpload(file, tmpMedia)
appendFileChunks(file, media, mediaId)
seconds := finalizeFileUpload(file, mediaId)
if seconds > 0 {
wait(seconds)
pollStatus(client, mediaId)
pollStatus(mediaId)
}
return mediaId
}
func uploadAll(client *http.Client, files []string) []ObjectID {
func uploadAll(files []string) []ObjectID {
ids := []ObjectID{}
for _, f := range files {
if f != "" {
id := uploadFile(client, f)
id := uploadFile(f)
ids = append(ids, id)
}
}
@ -391,10 +396,10 @@ func splitArguments(args []string) data {
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) }
request := UpdateStatusRequest(status, mediaIDs, previousID)
body := send(client, STATUS_ENDPOINT, request)
body := send(STATUS_ENDPOINT, request)
var sr UpdateStatusResponse
err := json.Unmarshal(body, &sr)
log(err)
@ -424,22 +429,22 @@ func (d *data) getVideo(i int) string {
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)
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)
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)
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 {
return
}
@ -454,20 +459,20 @@ func (d *data) push(client *http.Client, previous ObjectID) {
}
from := i * 4
to := (i + 1) * 4
mediaIDs = d.uploadPics(client, from, to)
mediaIDs = d.uploadPics(from, to)
if len(mediaIDs) == 0 {
mediaIDs = d.uploadGif(client, g)
mediaIDs = d.uploadGif(g)
g++
}
if len(mediaIDs) == 0 {
mediaIDs = d.uploadVideo(client, v)
mediaIDs = d.uploadVideo(v)
v++
}
if len(mediaIDs) > 0 {
empty = false
}
if !empty {
previous = tweet(client, status, mediaIDs, previous)
previous = tweet(status, mediaIDs, previous)
i++
}
}
@ -475,14 +480,13 @@ func (d *data) push(client *http.Client, previous ObjectID) {
func updateStatus(args []string, previous ObjectID, embedTweet ObjectID) {
d := splitArguments(args)
httpClient := getClient()
if embedTweet != "" {
tweets := _lookup(httpClient, []string{string(embedTweet)})
tweets := _lookup([]string{string(embedTweet)})
if len(tweets) == 1 {
d.status[0] += " " + tweets[0].URL()
}
}
d.push(httpClient, previous)
d.push(previous)
}
func status(args []string) error {
@ -508,9 +512,21 @@ func quote(args []string) error {
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 {
Full_text string
Id_str string
Created_at TwitterTime
In_reply_to_screen_name string
In_reply_to_status_id_str string
User StatusUser
@ -519,6 +535,10 @@ type Status struct {
Extended_entities Entities
}
func (t Status) equals(t2 Status) bool {
return t.Id_str == t2.Id_str
}
type Entities struct {
Media []Media
}
@ -573,9 +593,9 @@ type StatusUser struct {
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) }
body := get(client, LOOKUP_ENDPOINT+LookupParameters(ids))
body := get(LOOKUP_ENDPOINT + LookupParameters(ids))
var tweets []Status
err := json.Unmarshal(body, &tweets)
log(err)
@ -587,28 +607,30 @@ func lookup(args []string) error {
fmt.Fprintln(os.Stderr, "USAGE: drivel lookup TWEET_ID1 [TWEET_ID2 TWEET_ID3 ...]")
os.Exit(-1)
}
tweets := _lookup(getClient(), args)
tweets := _lookup(args)
PrintTweets(tweets)
return nil
}
func timeline(endpoint string) {
func timeline(endpoint string, quiet bool) []Status {
log := func(err error) { optLogFatal("timeline", err) }
client := getClient()
body := get(client, endpoint)
body := get(endpoint)
var tweets []Status
err := json.Unmarshal(body, &tweets)
log(err)
if !quiet {
PrintTweets(tweets)
}
return tweets
}
func mentions(args []string) error {
timeline(MENTIONS_ENDPOINT)
timeline(MENTIONS_ENDPOINT, false)
return nil
}
func home(args []string) error {
timeline(HOME_ENDPOINT)
timeline(HOME_ENDPOINT, false)
return nil
}
@ -617,7 +639,7 @@ func UserTimelineParameters(screenName string) string {
}
func userTimeline(args []string) error {
timeline(TIMELINE_ENDPOINT + UserTimelineParameters(args[0]))
timeline(TIMELINE_ENDPOINT+UserTimelineParameters(args[0]), false)
return nil
}
@ -631,13 +653,12 @@ func retweet(args []string) error {
fmt.Fprintln(os.Stderr, "USAGE: drivel retweet TWEET_ID")
os.Exit(-1)
}
client := getClient()
id := args[0]
tweets := _lookup(client, []string{id})
tweets := _lookup([]string{id})
if len(tweets) != 1 {
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
err := json.Unmarshal(body, &retweet)
log(err)
@ -658,8 +679,7 @@ func like(args []string) error {
fmt.Fprintln(os.Stderr, "USAGE: drivel like TWEET_ID")
os.Exit(-1)
}
client := getClient()
body := send(client, LIKE_ENDPOINT, LikeRequest(args[0]))
body := send(LIKE_ENDPOINT, LikeRequest(args[0]))
var tweet Status
err := json.Unmarshal(body, &tweet)
log(err)
@ -667,7 +687,103 @@ func like(args []string) error {
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() {
client = getClient()
commands := []goutil.Command{
goutil.NewCommand("status", status, "post a status with message and/or media"),
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("retweet", retweet, "retweet 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)
if err != nil {