new wipe subcommand
This commit is contained in:
parent
2c11d2102c
commit
dae7c11433
|
@ -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
217
drivel.go
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user