Go Program for a Unique Twitter Profile Banner

Do you think your Twitter profile banner is boring? Do you wish there was something that could rotate your Twitter banner images? Maybe what you want is to brag about your Twitter account statistics on your profile banner to make a unique Twitter profile banner. This article showcases a Go program that does just that.

Landscape image showing clouds and grass generated by the program. Statistics: 217 Following, 68 Followers, 461 Tweets, In 0 lists, Located in London, UK, 2021-11-26 23:27 UTC, https://git.io/JMOjc
A Twitter Banner generated and uploaded by the program

Retrieve User Statistics from Twitter

We will authenticate with Twitter API using OAuth 2.0 to retrieve the statistics for the given Twitter profile. To achieve this, we need the Go oauth2 package.

You can find the code for user data in user_data.go.

Get OAuth 2.0 Client

Using the Go oauth2 package you can make an HTTP Client. OAuth 2.0 requires your consumer key and consumer secret which has access to Twitter public API.

import (
	"net/http"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/clientcredentials"
)

func getTwitterOauth2Client(consumerKey string, consumerSecret string) *http.Client {
	config := &clientcredentials.Config{
		ClientID:     consumerKey,
		ClientSecret: consumerSecret,
		TokenURL:     "https://api.twitter.com/oauth2/token",
	}

	return config.Client(oauth2.NoContext)
}

Fetch the user ID for the given Twitter user name

For convenience, we want to allow people to specify user names to get the user statistics. This function uses the OAuth 2.0 client created above with the user name to get the ID of the user.

import (
	"encoding/json"
	"net/http"
)

type userLookup struct {
	Id       string `json:"id"`
	Name     string `json:"name"`
	Username string `json:"username"`
}

func fetchIdForUsername(client *http.Client, username string) (string, error) {
	req, err := http.NewRequest("GET", "https://api.twitter.com/2/users/by/username/"+username, nil)
	if err != nil {
		return "", err
	}

	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}

	dec := json.NewDecoder(resp.Body)
	var data = make(map[string]userLookup)
	err = dec.Decode(&data)
	if err != nil {
		return "", err
	}

	return data["data"].Id, nil
}

Fetch user data for the given user ID

Using the user ID we obtained above we need to make a request to Twitter public API to fetch the user data.

import (
	"encoding/json"
	"net/http"
)

type publicMetrics struct {
	FollowerCount  int `json:"followers_count"`
	FollowingCount int `json:"following_count"`
	TweetCount     int `json:"tweet_count"`
	ListedCount    int `json:"listed_count"`
}

type user struct {
	PublicMetrics publicMetrics `json:"public_metrics"`
	Location      string
}

func fetchUserData(client *http.Client, userId string) (map[string]user, error) {
	req, err := http.NewRequest("GET", "https://api.twitter.com/2/users/"+userId+"?user.fields=public_metrics,location", nil)
	if err != nil {
		return make(map[string]user), err
	}

	resp, err := client.Do(req)
	if err != nil {
		return make(map[string]user), err
	}

	dec := json.NewDecoder(resp.Body)
	userData := make(map[string]user)
	err = dec.Decode(&userData)
	if err != nil {
		return make(map[string]user), err
	}

	return userData, nil
}

Get Twitter user data function

We will have a separate function that simplifies the user data retrieving functionality so it’s easier to use externally.

import (
	"fmt"
)

func GetTwitterUserData(consumerKey string, consumerSecret string, username string) (map[string]string, error) {
	client := getTwitterOauth2Client(consumerKey, consumerSecret)

	userId, err := fetchIdForUsername(client, username)
	if err != nil {
		return make(map[string]string), err
	}

	userData, err := fetchUserData(client, userId)
	if err != nil {
		return make(map[string]string), err
	}

	return map[string]string{
		"followers_count": fmt.Sprintf("%d", userData["data"].PublicMetrics.FollowerCount),
		"following_count": fmt.Sprintf("%d", userData["data"].PublicMetrics.FollowingCount),
		"tweet_count":     fmt.Sprintf("%d", userData["data"].PublicMetrics.TweetCount),
		"listed_count":    fmt.Sprintf("%d", userData["data"].PublicMetrics.ListedCount),
		"location":        userData["data"].Location,
	}, nil
}

Get text lines function

This function will combine all the text we’re going to add to the image.

import (
	"fmt"
)

