diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index ea1e295..2ae6fdc 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -60,3 +60,26 @@ jobs: - name: Invalidate CloudFront distribution cache run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*" + + post-to-mastodon: + name: Post to Mastodon + needs: build-and-deploy + runs-on: ubuntu-22.04 + if: ${{ contains(github.event.head_commit.modified, 'content/posts/') }} + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # fetch all history for all tags and branches + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + - run: go mod download + - name: Auto-toot + env: + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} + MASTODON_ORIGIN: ${{ secrets.MASTODON_ORIGIN }} + BLOG_ORIGIN: ${{ secrets.BLOG_ORIGIN }} + working-directory: ./auto-toot + run: go run main.go diff --git a/auto-toot/README.md b/auto-toot/README.md new file mode 100644 index 0000000..50ab24b --- /dev/null +++ b/auto-toot/README.md @@ -0,0 +1,11 @@ +# Auto-toot + +This is a simple Go app that checks the latest commit for new files added within the `/content/posts` directory. If there are new files, it will parse the Hugo blog post and automatically publish a Mastodon toot with the description, hashtags and link to the post. + +### Environment Variables + +| Variable | Description | Example | +| ----------------------- | ---------------------------------------------------------- | --------------------------- | +| `MASTODON_ACCESS_TOKEN` | The access token for your Mastodon account. | `a1b2c3d4e5f6g7h8i9j0` | +| `MASTODON_ORIGIN` | The origin for your Mastodon instance. | `https://mas.to` | +| `BLOG_ORIGIN` | The origin used when generating the link to the blog post. | `https://www.tobyscott.dev` | diff --git a/auto-toot/go.mod b/auto-toot/go.mod new file mode 100644 index 0000000..18424b9 --- /dev/null +++ b/auto-toot/go.mod @@ -0,0 +1,3 @@ +module auto-toot + +go 1.21.1 diff --git a/auto-toot/helpers/getNewFilesInLastCommit.go b/auto-toot/helpers/getNewFilesInLastCommit.go new file mode 100644 index 0000000..588d2ab --- /dev/null +++ b/auto-toot/helpers/getNewFilesInLastCommit.go @@ -0,0 +1,24 @@ +package helpers + +import ( + "bytes" + "fmt" + "os/exec" + "strings" +) + +// Returns a slice of strings representing the file paths of newly created files in the last commit. +func GetNewFilesInLastCommit() ([]string, error) { + // Use 'git diff' to get a list of added files in the last commit + cmd := exec.Command("git", "diff", "--diff-filter=A", "--name-only", "HEAD~1", "HEAD") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("error running git diff: %v", err) + } + + // Split the output by new lines to get individual file paths + files := strings.Split(strings.TrimSpace(out.String()), "\n") + return files, nil +} diff --git a/auto-toot/helpers/parseHugoPost.go b/auto-toot/helpers/parseHugoPost.go new file mode 100644 index 0000000..9fd62e3 --- /dev/null +++ b/auto-toot/helpers/parseHugoPost.go @@ -0,0 +1,98 @@ +package helpers + +import ( + "bufio" + "fmt" + "net/url" + "os" + "regexp" + "strings" +) + +type HugoPost struct { + Description string `json:"description"` + Tags []string `json:"tags"` + URL string `json:"url"` +} + +func ParseHugoPost(filePath string, blogUrl string) (HugoPost, error) { + + file, err := os.Open(filePath) + if err != nil { + fmt.Printf("Error opening file: %v\n", err) + return HugoPost{}, err + } + defer file.Close() + + var fileContent strings.Builder + scanner := bufio.NewScanner(file) + for scanner.Scan() { + fileContent.WriteString(scanner.Text() + "\n") + } + if err := scanner.Err(); err != nil { + fmt.Printf("Error reading file: %v\n", err) + return HugoPost{}, err + } + + // Convert the builder to a string for regex processing + contentString := fileContent.String() + + // Convert the file path to a blog post URL + postSlug := strings.TrimPrefix(filePath, "../content/posts/") + postSlug = strings.TrimSuffix(postSlug, ".md") + postSlug = strings.TrimSuffix(postSlug, "/index") // Remove '/index' if present + + // Use url.QueryEscape to encode spaces and other characters in the post slug + postSlug = url.QueryEscape(postSlug) + + blogPostURL := fmt.Sprintf(blogUrl+"/posts/%s", postSlug) + + // Use regex to find the description block + descriptionRegex := regexp.MustCompile(`description: "(.*?)"`) + descriptionMatch := descriptionRegex.FindStringSubmatch(contentString) + + description := "" + if len(descriptionMatch) > 1 { + description = descriptionMatch[1] + } + + // Use regex to find the tags block (taking new lines into account) + tagsRegex := regexp.MustCompile(`(?s)tags:\s+\[(.*?)\]`) + tagsMatch := tagsRegex.FindStringSubmatch(contentString) + + // Process tags + var tags []string + if len(tagsMatch) > 1 { + // Remove the square brackets and split the string by comma + tagsStr := strings.Trim(tagsMatch[1], "[]") + tagsStr = strings.Replace(tagsStr, " \"", "\"", -1) // Remove 4-space indentations + tagsStr = strings.Replace(tagsStr, "\"", "", -1) // Remove quotes + tagsStr = strings.Replace(tagsStr, "\n", "", -1) // Remove newlines + tagsStr = strings.Replace(tagsStr, " ", "", -1) // Remove spaces + + // Remove trailing comma + if strings.HasSuffix(tagsStr, ",") { + tagsStr = tagsStr[:len(tagsStr)-1] + } + tags = strings.Split(tagsStr, ",") + + } + + post := HugoPost{ + Description: description, + Tags: tags, + URL: blogPostURL, + } + + return post, nil +} + +func (p HugoPost) GetHashtagString() string { + var hashtags []string + for _, tag := range p.Tags { + hashtags = append(hashtags, fmt.Sprintf("#%s", tag)) + } + hashtagsStr := strings.Join(hashtags, " ") + hashtagsStr = strings.TrimSpace(hashtagsStr) // Trim the trailing space + return hashtagsStr +} diff --git a/auto-toot/helpers/sendToot.go b/auto-toot/helpers/sendToot.go new file mode 100644 index 0000000..c37f0b8 --- /dev/null +++ b/auto-toot/helpers/sendToot.go @@ -0,0 +1,63 @@ +package helpers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// Structure to parse the response from Mastodon API +type MastodonStatusResponse struct { + URL string `json:"url"` +} + +func SendToot(mastodonURL string, accessToken string, status string) error { + data := url.Values{} + data.Set("status", status) + + // Create a new request + req, err := http.NewRequest("POST", mastodonURL+"/api/v1/statuses", strings.NewReader(data.Encode())) + if err != nil { + fmt.Printf("Error creating request: %v\n", err) + return err + } + + // Set headers + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + // Send the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Error posting to Mastodon: %v\n", err) + return err + } + defer resp.Body.Close() + + // Check the response + if resp.StatusCode != http.StatusOK { + fmt.Printf("Error response from Mastodon: %v\n", resp.Status) + return err + } + + // Read response body + respBody, error := io.ReadAll(resp.Body) + if error != nil { + fmt.Println(error) + } + + // Parse the JSON response + var mastodonResp MastodonStatusResponse + if err := json.Unmarshal(respBody, &mastodonResp); err != nil { + fmt.Printf("Error parsing JSON response: %v\n", err) + return err + } + + fmt.Printf("Successfully posted to Mastodon: %s \n", mastodonResp.URL) + + return nil +} diff --git a/auto-toot/main.go b/auto-toot/main.go new file mode 100644 index 0000000..8ce3b25 --- /dev/null +++ b/auto-toot/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "auto-toot/helpers" + "fmt" + "os" + "strings" +) + +func main() { + + mastodonOrigin := os.Getenv("MASTODON_ORIGIN") + blogOrigin := os.Getenv("BLOG_ORIGIN") + accessToken := os.Getenv("MASTODON_ACCESS_TOKEN") + + newFiles, err := helpers.GetNewFilesInLastCommit() + if err != nil { + fmt.Printf("Error getting new files: %v\n", err) + return + } + + for _, file := range newFiles { + + if strings.HasPrefix(file, "content/posts/") { + + filePath := "../" + file + + hugoPostDetails, err := helpers.ParseHugoPost(filePath, blogOrigin) + if err != nil { + fmt.Printf("Error parsing Hugo post: %v\n", err) + return + } + + hashtagString := hugoPostDetails.GetHashtagString() + status := fmt.Sprintf("%s\n\n%s\n\n%s", hugoPostDetails.Description, hugoPostDetails.URL, hashtagString) + + helpers.SendToot(mastodonOrigin, accessToken, status) + if err != nil { + fmt.Printf("Error posting about %s to Mastodon: %v\n", filePath, err) + } else { + fmt.Printf("Successfully posted about %s to Mastodon.\n", filePath) + } + } + } +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..eed2802 --- /dev/null +++ b/go.work @@ -0,0 +1,5 @@ +go 1.21.1 + +use ( + ./auto-toot +)