Skip to content
This repository has been archived by the owner on Apr 15, 2023. It is now read-only.

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergey Kibish committed Apr 25, 2021
0 parents commit e221b30
Show file tree
Hide file tree
Showing 13 changed files with 1,004 additions and 0 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: release

on:
push:
tags:
- '*'

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
on:
push:
branches:
- main
pull_request:
branches:
- main

name: run tests
jobs:
test:
strategy:
matrix:
go-version: [1.16.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
if: success()
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: go test -v -cover -race ./...
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
29 changes: 29 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This is an example .goreleaser.yml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarm:
- 6
- 7
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Sergey Kibish

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Twitter Cleaner

![Test](https://github.com/skibish/twitter-cleaner/workflows/run%20tests/badge.svg)
![Release](https://github.com/skibish/twitter-cleaner/workflows/release/badge.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/skibish/twitter-cleaner)](https://goreportcard.com/report/github.com/skibish/twitter-cleaner)

Clean your Twitter.

Once in 24 hours all tweets that are older than half a year (by default) will be deleted, un-retweeted and un-favorited from your feed.

## Motivation

I observed that tweets liveability is a few days maximum.
Why then I need to store them all in my timeline?
For me Twitter is a place to interact with others and not to use it as a diary for archeologists.

If you would like to do regular cleanups too, this tool is for you.

## Install

Download binary from [releases page](https://github.com/skibish/twitter-cleaner/releases).

If you have Go:

```sh
go get github.com/skibish/twitter-cleaner
```

## Example

```sh
$ ./twitter-retention \
-access-token aaa \
-access-token-secret ttt \
-consumer-key kkk \
-consumer-secret xxx
2021/04/25 13:19:53 successfully started
2021/04/25 13:20:52 scanned through 44 timeline tweets
2021/04/25 13:20:52 DELETING XXXXXXXXXXXXXX240
2021/04/25 13:20:52 UNRETWEETING XXXXXXXXXXXXXX624
2021/04/25 13:20:53 scanned through 0 timeline tweets
2021/04/25 13:20:53 scanned through 199 favorite tweets
2021/04/25 13:20:54 scanned through 105 favorite tweets
2021/04/25 13:20:54 UNFAVORITING XXXXXXXXXXXXXX416
2021/04/25 13:20:54 UNFAVORITING XXXXXXXXXXXXXX183
2021/04/25 13:20:54 UNFAVORITING XXXXXXXXXXXXXX100
2021/04/25 13:20:54 UNFAVORITING XXXXXXXXXXXXXX225
2021/04/25 13:20:55 UNFAVORITING XXXXXXXXXXXXXX508
2021/04/25 13:20:55 UNFAVORITING XXXXXXXXXXXXXX317
^C
2021/04/25 13:21:20 shutdown
```

## Usage

```sh
./twitter-cleaner:
-access-token string
Access token
-access-token-secret string
Access token secret
-check-interval duration
Cleanup interval (default 24h0m0s)
-consumer-key string
Consumer key
-consumer-secret string
Consumer secret
-dry-run
Check that something can be deleted, no real deletion is made
-tweet-age duration
Tweets older than this duration will be deleted (default 4380h0m0s)
-v Show version
```

You can [find out here](https://developer.twitter.com/en/docs/basics/authentication/guides/access-tokens) how to create all needed tokens.

## Development

```sh
go get
```
173 changes: 173 additions & 0 deletions cleaner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package main

import (
"fmt"
"log"
"sync"
"time"

"github.com/ChimeraCoder/anaconda"
)

type Cleaner struct {
twitter *Twitter
tweetAge time.Duration
checkInterval time.Duration
dryRun bool
userID int64
ticker *time.Ticker
shutdown chan bool
}

func NewCleaner(twitter *Twitter, tweetAge, checkInterval time.Duration, dryRun bool) *Cleaner {
return &Cleaner{
twitter: twitter,
tweetAge: tweetAge,
checkInterval: checkInterval,
dryRun: dryRun,
ticker: time.NewTicker(checkInterval),
shutdown: make(chan bool),
}
}

func (c *Cleaner) Init() error {
userID, err := c.twitter.Self()
if err != nil {
return fmt.Errorf("failed to get user ID: %w", err)
}
c.userID = userID

return nil
}

func (c *Cleaner) Start() error {
var wg sync.WaitGroup

for {
select {
case <-c.ticker.C:
wg.Add(2)
if err := c.cleanTimeline(); err != nil {
return fmt.Errorf("failed to clean timeline: %w", err)
}
wg.Done()

if err := c.cleanFavorites(); err != nil {
return fmt.Errorf("failed to clean favorites: %w", err)
}
wg.Done()
case <-c.shutdown:
wg.Wait()
return nil
}
}
}

func (c *Cleaner) Stop() {
c.ticker.Stop()
c.shutdown <- true
}

func (c *Cleaner) cleanTimeline() error {
var oldestTweetID int64
for {
tweets, err := c.twitter.GetUserTimeline(oldestTweetID)
if err != nil {
return fmt.Errorf("failed to get tweets from user timeline: %w", err)
}

log.Printf("scanned through %d timeline tweets", len(tweets))

if len(tweets) <= 1 {
break
}

// get oldest tweet ID from the response
// and set it as a limit for the next call
oldestTweetID = tweets[len(tweets)-1].Id

for _, tweet := range tweets {
if err := c.remove(tweet); err != nil {
return fmt.Errorf("failed to remove tweet: %w", err)
}
}
}

return nil
}

func (c *Cleaner) cleanFavorites() error {
var oldestTweetID int64
for {
tweets, err := c.twitter.GetUserFavorites(oldestTweetID)
if err != nil {
return fmt.Errorf("failed to get tweets from favorites: %w", err)
}

log.Printf("scanned through %d favorite tweets", len(tweets))

if len(tweets) <= 1 {
break
}

// get oldest tweet ID from the response
// and set it as a limit for the next call
oldestTweetID = tweets[len(tweets)-1].Id

for _, tweet := range tweets {
if err := c.remove(tweet); err != nil {
return fmt.Errorf("failed to remove tweet: %w", err)
}
}
}

return nil
}

func (c *Cleaner) remove(tweet anaconda.Tweet) error {
createdAt, err := tweet.CreatedAtTime()
if err != nil {
return fmt.Errorf("failed to get createdAt time of a tweet: %w", err)
}

// if duration is less than retention period specified,
// skip it
if time.Since(createdAt) < c.tweetAge {
return nil
}

if tweet.Favorited {
log.Printf("UNFAVORITING\t%d", tweet.Id)

if !c.dryRun {
if err := c.twitter.UnFavorite(tweet.Id); err != nil {
return fmt.Errorf("failed to unfavorite the tweet %d: %w", tweet.Id, err)
}
}
}

if tweet.Retweeted {
log.Printf("UNRETWEETING\t%d", tweet.Id)

if !c.dryRun {
if err := c.twitter.UnRetweet(tweet.Id); err != nil {
return fmt.Errorf("failed to unretweet the tweet %d: %w", tweet.Id, err)
}
}
}

// if favorited/retweeted tweet is not a users tweet,
// return earlier
if tweet.User.Id != c.userID {
return nil
}

log.Printf("DELETING\t%d", tweet.Id)
if !c.dryRun {
if err := c.twitter.Delete(tweet.Id); err != nil {
return fmt.Errorf("failed to delete the tweet %d: %w", tweet.Id, err)
}
}

return nil
}
Loading

0 comments on commit e221b30

Please sign in to comment.