func GetTwitterUserData(consumerKey string, consumerSecret string, username string) (map[string]string, error) {
	client := getTwitterOauth2Client(consumerKey, consumerSecret)

	userId, err := fetchIdForUsername(client, username)
	if err != nil {
		return make(map[string]string), err
	}

	userData, err := fetchUserData(client, userId)
	if err != nil {
		return make(map[string]string), err
	}

	return map[string]string{
		"followers_count": fmt.Sprintf("%d", userData["data"].PublicMetrics.FollowerCount),
		"following_count": fmt.Sprintf("%d", userData["data"].PublicMetrics.FollowingCount),
		"tweet_count":     fmt.Sprintf("%d", userData["data"].PublicMetrics.TweetCount),
		"listed_count":    fmt.Sprintf("%d", userData["data"].PublicMetrics.ListedCount),
		"location":        userData["data"].Location,
	}, nil
}

Choosing a base image

To make the banner image more exciting, we can switch between different images. The program finds a random jpeg in the “images/” directory and uses it as the base image.

You can find the source for this in io.go.

import (
	"errors"
	"fmt"
	"math/rand"
	"os"
	"path/filepath"
	"time"
)

func getJpegFilesInDirectory(directory string) []string {
	var files []string

	filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			fmt.Println(err)
			return nil
		}

		fileExt := filepath.Ext(path)

		if !info.IsDir() && (fileExt == ".jpg" || fileExt == ".jpeg") {
			files = append(files, path)
		}

		return nil
	})

	return files
}

func getRandIndexInArray(array []string) (int, error) {
	if len(array) == 0 {
		return -1, errors.New("Empty list")
	}

	rand.Seed(time.Now().Unix())

	return rand.Intn(len(array)), nil
}

func GetJpegPathInDirectory(directory string) (string, error) {
	files := getJpegFilesInDirectory(directory)

	rInd, err := getRandIndexInArray(files)
	if err != nil {
		return "", errEmptyDir
	}

	return files[rInd], nil
}

Image Manipulation

We’ll draw a rectangle onto the base JPEG file. After that, we’ll add the text to the rectangle.

You can see the source code for this section in image.go.

Convert JPEG to drawable

To allow manipulation, we need to create a draw object from the selected JPEG file.

import (
	"bufio"
	"image"
	_ "image/jpeg"
	"image/draw"
	"os"
)
func GetDrawableFromImagePath(imagePath string) *image.RGBA {
	file, err := os.Open(imagePath)
	if err != nil {
		panic(err)
	}

	reader := bufio.NewReader(file)

	decodedImage, _, err := image.Decode(reader)
	if err != nil {
		panic(err)
	}

	decodedImageBounds := decodedImage.Bounds()
	drawable := image.NewRGBA(image.Rect(0, 0, decodedImageBounds.Dx(), decodedImageBounds.Dy()))
	draw.Draw(drawable, drawable.Bounds(), decodedImage, decodedImageBounds.Min, draw.Src)

	return drawable
}

Load font to use

To print text on the draw object, we need to select a font. The source code contains the Open Sans font for ease of use.

import (
	"io/ioutil"

	"github.com/golang/freetype"
	"github.com/golang/freetype/truetype"
)

func LoadFontFromPath(fontPath string) *truetype.Font {
	fontBytes, err := ioutil.ReadFile(fontPath)
	if err != nil {
		panic(err)
	}

	font, err := freetype.ParseFont(fontBytes)
	if err != nil {
		panic(err)
	}

	return font
}

Draw overlay rectangle over the image

Before we draw the text we will draw the text background. This is to make the text more readable.

import (
	"image"
	"image/color"
)

func AddOverlayOnDrawable(drawable *image.RGBA, rectangle image.Rectangle, colour *color.RGBA, opacity *color.Alpha) {
	draw.DrawMask(drawable, rectangle, &image.Uniform{colour}, image.ZP, &image.Uniform{opacity}, image.ZP, draw.Over)
}

Obtain freetype context for writing

To draw text onto the image, we need a freetype context. We obtain this by providing the text options such as the font and the font size.

import (
	"image"

	"github.com/golang/freetype"
	"github.com/golang/freetype/truetype"
)

func GetFreetypeContext(font *truetype.Font, dpi float64, fontSize float64, drawable *image.RGBA) *freetype.Context {
	context := freetype.NewContext()
	context.SetDPI(dpi)
	context.SetFont(font)
	context.SetFontSize(fontSize)
	context.SetClip(drawable.Bounds())
	context.SetDst(drawable)
	context.SetSrc(image.Black)

	return context
}

Write text lines on the overlay

We want the text to be on the overlay rectangle. Therefore we’ll use the overlay rectangle coordinates when writing the text.

