misc

Unnamed repository; edit this file 'description' to name the repository.
git clone git://git.alexwennerberg.com/misc
Log | Files | Refs | README | LICENSE

mastodon-delete.go (3516B) - raw


      1 package main
      2 
      3 // Make your mastodon posts ephemeral by deleting old posts
      4 // Dies on rate limit. Designed to periodically run on cron, Not a one-time clear.
      5 // Write a wrapper script if you want that, but keep in mind the rate
      6 // limit for deleting statuses is very low.
      7 // Not thoroughly tested, caveat emptor
      8 
      9 // To create an application: go to Preferences > Development > New Application
     10 // Make sure the app has read:statuses and write:statuses. Then copy the access
     11 // Token "Your access token" into the config file, like this:
     12 
     13 // server=https://yourserver.domain
     14 // access_token=longkeystring
     15 
     16 // go build mastodon-delete.go
     17 // ./mastodon-delete -c [config filepath] -d [delete statuses older than this many days]
     18 
     19 import (
     20 	"bufio"
     21 	"encoding/json"
     22 	"flag"
     23 	"fmt"
     24 	"io/ioutil"
     25 	"log"
     26 	"net/http"
     27 	"os"
     28 	"strings"
     29 	"time"
     30 )
     31 
     32 type MastodonClient struct {
     33 	server       string
     34 	access_token string
     35 	oauthClient  http.Client
     36 }
     37 
     38 func (client *MastodonClient) do(method string, endpoint string) ([]byte, error) {
     39 	request, err := http.NewRequest(method, client.server+endpoint, nil)
     40 	request.Header.Add("Authorization", "Bearer "+client.access_token)
     41 	if err != nil {
     42 		return nil, err
     43 	}
     44 	resp, err := client.oauthClient.Do(request)
     45 	if err != nil {
     46 		return nil, err
     47 	}
     48 	defer resp.Body.Close()
     49 	body, err := ioutil.ReadAll(resp.Body)
     50 	if err != nil {
     51 		return nil, err
     52 	}
     53 	if resp.StatusCode >= 400 {
     54 		return nil, fmt.Errorf("Response code %d: %s", resp.StatusCode, string(body))
     55 	}
     56 	return body, nil
     57 }
     58 
     59 func main() {
     60 	var configPath = flag.String("c", "config.txt", "config file path")
     61 	var days = flag.Int("d", 90, "will delete statuses older than this many days")
     62 	var dryrun = flag.Bool("dryrun", false, "don't actually delete anything, just list what will be deleted")
     63 	flag.Parse()
     64 	fmt.Println("Deleting all statuses older than", *days, "days")
     65 	if *dryrun {
     66 		fmt.Println("DRY RUN -- NOT ACTUALLY DELETING ANYTHING")
     67 	}
     68 	file, err := os.Open(*configPath)
     69 	if err != nil {
     70 		log.Fatal(err)
     71 	}
     72 	scanner := bufio.NewScanner(file)
     73 	client := MastodonClient{}
     74 	for scanner.Scan() {
     75 		t := scanner.Text()
     76 		i := strings.Index(t, "=")
     77 		if i == -1 {
     78 			log.Fatal("Could not parse config file, line: ", t)
     79 		}
     80 		v := t[i+1:]
     81 		switch k := t[:i]; k {
     82 		case "server":
     83 			client.server = v
     84 		case "access_token":
     85 			client.access_token = v
     86 		}
     87 	}
     88 
     89 	resp, err := client.do("GET", "/api/v1/accounts/verify_credentials")
     90 	if err != nil {
     91 		log.Fatal(err)
     92 	}
     93 	account := &struct {
     94 		Id string `json:"id"`
     95 	}{}
     96 	err = json.Unmarshal(resp, account)
     97 	if err != nil {
     98 		log.Fatal(err)
     99 	}
    100 
    101 	var max_id string
    102 	cutoff := time.Now().UTC().AddDate(0, 0, *days*-1).Format(time.RFC3339)
    103 	for {
    104 		url := "/api/v1/accounts/" + account.Id + "/statuses"
    105 		if max_id != "" {
    106 			url += "?max_id=" + max_id
    107 		}
    108 		resp, err = client.do("GET", url)
    109 		if err != nil {
    110 			log.Fatal(err)
    111 		}
    112 		statuses := []struct {
    113 			Id        string
    114 			Content   string
    115 			CreatedAt string `json:"created_at"`
    116 		}{}
    117 		// Assumes result is already date sorted
    118 		err = json.Unmarshal(resp, &statuses)
    119 		if err != nil {
    120 			log.Fatal(err)
    121 		}
    122 		if len(statuses) == 0 {
    123 			break
    124 		}
    125 		max_id = statuses[len(statuses)-1].Id
    126 		for _, status := range statuses {
    127 			if status.CreatedAt < cutoff { // RFC 3339 date strings are sortable
    128 				fmt.Printf("Deleting status %s: '%s'\n", status.Id, status.Content)
    129 				if !*dryrun {
    130 					_, err := client.do("DELETE", "/api/v1/statuses/"+status.Id)
    131 					if err != nil {
    132 						log.Fatal(err)
    133 					}
    134 				}
    135 			}
    136 		}
    137 	}
    138 }