package main import ( "encoding/base64" "encoding/json" "errors" "fmt" "git.gutmet.org/goutil.git" "io/ioutil" "net/http" "net/url" "os" "path/filepath" "strconv" "strings" ) const ( MAX_BYTES = 5 * 1024 * 1024 CHARACTER_LIMIT = 280 UPLOAD_ENDPOINT = "https://upload.twitter.com/1.1/media/upload.json" STATUS_ENDPOINT = "https://api.twitter.com/1.1/statuses/update.json" ) func optLogFatal(decorum string, err error) { if err != nil { fmt.Fprintln(os.Stderr, "drivel: "+decorum+": "+err.Error()) os.Exit(-1) } } 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 { return map[string][]string{ "command": {"INIT"}, "media_type": {mediaType}, "total_bytes": {strconv.Itoa(totalBytes)}, } } type InitResponse struct { Response Media_id_string string } func AppendRequest(mediaID string, mediaData string, segmentIndex int) url.Values { return map[string][]string{ "command": {"APPEND"}, "media_id": {mediaID}, "media_data": {mediaData}, "segment_index": {strconv.Itoa(segmentIndex)}, } } func FinalizeRequest(mediaID string) url.Values { return map[string][]string{ "command": {"FINALIZE"}, "media_id": {mediaID}, } } type FinalizeResponse struct { Error string Media_id_string string } 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 } 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 send(client *http.Client, url string, vals url.Values) []byte { log := func(err error) { v, _ := json.Marshal(vals) optLogFatal("send "+url+" "+string(v), err) } resp, err := client.PostForm(url, vals) log(err) defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { fmt.Fprintln(os.Stderr, "response:", resp, "\n") fmt.Fprintln(os.Stderr, "body:", string(body), "\n") log(errors.New("HTTP status " + fmt.Sprint(resp.StatusCode))) } log(err) return body } func uploadFile(client *http.Client, file string) ObjectID { log := func(err error) { optLogFatal("uploadFile "+file, err) } media, err := ioutil.ReadFile(file) log(err) initRequest := InitRequest(getMimetype(file), len(media)) body := send(client, UPLOAD_ENDPOINT, initRequest) var initResponse InitResponse err = json.Unmarshal(body, &initResponse) log(err) if len(initResponse.Errors) == 0 { mediaId := initResponse.Media_id_string appRequest := AppendRequest(mediaId, base64.RawURLEncoding.EncodeToString(media), 0) body := send(client, UPLOAD_ENDPOINT, appRequest) if string(body) == "" { body := send(client, UPLOAD_ENDPOINT, FinalizeRequest(mediaId)) var finalizeResponse FinalizeResponse json.Unmarshal(body, &finalizeResponse) if id := ObjectID(finalizeResponse.Media_id_string); id != "" { fmt.Println("==> Uploaded " + file + " with id " + string(id)) return id } } } log(errors.New("Could not upload file " + file)) return "" } func uploadAll(client *http.Client, files []string) []ObjectID { ids := []ObjectID{} for _, f := range files { if f != "" { id := uploadFile(client, 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 { split := []string{} words := strings.Split(status, " ") s := "" for _, word := range words { if s == "" && len(word) <= CHARACTER_LIMIT { s = word } else if len(s)+1+len(word) <= CHARACTER_LIMIT { s = s + " " + word } else { split = append(split, s) bound := goutil.IntMin(len(word), CHARACTER_LIMIT) s = string([]rune(word)[:bound]) } } if s != "" { split = append(split, s) } 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() data { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "Usage: drivel STATUS [FILE1, FILE2, ...]") os.Exit(-1) } d := data{} d.status = splitStatus(os.Args[1]) for _, arg := range os.Args[2:] { 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 } func tweet(client *http.Client, 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) var sr UpdateStatusResponse err := json.Unmarshal(body, &sr) log(err) if len(sr.Errors) > 0 { log(sr) } fmt.Println("==> Updated status to '" + status + "' with id " + sr.Id_str) return ObjectID(sr.Id_str) } 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) } func (d *data) uploadPics(client *http.Client, from, to int) []ObjectID { pics := goutil.StrSlice(d.pics, from, to) return uploadAll(client, pics) } func (d *data) uploadGif(client *http.Client, i int) []ObjectID { gif := d.getGif(i) return uploadAll(client, []string{gif}) } func (d *data) uploadVideo(client *http.Client, i int) []ObjectID { vid := d.getVideo(i) return uploadAll(client, []string{vid}) } func (d *data) push(client *http.Client) { if d == nil { return } var previous ObjectID = "" empty := false i, g, v := 0, 0, 0 for !empty { empty = true status := d.getStatus(i) mediaIDs := []ObjectID{} if status != "" { empty = false } from := i * 4 to := (i + 1) * 4 mediaIDs = d.uploadPics(client, from, to) if len(mediaIDs) == 0 { mediaIDs = d.uploadGif(client, g) g++ } if len(mediaIDs) == 0 { mediaIDs = d.uploadVideo(client, v) v++ } if len(mediaIDs) > 0 { empty = false } if !empty { previous = tweet(client, status, mediaIDs, previous) i++ } } } func main() { d := splitArguments() httpClient := getClient() d.push(httpClient) }