import (
	"image"

	"github.com/golang/freetype"
)
func WriteLinesOnRectangle(rectangle image.Rectangle, context *freetype.Context, lines []string, fontSize int, padding int) {
	pointX := rectangle.Min.X + padding
	pointY := rectangle.Min.Y + fontSize

	for _, text := range lines {
		labelPoint := freetype.Pt(pointX, pointY)
		_, err := context.DrawString(text, labelPoint)
		if err != nil {
			panic(err)
		}

		pointY += fontSize + padding
	}
}

Save to PNG file

When debugging it’s useful to see the banner image before it’s uploaded to Twitter. For that reason, we have an option to save the banner as a PNG.

import (
	"image/png"
	"os"
)

func WriteToPngFile(filename string, drawable *image.RGBA) {
	outFile, err := os.Create(filename)
	if err != nil {
		panic(err)
	}

	png.Encode(outFile, drawable)
}

Upload Banner Image to Twitter

Finally, we’ll need to upload the generated banner image to Twitter.

Source code is available in banner_update.go.

Get Twitter OAuth 1.0 client

You can only do banner image uploads using Twitter v1 API. Twitter v1 API uses OAuth 1.0, so we’ll need an OAuth 1.0 client in our program.

import (
	"net/http"

	"github.com/dghubble/oauth1"
)

func getTwitterOauth1Client(consumerKey string, consumerSecret string, accessToken string, accessSecret string) *http.Client {
	config := oauth1.NewConfig(consumerKey, consumerSecret)
	token := oauth1.NewToken(accessToken, accessSecret)

	return config.Client(oauth1.NoContext, token)
}

Encoding banner form

Twitter update_profile_banner API requires us to encode the banner image in a particular way. In the function below we’re reading the image and writing into a buffer.

import (
	"bytes"
	"image"
	"image/png"
	"mime/multipart"
)

func writeBannerForm(body *bytes.Buffer, drawable *image.RGBA) (*multipart.Writer, error) {
	multipartWriter := multipart.NewWriter(body)

	fwriter, err := multipartWriter.CreateFormField("banner")
	if err != nil {
		return multipartWriter, err
	}

	err = png.Encode(fwriter, drawable)
	if err != nil {
		return multipartWriter, err
	}

	multipartWriter.Close()

	return multipartWriter, nil
}

Do upload request

When updating the banner, we need to ensure the content type is correctly set. The update_profile_banner API returns HTTP 201 for successful uploads.

import (
	"bytes"
	"fmt"
	"mime/multipart"
	"net/http"
)

func doTwitterUploadRequest(client *http.Client, multipartWriter *multipart.Writer, body *bytes.Buffer, debug bool) error {
	const apiUrl = "https://api.twitter.com/1.1/account/update_profile_banner.json"

	req, err := http.NewRequest("POST", apiUrl, body)
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", multipartWriter.FormDataContentType())

	res, err := client.Do(req)
	if err != nil {
		return err
	}

	if res.StatusCode != 201 {
		return errors.New(fmt.Sprintf("Failed upload request. Status: %s", res.Status))
	}

	if debug {
		fmt.Printf("Upload request returned: %s\n", res.Status)
	}

	return nil
}

Update Twitter banner

For convenience, the upload process can be triggered from a single public function.

import (
	"bytes"
	"image"
	"net/http"
)

func UpdateTwitterBanner(consumerKey string, consumerSecret string, accessToken string, accessSecret string, drawable *image.RGBA, debug bool) error {
	var body bytes.Buffer

	client := getTwitterOauth1Client(consumerKey, consumerSecret, accessToken, accessSecret)

	multipartWriter, err := writeBannerForm(&body, drawable)
	if err != nil {
		return err
	}

	err = doTwitterUploadRequest(client, multipartWriter, &body, debug)
	if err != nil {
		return err
	}

	return nil
}

Usage

Download Twitter data and replace profile banner

The main use case for the program is to fetch Twitter profile statistics, place them on a base image and upload it as a profile banner. You can achieve this by running the program in the following way:

./verbose-twit-banner -access-secret=x -access-token=x \
    -consumer-key=x -consumer-secret=x \
    -username=oliverradwell

Generate an image without any changes to your profile

You can run the program for any Twitter profile to see what image it would generate. To achieve this, the program needs to run with the following parameters:

./verbose-twit-banner -consumer-key=x -consumer-secret=x \
    -username=oliverradwell -dry-run

Your unique Twitter profile banner

Hope you found this article showcasing the verbose-twit-banner Go program useful. Now, you can also have a unique Twitter profile banner. For more details, please visit the verbose-twit-banner GitHub repository.

Links

Leave a Reply

Your email address will not be published. Required fields are marked *