* group code somewhat sanely in separate files
* unify output of update endpoint requests and the rest as preparation for templates
This commit is contained in:
gutmet 2020-10-15 23:10:09 +02:00
parent 5d4f643689
commit b5d843e4a6
4 changed files with 474 additions and 431 deletions

474
drivel.go
View File

@ -1,30 +1,23 @@
package main package main
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
goutil "git.gutmet.org/goutil.git/misc" goutil "git.gutmet.org/goutil.git/misc"
"html"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
) )
const ( const (
MAX_BYTES = 50 * 1024 * 1024
CHUNK_SIZE = 1024 * 1024
CHARACTER_LIMIT = 280 CHARACTER_LIMIT = 280
WIPE_KEEP_DAYS = 10 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" 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"
@ -44,123 +37,6 @@ func optLogFatal(decorum string, err error) {
} }
} }
type ObjectID string
type TwitterError struct {
Code int64
Message string
Label string
}
type Response struct {
Errors []TwitterError
}
func (r Response) Error() string {
if len(r.Errors) == 0 {
return ""
} else {
s, _ := json.Marshal(r)
return "Response error " + string(s)
}
}
func InitRequest(mediaType string, totalBytes int) url.Values {
r := map[string][]string{
"command": {"INIT"},
"media_type": {mediaType},
"total_bytes": {strconv.Itoa(totalBytes)},
}
if mediaType == "video/mp4" {
r["media_category"] = []string{"tweet_video"}
}
return r
}
type InitResponse struct {
Response
Media_id_string string
}
func AppendRequest(mediaID ObjectID, mediaData string, segmentIndex int) url.Values {
return map[string][]string{
"command": {"APPEND"},
"media_id": {string(mediaID)},
"media_data": {mediaData},
"segment_index": {strconv.Itoa(segmentIndex)},
}
}
func FinalizeRequest(mediaID ObjectID) url.Values {
return map[string][]string{
"command": {"FINALIZE"},
"media_id": {string(mediaID)},
}
}
type FinalizeResponse struct {
Error string
Media_id_string string
Processing_info ProcessingInfo
}
type ProcessingInfo struct {
State string
Check_after_secs int64
Progress_percent int64
Error TwitterError
}
func PollStatusParameters(mediaID ObjectID) string {
return "?command=STATUS&media_id=" + string(mediaID)
}
type PollStatusResponse struct {
Processing_info ProcessingInfo
}
func UpdateStatusRequest(status string, mediaIDs []ObjectID, previousStatusID ObjectID) url.Values {
r := map[string][]string{"status": {status}}
if len(mediaIDs) > 0 {
ids := []string{}
for _, id := range mediaIDs {
ids = append(ids, string(id))
}
r["media_ids"] = []string{strings.Join(ids, ",")}
}
if len(previousStatusID) > 0 {
r["in_reply_to_status_id"] = []string{string(previousStatusID)}
r["auto_populate_reply_metadata"] = []string{"true"}
}
return r
}
type UpdateStatusResponse struct {
Response
Id_str string
}
func LookupParameters(ids []string) string {
return "&id=" + strings.Join(ids, ",")
}
var mimetype = map[string]string{
".mp4": "video/mp4",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
}
func getMimetype(file string) string {
ext := filepath.Ext(file)
if v, ok := mimetype[ext]; ok {
return v
} else {
return "application/octet-stream"
}
}
func get(url string) []byte { func get(url string) []byte {
return _send(url, nil, false) return _send(url, nil, false)
} }
@ -193,151 +69,13 @@ func _send(url string, vals url.Values, usePost bool) []byte {
return body return body
} }
func initFileUpload(file string, mediaData []byte) ObjectID { func lastSpace(slice []rune) int {
log := func(err error) { optLogFatal("initFileUpload "+file, err) } for i := len(slice) - 1; i >= 0; i-- {
initRequest := InitRequest(getMimetype(file), len(mediaData)) if slice[i] == ' ' {
body := send(UPLOAD_ENDPOINT, initRequest) return i
var initResponse InitResponse
err := json.Unmarshal(body, &initResponse)
log(err)
log(initResponse)
return ObjectID(initResponse.Media_id_string)
}
func appendFileChunks(file string, media string, mediaId ObjectID) {
log := func(err error) { optLogFatal("appendFileChunks", err) }
info := func(v ...interface{}) {
if len(media) > CHUNK_SIZE {
fmt.Println(v...)
} }
} }
info("chunk upload", file) return -1
info("total", len(media))
for i := 0; i*CHUNK_SIZE < len(media); i = i + 1 {
start := i * CHUNK_SIZE
end := (i + 1) * CHUNK_SIZE
if end > len(media) {
end = len(media)
}
info("segment", i, "start", start, "end", end)
appended := false
var body []byte
for try := 0; try < 3 && !appended; try++ {
appRequest := AppendRequest(mediaId, media[start:end], i)
body = send(UPLOAD_ENDPOINT, appRequest)
if string(body) == "" {
appended = true
}
}
if !appended {
log(errors.New(string(body)))
}
}
}
func finalizeFileUpload(file string, mediaId ObjectID) int64 {
log := func(err error) { optLogFatal("finalizeFileUpload", err) }
body := send(UPLOAD_ENDPOINT, FinalizeRequest(mediaId))
var finalizeResponse FinalizeResponse
err := json.Unmarshal(body, &finalizeResponse)
log(err)
log(errors.New(finalizeResponse.Error))
if id := ObjectID(finalizeResponse.Media_id_string); id != "" {
fmt.Println("==> Uploaded " + file + " with id " + string(id))
procInfo := finalizeResponse.Processing_info
return procInfo.Check_after_secs
} else {
log(errors.New("Could not finalize " + string(mediaId)))
return 0
}
}
func wait(seconds int64) {
fmt.Println("Waiting", seconds, "seconds")
time.Sleep(time.Duration(seconds) * time.Second)
}
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(UPLOAD_ENDPOINT + PollStatusParameters(mediaId))
var response PollStatusResponse
err := json.Unmarshal(body, &response)
log(err)
procInfo := response.Processing_info
state := procInfo.State
error = procInfo.Error
if state == "succeeded" {
succeeded = true
break
} else if state == "failed" {
break
} else {
fmt.Println("Processing progress: ", procInfo.Progress_percent, "%")
seconds := procInfo.Check_after_secs
if seconds > 10 {
seconds = 10
}
wait(seconds)
}
}
if !succeeded {
log(errors.New("File upload failed " + error.Message))
}
}
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(file, tmpMedia)
appendFileChunks(file, media, mediaId)
seconds := finalizeFileUpload(file, mediaId)
if seconds > 0 {
wait(seconds)
pollStatus(mediaId)
}
return mediaId
}
func uploadAll(files []string) []ObjectID {
ids := []ObjectID{}
for _, f := range files {
if f != "" {
id := uploadFile(f)
ids = append(ids, id)
}
}
return ids
}
type mediaKind int
const (
UNKNOWN mediaKind = iota
PIC
GIF
VIDEO
)
func kind(path string) mediaKind {
ext := filepath.Ext(path)
switch ext {
case ".jpg":
fallthrough
case ".jpeg":
fallthrough
case ".png":
return PIC
case ".gif":
return GIF
case ".mp4":
return VIDEO
}
return UNKNOWN
} }
func splitStatus(status string) []string { func splitStatus(status string) []string {
@ -358,12 +96,11 @@ func splitStatus(status string) []string {
if len(asRunes) <= characterLimit { if len(asRunes) <= characterLimit {
limit = len(asRunes) limit = len(asRunes)
} else { } else {
tmp := asRunes[0:characterLimit] limit = lastSpace(asRunes[0:characterLimit])
lastSpace := strings.LastIndex(string(tmp), " ") if limit == -1 {
if lastSpace == -1 {
limit = characterLimit limit = characterLimit
} else { } else {
limit = lastSpace + 1 limit = limit + 1
} }
} }
split = append(split, string(asRunes[0:limit])) split = append(split, string(asRunes[0:limit]))
@ -372,19 +109,6 @@ func splitStatus(status string) []string {
return split return split
} }
func exitIfInvalid(path string) {
log := func(err error) { optLogFatal("exitIfInvalid", err) }
// check existence AND readability
f, err := os.Open(path)
log(err)
defer f.Close()
tmp, err := ioutil.ReadAll(f)
log(err)
if len(tmp) > MAX_BYTES {
log(errors.New("File too big: " + path + " is bigger than maximum of " + strconv.Itoa(MAX_BYTES) + " Bytes"))
}
}
func splitArguments(args []string) data { func splitArguments(args []string) data {
if len(args) < 1 { if len(args) < 1 {
fmt.Fprintln(os.Stderr, "Usage: drivel status STATUS [FILE1 FILE2 ...]") fmt.Fprintln(os.Stderr, "Usage: drivel status STATUS [FILE1 FILE2 ...]")
@ -408,18 +132,15 @@ func splitArguments(args []string) data {
return d return d
} }
func tweet(status string, mediaIDs []ObjectID, previousID ObjectID) ObjectID { func tweet(status string, mediaIDs []ObjectID, previousID ObjectID) Status {
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(STATUS_ENDPOINT, request) body := send(STATUS_ENDPOINT, request)
var sr UpdateStatusResponse var tweet Status
err := json.Unmarshal(body, &sr) err := json.Unmarshal(body, &tweet)
log(err) log(err)
if len(sr.Errors) > 0 { log(tweet)
log(sr) return tweet
}
fmt.Println("==> Updated status to '" + status + "' with id " + sr.Id_str)
return ObjectID(sr.Id_str)
} }
type data struct { type data struct {
@ -456,12 +177,13 @@ func (d *data) uploadVideo(i int) []ObjectID {
return uploadAll([]string{vid}) return uploadAll([]string{vid})
} }
func (d *data) push(previous ObjectID) { func (d *data) push(previous ObjectID) []Status {
if d == nil {
return
}
empty := false empty := false
i, g, v := 0, 0, 0 i, g, v := 0, 0, 0
tweets := []Status{}
if d == nil {
return tweets
}
for !empty { for !empty {
empty = true empty = true
status := d.getStatus(i) status := d.getStatus(i)
@ -484,13 +206,16 @@ func (d *data) push(previous ObjectID) {
empty = false empty = false
} }
if !empty { if !empty {
previous = tweet(status, mediaIDs, previous) t := tweet(status, mediaIDs, previous)
tweets = append(tweets, t)
previous = ObjectID(t.Id_str)
i++ i++
} }
} }
return tweets
} }
func updateStatus(args []string, previous ObjectID, embedTweet ObjectID) { func updateStatus(args []string, previous ObjectID, embedTweet ObjectID) []Status {
d := splitArguments(args) d := splitArguments(args)
if embedTweet != "" { if embedTweet != "" {
tweets := _lookup([]string{string(embedTweet)}) tweets := _lookup([]string{string(embedTweet)})
@ -498,11 +223,21 @@ func updateStatus(args []string, previous ObjectID, embedTweet ObjectID) {
d.status[0] += " " + tweets[0].URL() d.status[0] += " " + tweets[0].URL()
} }
} }
d.push(previous) return d.push(previous)
}
func PrintTweets(tweets []Status, userFilter hashset) {
for _, tweet := range tweets {
if !userFilter.contains(tweet.User.Screen_name) {
fmt.Println(tweet.String())
fmt.Println("---------")
}
}
} }
func status(args []string) error { func status(args []string) error {
updateStatus(args, "", "") tweets := updateStatus(args, "", "")
PrintTweets(tweets, nil)
return nil return nil
} }
@ -511,7 +246,8 @@ func reply(args []string) error {
fmt.Fprintln(os.Stderr, "Usage: drivel reply TWEET_ID MESSAGE [FILE1 FILE2 ...]") fmt.Fprintln(os.Stderr, "Usage: drivel reply TWEET_ID MESSAGE [FILE1 FILE2 ...]")
os.Exit(-1) os.Exit(-1)
} }
updateStatus(args[1:], ObjectID(args[0]), "") tweets := updateStatus(args[1:], ObjectID(args[0]), "")
PrintTweets(tweets, nil)
return nil return nil
} }
@ -520,93 +256,11 @@ func quote(args []string) error {
fmt.Println(os.Stderr, "Usage: drivel quote TWEET_ID MESSAGE [FILE1 FILE2 ...]") fmt.Println(os.Stderr, "Usage: drivel quote TWEET_ID MESSAGE [FILE1 FILE2 ...]")
os.Exit(-1) os.Exit(-1)
} }
updateStatus(args[1:], "", ObjectID(args[0])) tweets := updateStatus(args[1:], "", ObjectID(args[0]))
PrintTweets(tweets, nil)
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 {
Full_text string
Id_str string
Created_at TwitterTime
In_reply_to_screen_name string
In_reply_to_status_id_str string
User StatusUser
Quoted_status *Status
Retweeted_status *Status
Extended_entities Entities
}
func (t Status) equals(t2 Status) bool {
return t.Id_str == t2.Id_str
}
type Entities struct {
Media []Media
}
type Media struct {
Media_url string
}
func (m Status) InReplyTo() string {
if m.In_reply_to_status_id_str != "" {
return m.In_reply_to_screen_name + " (" + m.In_reply_to_status_id_str + ")"
} else {
return ""
}
}
func (m Status) String() string {
if m.Retweeted_status != nil {
return m.User.Screen_name + " retweeted " + m.Retweeted_status.String()
}
s := m.User.Screen_name + " " + "(" + m.Id_str + ")"
if replyTo := m.InReplyTo(); replyTo != "" {
s += " in reply to " + replyTo
}
s += ":\n" + html.UnescapeString(m.Full_text)
allMedia := m.Extended_entities.Media
if len(allMedia) > 0 {
s += "\n\nMedia:"
for _, media := range allMedia {
s += " " + media.Media_url
}
}
if m.Quoted_status != nil {
s += "\n\nQuotes " + m.Quoted_status.String()
}
return s
}
func PrintTweets(tweets []Status, userFilter hashset) {
for _, tweet := range tweets {
if !userFilter.contains(tweet.User.Screen_name) {
fmt.Println(tweet)
fmt.Println("---------")
}
}
}
func (m Status) URL() string {
return "https://twitter.com/" + m.User.Screen_name + "/status/" + m.Id_str
}
type StatusUser struct {
Name string
Screen_name string
}
func _lookup(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(LOOKUP_ENDPOINT + LookupParameters(ids)) body := get(LOOKUP_ENDPOINT + LookupParameters(ids))
@ -647,15 +301,6 @@ func home(args []string) error {
return nil return nil
} }
func UserTimelineParameters(flags userTimelineFlags, screenName string) string {
s := "&screen_name=" + screenName
if flags.withReplies {
return s
} else {
return s + "&exclude_replies=true"
}
}
func userTimeline(flags userTimelineFlags, args []string) error { func userTimeline(flags userTimelineFlags, args []string) error {
tweets := timeline(TIMELINE_ENDPOINT + UserTimelineParameters(flags, args[0])) tweets := timeline(TIMELINE_ENDPOINT + UserTimelineParameters(flags, args[0]))
PrintTweets(tweets, nil) PrintTweets(tweets, nil)
@ -674,10 +319,6 @@ func userTimelineCommand() (goutil.CommandFlagsInit, goutil.CommandFunc) {
return flagsInit, func(args []string) error { return userTimeline(f, args) } return flagsInit, func(args []string) error { return userTimeline(f, args) }
} }
func RetweetParameters(id string) string {
return id + ".json"
}
func retweet(args []string) error { func retweet(args []string) error {
log := func(err error) { optLogFatal("retweet", err) } log := func(err error) { optLogFatal("retweet", err) }
if len(args) != 1 { if len(args) != 1 {
@ -693,17 +334,10 @@ func retweet(args []string) error {
var retweet Status var retweet Status
err := json.Unmarshal(body, &retweet) err := json.Unmarshal(body, &retweet)
log(err) log(err)
fmt.Println("Retweeted", tweets[0]) PrintTweets([]Status{retweet}, nil)
return nil return nil
} }
func LikeRequest(id string) url.Values {
return map[string][]string{
"id": {id},
"tweet_mode": {"extended"},
}
}
func like(args []string) error { func like(args []string) error {
log := func(err error) { optLogFatal("like", err) } log := func(err error) { optLogFatal("like", err) }
if len(args) != 1 { if len(args) != 1 {
@ -714,28 +348,10 @@ func like(args []string) error {
var tweet Status var tweet Status
err := json.Unmarshal(body, &tweet) err := json.Unmarshal(body, &tweet)
log(err) log(err)
fmt.Println("Liked", tweet) PrintTweets([]Status{tweet}, nil)
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) { func unlike(id string) {
log := func(err error) { optLogFatal("unlike", err) } log := func(err error) { optLogFatal("unlike", err) }
body := send(DESTROY_LIKE_ENDPOINT, UnlikeRequest(id)) body := send(DESTROY_LIKE_ENDPOINT, UnlikeRequest(id))
@ -745,10 +361,6 @@ func unlike(id string) {
fmt.Println("Unliked", tweet.Id_str) fmt.Println("Unliked", tweet.Id_str)
} }
func DestroyParameters(id string) string {
return id + ".json"
}
func destroyStatus(id string) { func destroyStatus(id string) {
log := func(err error) { optLogFatal("destroy", err) } log := func(err error) { optLogFatal("destroy", err) }
body := send(DESTROY_STATUS_ENDPOINT+DestroyParameters(id), nil) body := send(DESTROY_STATUS_ENDPOINT+DestroyParameters(id), nil)
@ -846,8 +458,8 @@ func setFilters(appDir string) {
} }
return set return set
} }
homeFilter = getHashset(goutil.ReadFile(path.Join(appDir, "FilterHome"))) homeFilter = getHashset(goutil.ReadFile(filepath.Join(appDir, "FilterHome")))
mentionsFilter = getHashset(goutil.ReadFile(path.Join(appDir, "FilterMentions"))) mentionsFilter = getHashset(goutil.ReadFile(filepath.Join(appDir, "FilterMentions")))
} }
var client *http.Client var client *http.Client

260
media.go Normal file
View File

@ -0,0 +1,260 @@
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strconv"
"time"
)
const (
MAX_BYTES = 50 * 1024 * 1024
CHUNK_SIZE = 1024 * 1024
UPLOAD_ENDPOINT = "https://upload.twitter.com/1.1/media/upload.json"
)
func InitRequest(mediaType string, totalBytes int) url.Values {
r := map[string][]string{
"command": {"INIT"},
"media_type": {mediaType},
"total_bytes": {strconv.Itoa(totalBytes)},
}
if mediaType == "video/mp4" {
r["media_category"] = []string{"tweet_video"}
}
return r
}
type InitResponse struct {
Errors []TwitterError
Media_id_string string
}
func (ir InitResponse) Error() string {
if len(ir.Errors) == 0 {
return ""
} else {
s, _ := json.Marshal(ir)
return "Response error " + string(s)
}
}
func AppendRequest(mediaID ObjectID, mediaData string, segmentIndex int) url.Values {
return map[string][]string{
"command": {"APPEND"},
"media_id": {string(mediaID)},
"media_data": {mediaData},
"segment_index": {strconv.Itoa(segmentIndex)},
}
}
func FinalizeRequest(mediaID ObjectID) url.Values {
return map[string][]string{
"command": {"FINALIZE"},
"media_id": {string(mediaID)},
}
}
type FinalizeResponse struct {
Error string
Media_id_string string
Processing_info ProcessingInfo
}
type ProcessingInfo struct {
State string
Check_after_secs int64
Progress_percent int64
Error TwitterError
}
func PollStatusParameters(mediaID ObjectID) string {
return "?command=STATUS&media_id=" + string(mediaID)
}
type PollStatusResponse struct {
Processing_info ProcessingInfo
}
var mimetype = map[string]string{
".mp4": "video/mp4",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
}
func getMimetype(file string) string {
ext := filepath.Ext(file)
if v, ok := mimetype[ext]; ok {
return v
} else {
return "application/octet-stream"
}
}
func initFileUpload(file string, mediaData []byte) ObjectID {
log := func(err error) { optLogFatal("initFileUpload "+file, err) }
initRequest := InitRequest(getMimetype(file), len(mediaData))
body := send(UPLOAD_ENDPOINT, initRequest)
var initResponse InitResponse
err := json.Unmarshal(body, &initResponse)
log(err)
log(initResponse)
return ObjectID(initResponse.Media_id_string)
}
func appendFileChunks(file string, media string, mediaId ObjectID) {
log := func(err error) { optLogFatal("appendFileChunks", err) }
info := func(v ...interface{}) {
if len(media) > CHUNK_SIZE {
fmt.Println(v...)
}
}
info("chunk upload", file)
info("total", len(media))
for i := 0; i*CHUNK_SIZE < len(media); i = i + 1 {
start := i * CHUNK_SIZE
end := (i + 1) * CHUNK_SIZE
if end > len(media) {
end = len(media)
}
info("segment", i, "start", start, "end", end)
appended := false
var body []byte
for try := 0; try < 3 && !appended; try++ {
appRequest := AppendRequest(mediaId, media[start:end], i)
body = send(UPLOAD_ENDPOINT, appRequest)
if string(body) == "" {
appended = true
}
}
if !appended {
log(errors.New(string(body)))
}
}
}
func finalizeFileUpload(file string, mediaId ObjectID) int64 {
log := func(err error) { optLogFatal("finalizeFileUpload", err) }
body := send(UPLOAD_ENDPOINT, FinalizeRequest(mediaId))
var finalizeResponse FinalizeResponse
err := json.Unmarshal(body, &finalizeResponse)
log(err)
log(errors.New(finalizeResponse.Error))
if id := ObjectID(finalizeResponse.Media_id_string); id != "" {
fmt.Println("==> Uploaded " + file + " with id " + string(id))
procInfo := finalizeResponse.Processing_info
return procInfo.Check_after_secs
} else {
log(errors.New("Could not finalize " + string(mediaId)))
return 0
}
}
func wait(seconds int64) {
fmt.Println("Waiting", seconds, "seconds")
time.Sleep(time.Duration(seconds) * time.Second)
}
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(UPLOAD_ENDPOINT + PollStatusParameters(mediaId))
var response PollStatusResponse
err := json.Unmarshal(body, &response)
log(err)
procInfo := response.Processing_info
state := procInfo.State
error = procInfo.Error
if state == "succeeded" {
succeeded = true
break
} else if state == "failed" {
break
} else {
fmt.Println("Processing progress: ", procInfo.Progress_percent, "%")
seconds := procInfo.Check_after_secs
if seconds > 10 {
seconds = 10
}
wait(seconds)
}
}
if !succeeded {
log(errors.New("File upload failed " + error.Message))
}
}
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(file, tmpMedia)
appendFileChunks(file, media, mediaId)
seconds := finalizeFileUpload(file, mediaId)
if seconds > 0 {
wait(seconds)
pollStatus(mediaId)
}
return mediaId
}
func uploadAll(files []string) []ObjectID {
ids := []ObjectID{}
for _, f := range files {
if f != "" {
id := uploadFile(f)
ids = append(ids, id)
}
}
return ids
}
type mediaKind int
const (
UNKNOWN mediaKind = iota
PIC
GIF
VIDEO
)
func kind(path string) mediaKind {
ext := filepath.Ext(path)
switch ext {
case ".jpg":
fallthrough
case ".jpeg":
fallthrough
case ".png":
return PIC
case ".gif":
return GIF
case ".mp4":
return VIDEO
}
return UNKNOWN
}
func exitIfInvalid(path string) {
log := func(err error) { optLogFatal("exitIfInvalid", err) }
// check existence AND readability
f, err := os.Open(path)
log(err)
defer f.Close()
tmp, err := ioutil.ReadAll(f)
log(err)
if len(tmp) > MAX_BYTES {
log(errors.New("File too big: " + path + " is bigger than maximum of " + strconv.Itoa(MAX_BYTES) + " Bytes"))
}
}

59
requests.go Normal file
View File

@ -0,0 +1,59 @@
package main
import (
"net/url"
"strings"
)
func LookupParameters(ids []string) string {
return "&id=" + strings.Join(ids, ",")
}
func UpdateStatusRequest(status string, mediaIDs []ObjectID, previousStatusID ObjectID) url.Values {
r := map[string][]string{
"status": {status},
"tweet_mode": {"extended"},
}
if len(mediaIDs) > 0 {
ids := []string{}
for _, id := range mediaIDs {
ids = append(ids, string(id))
}
r["media_ids"] = []string{strings.Join(ids, ",")}
}
if len(previousStatusID) > 0 {
r["in_reply_to_status_id"] = []string{string(previousStatusID)}
r["auto_populate_reply_metadata"] = []string{"true"}
}
return r
}
func UserTimelineParameters(flags userTimelineFlags, screenName string) string {
s := "&screen_name=" + screenName
if flags.withReplies {
return s
} else {
return s + "&exclude_replies=true"
}
}
func RetweetParameters(id string) string {
return id + ".json"
}
func LikeRequest(id string) url.Values {
return map[string][]string{
"id": {id},
"tweet_mode": {"extended"},
}
}
func UnlikeRequest(id string) url.Values {
return map[string][]string{
"id": {id},
}
}
func DestroyParameters(id string) string {
return id + ".json"
}

112
types.go Normal file
View File

@ -0,0 +1,112 @@
package main
import (
"encoding/json"
"html"
"strings"
"time"
)
type ObjectID string
type TwitterError struct {
Code int64
Message string
Label string
}
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 Entities struct {
Media []Media
}
type Media struct {
Media_url string
}
type StatusUser struct {
Name string
Screen_name string
}
type Status struct {
Errors []TwitterError
Full_text string
Id_str string
Created_at TwitterTime
In_reply_to_screen_name string
In_reply_to_status_id_str string
User StatusUser
Quoted_status *Status
Retweeted_status *Status
Extended_entities Entities
}
func (t Status) Error() string {
if len(t.Errors) == 0 {
return ""
} else {
s, _ := json.Marshal(t)
return "Response error " + string(s)
}
}
func (t Status) equals(t2 Status) bool {
return t.Id_str == t2.Id_str
}
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 (t Status) InReplyTo() string {
if t.In_reply_to_status_id_str != "" {
return t.In_reply_to_screen_name + " (" + t.In_reply_to_status_id_str + ")"
} else {
return ""
}
}
func (t Status) String() string {
if t.Retweeted_status != nil {
return t.User.Screen_name + " retweeted " + t.Retweeted_status.String()
}
s := t.User.Screen_name + " " + "(" + t.Id_str + ")"
if replyTo := t.InReplyTo(); replyTo != "" {
s += " in reply to " + replyTo
}
s += ":\n" + html.UnescapeString(t.Full_text)
allMedia := t.Extended_entities.Media
if len(allMedia) > 0 {
s += "\n\nMedia:"
for _, media := range allMedia {
s += " " + media.Media_url
}
}
if t.Quoted_status != nil {
s += "\n\nQuotes " + t.Quoted_status.String()
}
return s
}
func (t Status) URL() string {
return "https://twitter.com/" + t.User.Screen_name + "/status/" + t.Id_str
}