2019-01-01 19:35:22 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
2020-09-27 11:46:52 +02:00
|
|
|
"flag"
|
2019-01-01 19:35:22 +01:00
|
|
|
"fmt"
|
2020-10-11 13:14:57 +02:00
|
|
|
goutil "git.gutmet.org/goutil.git/misc"
|
2019-01-01 19:35:22 +01:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2020-10-21 12:19:19 +02:00
|
|
|
"strconv"
|
2019-01-01 19:35:22 +01:00
|
|
|
"strings"
|
2020-10-16 19:20:53 +02:00
|
|
|
"text/template"
|
2020-09-18 20:49:46 +02:00
|
|
|
"time"
|
2019-01-01 19:35:22 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-09-27 11:46:52 +02:00
|
|
|
CHARACTER_LIMIT = 280
|
2020-10-23 19:20:34 +02:00
|
|
|
ALLOWLIST_MENTIONS = "AllowlistMentions"
|
|
|
|
ALLOWLIST_HOME = "AllowlistHome"
|
2020-09-27 11:46:52 +02:00
|
|
|
WIPE_KEEP_DAYS = 10
|
|
|
|
STATUS_ENDPOINT = "https://api.twitter.com/1.1/statuses/update.json"
|
2020-10-21 12:19:19 +02:00
|
|
|
MAX_TIMELINE_REQUESTS = 15
|
|
|
|
DEFAULT_COUNT = 200
|
2020-09-27 11:46:52 +02:00
|
|
|
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"
|
2020-10-23 19:54:15 +02:00
|
|
|
FOLLOWING_ENDPOINT = "https://api.twitter.com/1.1/friends/list.json?count=200"
|
2020-09-27 11:46:52 +02:00
|
|
|
DESTROY_STATUS_ENDPOINT = "https://api.twitter.com/1.1/statuses/destroy/"
|
|
|
|
DESTROY_LIKE_ENDPOINT = "https://api.twitter.com/1.1/favorites/destroy.json"
|
2019-01-01 19:35:22 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func optLogFatal(decorum string, err error) {
|
2020-09-18 23:22:42 +02:00
|
|
|
if err != nil && err.Error() != "" {
|
2020-10-21 12:19:19 +02:00
|
|
|
panic("drivel: " + decorum + ": " + err.Error())
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-17 20:15:50 +02:00
|
|
|
func checkUsage(args []string, argcMin int, argcMax int, help string) {
|
|
|
|
argc := len(args)
|
|
|
|
if (argcMin > -1 && argc < argcMin) || (argcMax > -1 && argc > argcMax) {
|
|
|
|
fmt.Fprintf(os.Stderr, "USAGE: %s %s\n", os.Args[0], help)
|
|
|
|
os.Exit(-1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-27 11:46:52 +02:00
|
|
|
func get(url string) []byte {
|
2020-10-21 13:42:04 +02:00
|
|
|
return send(url, nil, false)
|
2020-09-17 16:56:18 +02:00
|
|
|
}
|
|
|
|
|
2020-10-21 13:42:04 +02:00
|
|
|
func post(url string, vals url.Values) []byte {
|
|
|
|
return send(url, vals, true)
|
2020-09-17 16:56:18 +02:00
|
|
|
}
|
|
|
|
|
2020-10-21 13:42:04 +02:00
|
|
|
func send(url string, vals url.Values, usePost bool) []byte {
|
2019-01-01 19:35:22 +01:00
|
|
|
log := func(err error) {
|
|
|
|
v, _ := json.Marshal(vals)
|
2020-09-18 23:22:42 +02:00
|
|
|
optLogFatal("get/post "+url+" "+string(v), err)
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
2020-09-17 16:56:18 +02:00
|
|
|
var resp *http.Response
|
|
|
|
var err error
|
|
|
|
if usePost {
|
|
|
|
resp, err = client.PostForm(url, vals)
|
|
|
|
} else {
|
|
|
|
resp, err = client.Get(url)
|
|
|
|
}
|
2019-01-01 19:35:22 +01:00
|
|
|
log(err)
|
|
|
|
defer resp.Body.Close()
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
2020-08-08 12:00:45 +02:00
|
|
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
2020-10-21 12:19:19 +02:00
|
|
|
log(errors.New(fmt.Sprintf("HTTP status %d\n\nresponse: %v\n\nbody: %s", resp.StatusCode, resp, string(body))))
|
2020-08-08 11:52:42 +02:00
|
|
|
}
|
2019-01-01 19:35:22 +01:00
|
|
|
log(err)
|
|
|
|
return body
|
|
|
|
}
|
|
|
|
|
2020-10-15 23:10:09 +02:00
|
|
|
func lastSpace(slice []rune) int {
|
|
|
|
for i := len(slice) - 1; i >= 0; i-- {
|
|
|
|
if slice[i] == ' ' {
|
|
|
|
return i
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
|
|
|
}
|
2020-10-15 23:10:09 +02:00
|
|
|
return -1
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func splitStatus(status string) []string {
|
2020-10-12 10:57:08 +02:00
|
|
|
characterLimit := CHARACTER_LIMIT
|
|
|
|
// Twitter has an insane definition of what counts as a character
|
|
|
|
// ( see https://developer.twitter.com/en/docs/counting-characters )
|
|
|
|
// - as a crude approximation, anything outside LATIN-1 halfs the limit
|
|
|
|
for _, ch := range status {
|
|
|
|
if ch > 0x10FF {
|
|
|
|
characterLimit = CHARACTER_LIMIT / 2
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
asRunes := []rune(status)
|
2019-01-01 19:35:22 +01:00
|
|
|
split := []string{}
|
2020-10-12 10:57:08 +02:00
|
|
|
for len(asRunes) != 0 {
|
|
|
|
var limit int
|
|
|
|
if len(asRunes) <= characterLimit {
|
|
|
|
limit = len(asRunes)
|
2019-01-01 19:35:22 +01:00
|
|
|
} else {
|
2020-10-15 23:10:09 +02:00
|
|
|
limit = lastSpace(asRunes[0:characterLimit])
|
|
|
|
if limit == -1 {
|
2020-10-12 10:57:08 +02:00
|
|
|
limit = characterLimit
|
|
|
|
} else {
|
2020-10-15 23:10:09 +02:00
|
|
|
limit = limit + 1
|
2020-10-12 10:57:08 +02:00
|
|
|
}
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
2020-10-12 10:57:08 +02:00
|
|
|
split = append(split, string(asRunes[0:limit]))
|
|
|
|
asRunes = asRunes[limit:]
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
|
|
|
return split
|
|
|
|
}
|
|
|
|
|
2020-09-17 16:56:18 +02:00
|
|
|
func splitArguments(args []string) data {
|
|
|
|
if len(args) < 1 {
|
2020-09-22 08:09:34 +02:00
|
|
|
fmt.Fprintln(os.Stderr, "Usage: drivel status STATUS [FILE1 FILE2 ...]")
|
2019-01-01 19:35:22 +01:00
|
|
|
os.Exit(-1)
|
|
|
|
}
|
|
|
|
d := data{}
|
2020-09-17 16:56:18 +02:00
|
|
|
d.status = splitStatus(args[0])
|
|
|
|
for _, arg := range args[1:] {
|
2019-01-01 19:35:22 +01:00
|
|
|
exitIfInvalid(arg)
|
|
|
|
switch kind(arg) {
|
|
|
|
case PIC:
|
|
|
|
d.pics = append(d.pics, arg)
|
|
|
|
case GIF:
|
|
|
|
d.gifs = append(d.gifs, arg)
|
|
|
|
case VIDEO:
|
|
|
|
d.videos = append(d.videos, arg)
|
|
|
|
default:
|
|
|
|
optLogFatal("splitArguments", errors.New("Unsupported file: "+arg))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
2020-10-15 23:10:09 +02:00
|
|
|
func tweet(status string, mediaIDs []ObjectID, previousID ObjectID) Status {
|
2019-01-01 19:35:22 +01:00
|
|
|
log := func(err error) { optLogFatal("tweet "+status, err) }
|
|
|
|
request := UpdateStatusRequest(status, mediaIDs, previousID)
|
2020-10-21 13:42:04 +02:00
|
|
|
body := post(STATUS_ENDPOINT, request)
|
2020-10-15 23:10:09 +02:00
|
|
|
var tweet Status
|
|
|
|
err := json.Unmarshal(body, &tweet)
|
2019-01-01 19:35:22 +01:00
|
|
|
log(err)
|
2020-10-15 23:10:09 +02:00
|
|
|
log(tweet)
|
|
|
|
return tweet
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type data struct {
|
|
|
|
status []string
|
|
|
|
pics []string
|
|
|
|
gifs []string
|
|
|
|
videos []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *data) getStatus(i int) string {
|
|
|
|
return goutil.StrSliceAt(d.status, i)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *data) getGif(i int) string {
|
|
|
|
return goutil.StrSliceAt(d.gifs, i)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *data) getVideo(i int) string {
|
|
|
|
return goutil.StrSliceAt(d.videos, i)
|
|
|
|
}
|
|
|
|
|
2020-09-27 11:46:52 +02:00
|
|
|
func (d *data) uploadPics(from, to int) []ObjectID {
|
2019-01-01 19:35:22 +01:00
|
|
|
pics := goutil.StrSlice(d.pics, from, to)
|
2020-09-27 11:46:52 +02:00
|
|
|
return uploadAll(pics)
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
|
|
|
|
2020-09-27 11:46:52 +02:00
|
|
|
func (d *data) uploadGif(i int) []ObjectID {
|
2019-01-01 19:35:22 +01:00
|
|
|
gif := d.getGif(i)
|
2020-09-27 11:46:52 +02:00
|
|
|
return uploadAll([]string{gif})
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
|
|
|
|
2020-09-27 11:46:52 +02:00
|
|
|
func (d *data) uploadVideo(i int) []ObjectID {
|
2019-01-01 19:35:22 +01:00
|
|
|
vid := d.getVideo(i)
|
2020-09-27 11:46:52 +02:00
|
|
|
return uploadAll([]string{vid})
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
|
|
|
|
2020-10-15 23:10:09 +02:00
|
|
|
func (d *data) push(previous ObjectID) []Status {
|
2019-01-01 19:35:22 +01:00
|
|
|
empty := false
|
|
|
|
i, g, v := 0, 0, 0
|
2020-10-15 23:10:09 +02:00
|
|
|
tweets := []Status{}
|
|
|
|
if d == nil {
|
|
|
|
return tweets
|
|
|
|
}
|
2019-01-01 19:35:22 +01:00
|
|
|
for !empty {
|
|
|
|
empty = true
|
|
|
|
status := d.getStatus(i)
|
|
|
|
mediaIDs := []ObjectID{}
|
|
|
|
if status != "" {
|
|
|
|
empty = false
|
|
|
|
}
|
|
|
|
from := i * 4
|
|
|
|
to := (i + 1) * 4
|
2020-09-27 11:46:52 +02:00
|
|
|
mediaIDs = d.uploadPics(from, to)
|
2019-01-01 19:35:22 +01:00
|
|
|
if len(mediaIDs) == 0 {
|
2020-09-27 11:46:52 +02:00
|
|
|
mediaIDs = d.uploadGif(g)
|
2019-01-01 19:35:22 +01:00
|
|
|
g++
|
|
|
|
}
|
|
|
|
if len(mediaIDs) == 0 {
|
2020-09-27 11:46:52 +02:00
|
|
|
mediaIDs = d.uploadVideo(v)
|
2019-01-01 19:35:22 +01:00
|
|
|
v++
|
|
|
|
}
|
|
|
|
if len(mediaIDs) > 0 {
|
|
|
|
empty = false
|
|
|
|
}
|
|
|
|
if !empty {
|
2020-10-15 23:10:09 +02:00
|
|
|
t := tweet(status, mediaIDs, previous)
|
|
|
|
tweets = append(tweets, t)
|
|
|
|
previous = ObjectID(t.Id_str)
|
2019-01-01 19:35:22 +01:00
|
|
|
i++
|
|
|
|
}
|
|
|
|
}
|
2020-10-15 23:10:09 +02:00
|
|
|
return tweets
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|
|
|
|
|
2020-10-15 23:10:09 +02:00
|
|
|
func updateStatus(args []string, previous ObjectID, embedTweet ObjectID) []Status {
|
2020-09-17 16:56:18 +02:00
|
|
|
d := splitArguments(args)
|
2020-09-19 22:12:59 +02:00
|
|
|
if embedTweet != "" {
|
2020-09-27 11:46:52 +02:00
|
|
|
tweets := _lookup([]string{string(embedTweet)})
|
2020-09-19 22:12:59 +02:00
|
|
|
if len(tweets) == 1 {
|
|
|
|
d.status[0] += " " + tweets[0].URL()
|
|
|
|
}
|
|
|
|
}
|
2020-10-15 23:10:09 +02:00
|
|
|
return d.push(previous)
|
|
|
|
}
|
|
|
|
|
2020-10-23 19:20:34 +02:00
|
|
|
func PrintTweets(tweets []Status) {
|
2020-10-16 19:20:53 +02:00
|
|
|
if formatTemplate != nil {
|
2020-10-23 19:20:34 +02:00
|
|
|
optLogFatal("printTweets", formatTemplate.Execute(os.Stdout, tweets))
|
2020-10-16 19:20:53 +02:00
|
|
|
} else {
|
2020-10-23 19:20:34 +02:00
|
|
|
for _, tweet := range tweets {
|
2020-10-15 23:10:09 +02:00
|
|
|
fmt.Println(tweet.String())
|
|
|
|
fmt.Println("---------")
|
|
|
|
}
|
|
|
|
}
|
2020-09-17 21:30:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func status(args []string) error {
|
2020-10-17 20:15:50 +02:00
|
|
|
checkUsage(args, 1, -1, "status STATUS [FILE1 FILE2 ...]")
|
2020-10-15 23:10:09 +02:00
|
|
|
tweets := updateStatus(args, "", "")
|
2020-10-23 19:20:34 +02:00
|
|
|
PrintTweets(tweets)
|
2020-09-17 16:56:18 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-17 21:44:57 +02:00
|
|
|
func reply(args []string) error {
|
2020-10-17 20:15:50 +02:00
|
|
|
checkUsage(args, 2, -1, "reply TWEET_ID MESSAGE [FILE1 FILE2 ...]")
|
2020-10-15 23:10:09 +02:00
|
|
|
tweets := updateStatus(args[1:], ObjectID(args[0]), "")
|
2020-10-23 19:20:34 +02:00
|
|
|
PrintTweets(tweets)
|
2020-09-19 22:12:59 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func quote(args []string) error {
|
2020-10-17 20:15:50 +02:00
|
|
|
checkUsage(args, 2, -1, "quote TWEET_ID MESSAGE [FILE1 FILE2 ...]")
|
2020-10-15 23:10:09 +02:00
|
|
|
tweets := updateStatus(args[1:], "", ObjectID(args[0]))
|
2020-10-23 19:20:34 +02:00
|
|
|
PrintTweets(tweets)
|
2020-09-17 21:44:57 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-27 11:46:52 +02:00
|
|
|
func _lookup(ids []string) []Status {
|
2020-09-19 22:12:59 +02:00
|
|
|
log := func(err error) { optLogFatal("lookup "+strings.Join(ids, ","), err) }
|
2020-09-27 11:46:52 +02:00
|
|
|
body := get(LOOKUP_ENDPOINT + LookupParameters(ids))
|
2020-09-19 22:12:59 +02:00
|
|
|
var tweets []Status
|
|
|
|
err := json.Unmarshal(body, &tweets)
|
|
|
|
log(err)
|
|
|
|
return tweets
|
2020-09-17 16:56:18 +02:00
|
|
|
}
|
|
|
|
|
2020-09-22 08:09:34 +02:00
|
|
|
func lookup(args []string) error {
|
2020-10-17 20:15:50 +02:00
|
|
|
checkUsage(args, 1, -1, "lookup TWEET_ID1 [TWEET_ID2 TWEET_ID3 ...]")
|
2020-09-27 11:46:52 +02:00
|
|
|
tweets := _lookup(args)
|
2020-10-23 19:20:34 +02:00
|
|
|
PrintTweets(tweets)
|
2020-09-22 08:09:34 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-21 12:19:19 +02:00
|
|
|
func timeline(endpoint string, maxID string) []Status {
|
2020-09-17 21:44:57 +02:00
|
|
|
log := func(err error) { optLogFatal("timeline", err) }
|
2020-10-21 12:19:19 +02:00
|
|
|
if maxID != "" {
|
|
|
|
endpoint += "&max_id=" + maxID
|
|
|
|
}
|
2020-09-27 11:46:52 +02:00
|
|
|
body := get(endpoint)
|
2020-09-17 21:44:57 +02:00
|
|
|
var tweets []Status
|
|
|
|
err := json.Unmarshal(body, &tweets)
|
2020-09-17 16:56:18 +02:00
|
|
|
log(err)
|
2020-09-27 11:46:52 +02:00
|
|
|
return tweets
|
2020-09-17 21:44:57 +02:00
|
|
|
}
|
|
|
|
|
2020-10-23 19:20:34 +02:00
|
|
|
func timelineLoop(endpoint string, flags timelineFlags, allowlist hashset) (tweets []Status) {
|
2020-10-21 12:19:19 +02:00
|
|
|
defer func() {
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, "INFO:", r)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
requestCount := 0
|
2020-10-21 12:34:14 +02:00
|
|
|
maxID := flags.maxID
|
2020-10-21 12:19:19 +02:00
|
|
|
for len(tweets) < flags.count && requestCount < flags.maxRequests {
|
|
|
|
tmp := timeline(endpoint, maxID)
|
|
|
|
if len(tmp) == 0 {
|
|
|
|
break
|
|
|
|
}
|
2020-10-21 13:51:21 +02:00
|
|
|
var lowestSoFar int64
|
|
|
|
lowestSoFar, _ = strconv.ParseInt(tmp[len(tmp)-1].Id_str, 10, 64)
|
|
|
|
maxID = strconv.FormatInt(lowestSoFar-1, 10)
|
2020-10-23 19:20:34 +02:00
|
|
|
for _, tweet := range tmp {
|
|
|
|
if allowlist == nil || allowlist.contains(tweet.User.Screen_name) {
|
|
|
|
tweets = append(tweets, tweet)
|
|
|
|
}
|
|
|
|
}
|
2020-10-21 12:19:19 +02:00
|
|
|
if len(tweets) > flags.count {
|
|
|
|
tweets = tweets[:flags.count]
|
|
|
|
}
|
|
|
|
requestCount++
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
type timelineFlags struct {
|
|
|
|
count int
|
|
|
|
maxRequests int
|
2020-10-21 12:34:14 +02:00
|
|
|
maxID string
|
2020-10-21 12:19:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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")
|
2020-10-21 12:34:14 +02:00
|
|
|
s.StringVar(&f.maxID, "max-id", "", "only get tweets with an ID lower or equal to max-id")
|
2020-10-21 12:19:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func mentions(flags timelineFlags, args []string) error {
|
2020-10-17 20:15:50 +02:00
|
|
|
checkUsage(args, 0, 0, "mentions")
|
2020-10-23 19:20:34 +02:00
|
|
|
allowlist := getHashset(ALLOWLIST_MENTIONS)
|
|
|
|
tweets := timelineLoop(MENTIONS_ENDPOINT, flags, allowlist)
|
|
|
|
PrintTweets(tweets)
|
2020-09-17 16:56:18 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-21 12:19:19 +02:00
|
|
|
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 {
|
2020-10-17 20:15:50 +02:00
|
|
|
checkUsage(args, 0, 0, "home")
|
2020-10-23 19:20:34 +02:00
|
|
|
allowlist := getHashset(ALLOWLIST_HOME)
|
|
|
|
tweets := timelineLoop(HOME_ENDPOINT, flags, allowlist)
|
|
|
|
PrintTweets(tweets)
|
2020-09-17 21:30:51 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-21 12:19:19 +02:00
|
|
|
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) }
|
|
|
|
}
|
|
|
|
|
2020-10-15 14:47:28 +02:00
|
|
|
func userTimeline(flags userTimelineFlags, args []string) error {
|
2020-10-17 20:15:50 +02:00
|
|
|
checkUsage(args, 1, 1, "timeline USER")
|
2020-10-23 19:20:34 +02:00
|
|
|
tweets := timelineLoop(TIMELINE_ENDPOINT+UserTimelineParameters(flags, args[0]), flags.timelineFlags, nil)
|
|
|
|
PrintTweets(tweets)
|
2020-09-20 20:37:12 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-15 14:47:28 +02:00
|
|
|
type userTimelineFlags struct {
|
2020-10-21 12:19:19 +02:00
|
|
|
timelineFlags
|
2020-10-15 14:47:28 +02:00
|
|
|
withReplies bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func userTimelineCommand() (goutil.CommandFlagsInit, goutil.CommandFunc) {
|
|
|
|
f := userTimelineFlags{}
|
|
|
|
flagsInit := func(s *flag.FlagSet) {
|
2020-10-21 12:19:19 +02:00
|
|
|
timelineFlagsVars(s, &f.timelineFlags)
|
2020-10-15 14:47:28 +02:00
|
|
|
s.BoolVar(&f.withReplies, "with-replies", false, "include replies in timeline")
|
|
|
|
}
|
|
|
|
return flagsInit, func(args []string) error { return userTimeline(f, args) }
|
|
|
|
}
|
|
|
|
|
2020-09-19 22:31:09 +02:00
|
|
|
func retweet(args []string) error {
|
2020-10-17 20:15:50 +02:00
|
|
|
checkUsage(args, 1, 1, "retweet TWEET_ID")
|
2020-09-19 22:31:09 +02:00
|
|
|
log := func(err error) { optLogFatal("retweet", err) }
|
|
|
|
id := args[0]
|
2020-09-27 11:46:52 +02:00
|
|
|
tweets := _lookup([]string{id})
|
2020-09-19 22:31:09 +02:00
|
|
|
if len(tweets) != 1 {
|
|
|
|
log(errors.New("Could not find tweet " + id))
|
|
|
|
}
|
2020-10-21 13:42:04 +02:00
|
|
|
body := post(RETWEET_ENDPOINT+RetweetParameters(id), nil)
|
2020-09-19 22:31:09 +02:00
|
|
|
var retweet Status
|
|
|
|
err := json.Unmarshal(body, &retweet)
|
|
|
|
log(err)
|
2020-10-23 19:20:34 +02:00
|
|
|
PrintTweets([]Status{retweet})
|
2020-09-19 22:31:09 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-18 09:18:32 +02:00
|
|
|
func like(args []string) error {
|
2020-10-17 20:15:50 +02:00
|
|
|
checkUsage(args, 1, 1, "like TWEET_ID")
|
2020-09-18 09:18:32 +02:00
|
|
|
log := func(err error) { optLogFatal("like", err) }
|
2020-10-21 13:42:04 +02:00
|
|
|
body := post(LIKE_ENDPOINT, LikeRequest(args[0]))
|
2020-09-18 09:18:32 +02:00
|
|
|
var tweet Status
|
|
|
|
err := json.Unmarshal(body, &tweet)
|
|
|
|
log(err)
|
2020-10-23 19:20:34 +02:00
|
|
|
PrintTweets([]Status{tweet})
|
2020-09-18 09:18:32 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-23 20:05:55 +02:00
|
|
|
func _following() (following []string) {
|
2020-10-23 19:54:15 +02:00
|
|
|
log := func(err error) { optLogFatal("following", err) }
|
|
|
|
defer func() {
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, r)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
var cursor int64 = -1
|
|
|
|
var err error
|
|
|
|
for cursor != 0 && err == nil {
|
|
|
|
body := get(FOLLOWING_ENDPOINT + FollowingParameters(cursor))
|
|
|
|
var response FollowingResponse
|
|
|
|
err = json.Unmarshal(body, &response)
|
|
|
|
log(err)
|
|
|
|
for _, user := range response.Users {
|
2020-10-23 20:05:55 +02:00
|
|
|
following = append(following, user.Screen_name)
|
2020-10-23 19:54:15 +02:00
|
|
|
}
|
|
|
|
cursor = response.Next_cursor
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func following(args []string) error {
|
|
|
|
checkUsage(args, 0, 0, "following")
|
2020-10-23 20:05:55 +02:00
|
|
|
users := _following()
|
|
|
|
for _, user := range users {
|
2020-10-23 19:54:15 +02:00
|
|
|
fmt.Println(user)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-27 11:46:52 +02:00
|
|
|
func unlike(id string) {
|
|
|
|
log := func(err error) { optLogFatal("unlike", err) }
|
2020-10-21 13:42:04 +02:00
|
|
|
body := post(DESTROY_LIKE_ENDPOINT, UnlikeRequest(id))
|
2020-09-27 11:46:52 +02:00
|
|
|
var tweet Status
|
|
|
|
err := json.Unmarshal(body, &tweet)
|
|
|
|
log(err)
|
|
|
|
fmt.Println("Unliked", tweet.Id_str)
|
|
|
|
}
|
|
|
|
|
|
|
|
func destroyStatus(id string) {
|
|
|
|
log := func(err error) { optLogFatal("destroy", err) }
|
2020-10-21 13:42:04 +02:00
|
|
|
body := post(DESTROY_STATUS_ENDPOINT+DestroyParameters(id), nil)
|
2020-09-27 11:46:52 +02:00
|
|
|
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()
|
2020-10-21 12:19:19 +02:00
|
|
|
maxID := ""
|
|
|
|
tweets := timeline(endpoint, maxID)
|
2020-09-27 11:46:52 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2020-10-21 12:19:19 +02:00
|
|
|
maxID = tweet.Id_str
|
2020-09-27 11:46:52 +02:00
|
|
|
}
|
2020-10-21 12:19:19 +02:00
|
|
|
newTweets := timeline(endpoint, maxID)
|
2020-09-27 11:46:52 +02:00
|
|
|
if !equals(newTweets, tweets) {
|
|
|
|
tweets = newTweets
|
|
|
|
} else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-17 20:15:50 +02:00
|
|
|
func wipe(flags wipeFlags, args []string) error {
|
|
|
|
checkUsage(args, 0, 0, "wipe")
|
2020-09-27 11:46:52 +02:00
|
|
|
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) {
|
2020-10-15 14:47:28 +02:00
|
|
|
s.IntVar(&f.keepDays, "keep-days", WIPE_KEEP_DAYS, "don't wipe the last N days")
|
2020-09-27 11:46:52 +02:00
|
|
|
}
|
2020-10-17 20:15:50 +02:00
|
|
|
return flagsInit, func(args []string) error { return wipe(f, args) }
|
2020-09-27 11:46:52 +02:00
|
|
|
}
|
|
|
|
|
2020-10-15 11:32:12 +02:00
|
|
|
type hashset map[string]interface{}
|
|
|
|
|
|
|
|
func makeHashset() hashset {
|
|
|
|
return make(map[string]interface{})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s hashset) add(member string) {
|
|
|
|
if s != nil {
|
|
|
|
s[member] = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s hashset) contains(member string) bool {
|
|
|
|
if s == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if _, ok := s[member]; ok {
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-23 19:20:34 +02:00
|
|
|
func getHashset(name string) (set hashset) {
|
|
|
|
fullpath := filepath.Join(appDir(), name)
|
|
|
|
s, err := goutil.ReadFile(fullpath)
|
|
|
|
if err == nil {
|
|
|
|
set = makeHashset()
|
|
|
|
for _, id := range strings.Split(s, "\n") {
|
|
|
|
if trimmed := strings.TrimSpace(id); trimmed != "" {
|
|
|
|
set.add(trimmed)
|
2020-10-15 11:32:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-23 19:20:34 +02:00
|
|
|
return
|
2020-10-15 11:32:12 +02:00
|
|
|
}
|
|
|
|
|
2020-10-16 19:20:53 +02:00
|
|
|
type generalFlags struct {
|
|
|
|
templateFile string
|
|
|
|
}
|
|
|
|
|
|
|
|
func wrapCommand(cmd goutil.CommandFunc) func() (goutil.CommandFlagsInit, goutil.CommandFunc) {
|
|
|
|
return wrapCommandFl(func() (goutil.CommandFlagsInit, goutil.CommandFunc) { return nil, cmd })
|
|
|
|
}
|
|
|
|
|
|
|
|
func wrapCommandFl(cmd func() (goutil.CommandFlagsInit, goutil.CommandFunc)) func() (goutil.CommandFlagsInit, goutil.CommandFunc) {
|
|
|
|
f := generalFlags{}
|
|
|
|
flagsInit := func(s *flag.FlagSet) {
|
|
|
|
s.StringVar(&f.templateFile, "template", "", "use a template file to format tweets")
|
|
|
|
}
|
|
|
|
return func() (goutil.CommandFlagsInit, goutil.CommandFunc) {
|
|
|
|
formerInit, commandFunc := cmd()
|
|
|
|
return func(s *flag.FlagSet) {
|
|
|
|
if formerInit != nil {
|
|
|
|
formerInit(s)
|
|
|
|
}
|
|
|
|
flagsInit(s)
|
|
|
|
}, func(args []string) error {
|
|
|
|
if f.templateFile != "" {
|
2020-11-25 14:49:49 +01:00
|
|
|
formatTemplate = template.Must(template.New(filepath.Base(f.templateFile)).Funcs(template.FuncMap{"replaceAll": strings.ReplaceAll}).ParseFiles(f.templateFile))
|
2020-10-16 19:20:53 +02:00
|
|
|
}
|
|
|
|
return commandFunc(args)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-27 11:46:52 +02:00
|
|
|
var client *http.Client
|
2020-10-16 19:20:53 +02:00
|
|
|
var formatTemplate *template.Template
|
2020-09-27 11:46:52 +02:00
|
|
|
|
2020-09-17 16:56:18 +02:00
|
|
|
func main() {
|
2020-10-21 12:19:19 +02:00
|
|
|
defer func() {
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, r)
|
|
|
|
os.Exit(-1)
|
|
|
|
}
|
|
|
|
}()
|
2020-09-27 11:46:52 +02:00
|
|
|
client = getClient()
|
2020-09-17 16:56:18 +02:00
|
|
|
commands := []goutil.Command{
|
2020-10-16 19:20:53 +02:00
|
|
|
goutil.NewCommandWithFlags("status", wrapCommand(status), "post a status with message and/or media"),
|
2020-10-21 12:19:19 +02:00
|
|
|
goutil.NewCommandWithFlags("home", wrapCommandFl(homeCommand), "get your home timeline"),
|
|
|
|
goutil.NewCommandWithFlags("mentions", wrapCommandFl(mentionsCommand), "get your mention timeline"),
|
2020-10-16 19:20:53 +02:00
|
|
|
goutil.NewCommandWithFlags("timeline", wrapCommandFl(userTimelineCommand), "get timeline of a specific user"),
|
|
|
|
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("quote", wrapCommand(quote), "quote retweet a tweet with a specific ID"),
|
|
|
|
goutil.NewCommandWithFlags("retweet", wrapCommand(retweet), "retweet a tweet with a specific ID"),
|
|
|
|
goutil.NewCommandWithFlags("like", wrapCommand(like), "like a tweet with a specific ID"),
|
2020-10-23 19:54:15 +02:00
|
|
|
goutil.NewCommand("following", following, "try to get the list of users you are following"),
|
2020-09-27 11:46:52 +02:00
|
|
|
goutil.NewCommandWithFlags("wipe", wipeCommand, "wipe your timeline and likes"),
|
2020-09-17 16:56:18 +02:00
|
|
|
}
|
2020-10-21 12:19:19 +02:00
|
|
|
_ = goutil.Execute(commands)
|
2019-01-01 19:35:22 +01:00
|
|
|
}
|