Deleting a message on the watched channel now deletes the posts it
produced. Making that possible reshapes how a message reaches an output:
- OutputInterface takes an output.Post and returns an output.Ref, the
identifier the platform gave the new post (tweet ID for X, URI and
CID for bluesky). It also gained Delete(Ref).
- The discord package no longer forwards raw MessageCreate values but
a discord.Event carrying the kind of change, so the MessageDelete
handler can use the same channel. Deleted messages arrive without an
author, so only the channel ID can be filtered on.
- The new store package remembers Discord message ID -> per output Ref.
Set POST_STORE to a path to keep that mapping across restarts;
without it the mapping is memory only and a restart makes older
messages undeletable. It holds the most recent 1000 messages.
main.go grows a distributor type to hold the outputs and the store
rather than threading them through the event loop.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
2.7 KiB
Go
119 lines
2.7 KiB
Go
package output
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/michimani/gotwi"
|
|
"github.com/michimani/gotwi/media/upload"
|
|
uploadtypes "github.com/michimani/gotwi/media/upload/types"
|
|
"github.com/michimani/gotwi/tweet/managetweet"
|
|
"github.com/michimani/gotwi/tweet/managetweet/types"
|
|
)
|
|
|
|
type twitteroutput struct {
|
|
accessToken string
|
|
accessSecret string
|
|
client *gotwi.Client
|
|
}
|
|
|
|
func TwitterOutput(accessToken string, accessSecret string) *twitteroutput {
|
|
twitteroutput := &twitteroutput{
|
|
accessToken: accessToken,
|
|
accessSecret: accessSecret,
|
|
}
|
|
errOAuth := twitteroutput.newOAuth1Client(accessToken, accessSecret)
|
|
if errOAuth != nil {
|
|
fmt.Fprintln(os.Stderr, errOAuth)
|
|
os.Exit(1)
|
|
}
|
|
return twitteroutput
|
|
}
|
|
|
|
func (to *twitteroutput) newOAuth1Client(accessToken, accessSecret string) error {
|
|
in := &gotwi.NewClientInput{
|
|
AuthenticationMethod: gotwi.AuthenMethodOAuth1UserContext,
|
|
OAuthToken: accessToken,
|
|
OAuthTokenSecret: accessSecret,
|
|
}
|
|
|
|
client, err := gotwi.NewClient(in)
|
|
to.client = client
|
|
return err
|
|
}
|
|
|
|
func (to *twitteroutput) Write(post Post) (Ref, error) {
|
|
mediaIDs := make([]string, 0, len(post.Images))
|
|
for _, img := range post.Images {
|
|
mediaID, err := to.uploadImage(img)
|
|
if err != nil {
|
|
return Ref{}, fmt.Errorf("uploading %s: %w", img.Filename, err)
|
|
}
|
|
mediaIDs = append(mediaIDs, mediaID)
|
|
}
|
|
|
|
p := &types.CreateInput{
|
|
Text: gotwi.String(post.Text),
|
|
}
|
|
if len(mediaIDs) > 0 {
|
|
p.Media = &types.CreateInputMedia{
|
|
MediaIDs: mediaIDs,
|
|
}
|
|
}
|
|
out, err := managetweet.Create(context.Background(), to.client, p)
|
|
if err != nil {
|
|
return Ref{}, err
|
|
}
|
|
if out.Data.ID == nil {
|
|
return Ref{}, errors.New("created tweet has no ID")
|
|
}
|
|
return Ref{ID: *out.Data.ID}, nil
|
|
}
|
|
|
|
func (to *twitteroutput) Delete(ref Ref) error {
|
|
if ref.ID == "" {
|
|
return errors.New("no tweet ID to delete")
|
|
}
|
|
_, err := managetweet.Delete(context.Background(), to.client, &types.DeleteInput{ID: ref.ID})
|
|
return err
|
|
}
|
|
|
|
func (to *twitteroutput) uploadImage(img Image) (string, error) {
|
|
ctx := context.Background()
|
|
|
|
initialized, err := upload.Initialize(ctx, to.client, &uploadtypes.InitializeInput{
|
|
MediaCategory: uploadtypes.MediaCategoryTweetImage,
|
|
MediaType: uploadtypes.MediaType(img.ContentType),
|
|
TotalBytes: len(img.Data),
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
mediaID := initialized.Data.MediaID
|
|
|
|
_, err = upload.Append(ctx, to.client, &uploadtypes.AppendInput{
|
|
MediaID: mediaID,
|
|
Media: bytes.NewReader(img.Data),
|
|
SegmentIndex: 0,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, err = upload.Finalize(ctx, to.client, &uploadtypes.FinalizeInput{
|
|
MediaID: mediaID,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return mediaID, nil
|
|
}
|
|
|
|
func (to *twitteroutput) GetName() string {
|
|
return "twitter"
|
|
}
|