The code and data behind xeiaso.net
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

bring back the IRC announcer

Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 690f3d0a 1a1c4141

+1318 -86
+1 -68
cmd/xesite/api.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "encoding/json" 6 - "io/fs" 7 5 "os" 8 6 "os/exec" 9 7 "runtime" ··· 12 10 "github.com/twitchtv/twirp" 13 11 "google.golang.org/protobuf/types/known/emptypb" 14 12 "google.golang.org/protobuf/types/known/timestamppb" 15 - "xeiaso.net/v4/internal/jsonfeed" 16 13 "xeiaso.net/v4/internal/lume" 17 14 "xeiaso.net/v4/pb" 18 15 "xeiaso.net/v4/pb/external/protofeed" ··· 60 57 } 61 58 62 59 func (f *FeedServer) Get(ctx context.Context, _ *emptypb.Empty) (*protofeed.Feed, error) { 63 - data, err := fs.ReadFile(f.fs, "blog.json") 64 - if err != nil { 65 - return nil, twirp.InternalErrorf("can't read blog.json: %w", err) 66 - } 67 - 68 - var feed jsonfeed.Feed 69 - 70 - if err := json.Unmarshal(data, &feed); err != nil { 71 - return nil, twirp.InternalErrorf("can't unmarshal blog.json: %w", err) 72 - } 73 - 74 - var result protofeed.Feed 75 - 76 - result.Title = feed.Title 77 - result.HomePageUrl = feed.HomePageURL 78 - result.FeedUrl = feed.FeedURL 79 - result.Description = feed.Description 80 - result.UserComment = feed.UserComment 81 - result.Icon = feed.Icon 82 - result.Favicon = feed.Favicon 83 - result.Expired = feed.Expired 84 - result.Language = feed.Language 85 - result.Items = make([]*protofeed.Item, len(feed.Items)) 86 - result.Authors = make([]*protofeed.Author, len(feed.Authors)) 87 - 88 - for i, item := range feed.Items { 89 - var atts []*protofeed.Attachment 90 - for _, att := range item.Attachments { 91 - atts = append(atts, &protofeed.Attachment{ 92 - Url: att.URL, 93 - MimeType: att.MIMEType, 94 - Title: att.Title, 95 - SizeInBytes: att.SizeInBytes, 96 - DurationInSeconds: att.DurationInSeconds, 97 - }) 98 - } 99 - 100 - var authors []*protofeed.Author 101 - for _, author := range item.Authors { 102 - authors = append(authors, &protofeed.Author{ 103 - Name: author.Name, 104 - Url: author.URL, 105 - Avatar: author.Avatar, 106 - }) 107 - } 108 - 109 - result.Items[i] = &protofeed.Item{ 110 - Id: item.ID, 111 - Url: item.URL, 112 - ExternalUrl: item.ExternalURL, 113 - Title: item.Title, 114 - ContentHtml: item.ContentHTML, 115 - ContentText: item.ContentText, 116 - Summary: item.Summary, 117 - Image: item.Image, 118 - BannerImage: item.BannerImage, 119 - DatePublished: timestamppb.New(item.DatePublished), 120 - DateModified: timestamppb.New(item.DateModified), 121 - Tags: item.Tags, 122 - Authors: authors, 123 - Attachments: atts, 124 - } 125 - } 126 - 127 - return &result, nil 60 + return f.fs.LoadProtoFeed() 128 61 }
+10 -8
cmd/xesite/main.go
··· 27 27 gitRepo = flag.String("git-repo", "https://github.com/Xe/site", "Git repository to clone") 28 28 githubSecret = flag.String("github-secret", "", "GitHub secret to use for webhooks") 29 29 internalAPIBind = flag.String("internal-api-bind", ":3001", "Port to listen on for the internal API") 30 + mimiAnnounceURL = flag.String("mimi-announce-url", "", "URL to use for the mimi announce service") 30 31 miToken = flag.String("mi-token", "", "Token to use for the mi API") 31 32 patreonSaasProxyURL = flag.String("patreon-saasproxy-url", "http://xesite-patreon-saasproxy.flycast", "URL to use for the patreon saasproxy") 32 33 siteURL = flag.String("site-url", "https://xeiaso.net/", "URL to use for the site") ··· 53 54 } 54 55 55 56 fs, err := lume.New(ctx, &lume.Options{ 56 - Branch: *gitBranch, 57 - Repo: *gitRepo, 58 - StaticSiteDir: "lume", 59 - URL: *siteURL, 60 - Development: *devel, 61 - PatreonClient: pc, 62 - DataDir: *dataDir, 63 - MiToken: *miToken, 57 + Branch: *gitBranch, 58 + Repo: *gitRepo, 59 + StaticSiteDir: "lume", 60 + URL: *siteURL, 61 + Development: *devel, 62 + PatreonClient: pc, 63 + DataDir: *dataDir, 64 + MiToken: *miToken, 65 + MimiAnnounceURL: *mimiAnnounceURL, 64 66 }) 65 67 if err != nil { 66 68 log.Fatal(err)
+111 -9
internal/lume/lume.go
··· 11 11 "io/fs" 12 12 "log" 13 13 "log/slog" 14 + "net/http" 14 15 "os" 15 16 "os/exec" 16 17 "path/filepath" ··· 19 20 20 21 "github.com/go-git/go-git/v5" 21 22 "github.com/go-git/go-git/v5/plumbing" 23 + "github.com/twitchtv/twirp" 24 + "google.golang.org/protobuf/types/known/timestamppb" 22 25 "gopkg.in/mxpv/patreon-go.v1" 23 26 "tailscale.com/metrics" 24 27 "xeiaso.net/v4/internal/config" 28 + "xeiaso.net/v4/internal/jsonfeed" 25 29 "xeiaso.net/v4/internal/mi" 30 + "xeiaso.net/v4/pb/external/mimi/announce" 31 + "xeiaso.net/v4/pb/external/protofeed" 26 32 ) 27 33 28 34 var ( ··· 65 71 opt *Options 66 72 conf *config.Config 67 73 68 - miClient *mi.Client 74 + miClient *mi.Client 75 + mimiClient announce.Announce 69 76 70 77 fs fs.FS 71 78 lock sync.Mutex ··· 117 124 } 118 125 119 126 type Options struct { 120 - Development bool 121 - Branch string 122 - Repo string 123 - StaticSiteDir string 124 - URL string 125 - PatreonClient *patreon.Client 126 - DataDir string 127 - MiToken string 127 + Development bool 128 + Branch string 129 + Repo string 130 + StaticSiteDir string 131 + URL string 132 + PatreonClient *patreon.Client 133 + DataDir string 134 + MiToken string 135 + MimiAnnounceURL string 128 136 } 129 137 130 138 func New(ctx context.Context, o *Options) (*FS, error) { ··· 206 214 slog.Debug("mi integration enabled") 207 215 } 208 216 217 + if o.MimiAnnounceURL != "" { 218 + mimiClient := announce.NewAnnounceProtobufClient(o.MimiAnnounceURL, &http.Client{}) 219 + fs.mimiClient = mimiClient 220 + slog.Debug("mimi integration enabled") 221 + } 222 + 209 223 conf, err := config.Load(filepath.Join(fs.repoDir, "config.dhall")) 210 224 if err != nil { 211 225 log.Fatal(err) ··· 224 238 } 225 239 }() 226 240 } 241 + go fs.mimiRefresh() 227 242 fs.lastBuildTime = time.Now() 228 243 229 244 return fs, nil ··· 295 310 } 296 311 }() 297 312 } 313 + go f.mimiRefresh() 298 314 299 315 return nil 300 316 } ··· 512 528 cmd.Stderr = os.Stderr 513 529 return cmd.Run() 514 530 } 531 + 532 + func (f *FS) LoadProtoFeed() (*protofeed.Feed, error) { 533 + data, err := fs.ReadFile(f, "blog.json") 534 + if err != nil { 535 + return nil, twirp.InternalErrorf("can't read blog.json: %w", err) 536 + } 537 + 538 + var feed jsonfeed.Feed 539 + 540 + if err := json.Unmarshal(data, &feed); err != nil { 541 + return nil, twirp.InternalErrorf("can't unmarshal blog.json: %w", err) 542 + } 543 + 544 + var result protofeed.Feed 545 + 546 + result.Title = feed.Title 547 + result.HomePageUrl = feed.HomePageURL 548 + result.FeedUrl = feed.FeedURL 549 + result.Description = feed.Description 550 + result.UserComment = feed.UserComment 551 + result.Icon = feed.Icon 552 + result.Favicon = feed.Favicon 553 + result.Expired = feed.Expired 554 + result.Language = feed.Language 555 + result.Items = make([]*protofeed.Item, len(feed.Items)) 556 + result.Authors = make([]*protofeed.Author, len(feed.Authors)) 557 + 558 + for i, item := range feed.Items { 559 + var atts []*protofeed.Attachment 560 + for _, att := range item.Attachments { 561 + atts = append(atts, &protofeed.Attachment{ 562 + Url: att.URL, 563 + MimeType: att.MIMEType, 564 + Title: att.Title, 565 + SizeInBytes: att.SizeInBytes, 566 + DurationInSeconds: att.DurationInSeconds, 567 + }) 568 + } 569 + 570 + var authors []*protofeed.Author 571 + for _, author := range item.Authors { 572 + authors = append(authors, &protofeed.Author{ 573 + Name: author.Name, 574 + Url: author.URL, 575 + Avatar: author.Avatar, 576 + }) 577 + } 578 + 579 + result.Items[i] = &protofeed.Item{ 580 + Id: item.ID, 581 + Url: item.URL, 582 + ExternalUrl: item.ExternalURL, 583 + Title: item.Title, 584 + ContentHtml: item.ContentHTML, 585 + ContentText: item.ContentText, 586 + Summary: item.Summary, 587 + Image: item.Image, 588 + BannerImage: item.BannerImage, 589 + DatePublished: timestamppb.New(item.DatePublished), 590 + DateModified: timestamppb.New(item.DateModified), 591 + Tags: item.Tags, 592 + Authors: authors, 593 + Attachments: atts, 594 + } 595 + } 596 + 597 + return &result, nil 598 + } 599 + 600 + func (f *FS) mimiRefresh() { 601 + if f.mimiClient == nil { 602 + return 603 + } 604 + 605 + blog, err := f.LoadProtoFeed() 606 + if err != nil { 607 + slog.Error("failed to load proto feed", "err", err) 608 + return 609 + } 610 + 611 + for _, it := range blog.GetItems() { 612 + if _, err := f.mimiClient.Announce(context.Background(), it); err != nil { 613 + slog.Error("failed to announce", "err", err, "item", it.GetId()) 614 + } 615 + } 616 + }
+1
pb/external/generate.go
··· 1 1 package protofeed 2 2 3 3 //go:generate protoc --proto_path=. --proto_path=.. --go_out=./protofeed --go_opt=paths=source_relative ./protofeed.proto 4 + //go:generate protoc --proto_path=. --proto_path=.. --go_out=./mimi/announce --go_opt=paths=source_relative --twirp_out=./mimi/announce --twirp_opt=paths=source_relative ./mimi-announce.proto
+10
pb/external/mimi-announce.proto
··· 1 + syntax = "proto3"; 2 + package within.website.x.mimi.announce; 3 + option go_package = "xeiaso.net/v4/pb/external/mimi/announce"; 4 + 5 + import "google/protobuf/empty.proto"; 6 + import "protofeed.proto"; 7 + 8 + service Announce { 9 + rpc Announce(protofeed.Item) returns (google.protobuf.Empty) {} 10 + }
+79
pb/external/mimi/announce/mimi-announce.pb.go
··· 1 + // Code generated by protoc-gen-go. DO NOT EDIT. 2 + // versions: 3 + // protoc-gen-go v1.32.0 4 + // protoc v4.25.3 5 + // source: mimi-announce.proto 6 + 7 + package announce 8 + 9 + import ( 10 + protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 + protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 + emptypb "google.golang.org/protobuf/types/known/emptypb" 13 + reflect "reflect" 14 + protofeed "xeiaso.net/v4/pb/external/protofeed" 15 + ) 16 + 17 + const ( 18 + // Verify that this generated code is sufficiently up-to-date. 19 + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 + // Verify that runtime/protoimpl is sufficiently up-to-date. 21 + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 + ) 23 + 24 + var File_mimi_announce_proto protoreflect.FileDescriptor 25 + 26 + var file_mimi_announce_proto_rawDesc = []byte{ 27 + 0x0a, 0x13, 0x6d, 0x69, 0x6d, 0x69, 0x2d, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x2e, 28 + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x2e, 0x77, 0x65, 29 + 0x62, 0x73, 0x69, 0x74, 0x65, 0x2e, 0x78, 0x2e, 0x6d, 0x69, 0x6d, 0x69, 0x2e, 0x61, 0x6e, 0x6e, 30 + 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 31 + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 32 + 0x74, 0x6f, 0x1a, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x66, 0x65, 0x65, 0x64, 0x2e, 0x70, 0x72, 33 + 0x6f, 0x74, 0x6f, 0x32, 0x41, 0x0a, 0x08, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x12, 34 + 0x35, 0x0a, 0x08, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x12, 0x0f, 0x2e, 0x70, 0x72, 35 + 0x6f, 0x74, 0x6f, 0x66, 0x65, 0x65, 0x64, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x1a, 0x16, 0x2e, 0x67, 36 + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 37 + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x29, 0x5a, 0x27, 0x78, 0x65, 0x69, 0x61, 0x73, 0x6f, 38 + 0x2e, 0x6e, 0x65, 0x74, 0x2f, 0x76, 0x34, 0x2f, 0x70, 0x62, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x72, 39 + 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x69, 0x6d, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 40 + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 41 + } 42 + 43 + var file_mimi_announce_proto_goTypes = []interface{}{ 44 + (*protofeed.Item)(nil), // 0: protofeed.Item 45 + (*emptypb.Empty)(nil), // 1: google.protobuf.Empty 46 + } 47 + var file_mimi_announce_proto_depIdxs = []int32{ 48 + 0, // 0: within.website.x.mimi.announce.Announce.Announce:input_type -> protofeed.Item 49 + 1, // 1: within.website.x.mimi.announce.Announce.Announce:output_type -> google.protobuf.Empty 50 + 1, // [1:2] is the sub-list for method output_type 51 + 0, // [0:1] is the sub-list for method input_type 52 + 0, // [0:0] is the sub-list for extension type_name 53 + 0, // [0:0] is the sub-list for extension extendee 54 + 0, // [0:0] is the sub-list for field type_name 55 + } 56 + 57 + func init() { file_mimi_announce_proto_init() } 58 + func file_mimi_announce_proto_init() { 59 + if File_mimi_announce_proto != nil { 60 + return 61 + } 62 + type x struct{} 63 + out := protoimpl.TypeBuilder{ 64 + File: protoimpl.DescBuilder{ 65 + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 66 + RawDescriptor: file_mimi_announce_proto_rawDesc, 67 + NumEnums: 0, 68 + NumMessages: 0, 69 + NumExtensions: 0, 70 + NumServices: 1, 71 + }, 72 + GoTypes: file_mimi_announce_proto_goTypes, 73 + DependencyIndexes: file_mimi_announce_proto_depIdxs, 74 + }.Build() 75 + File_mimi_announce_proto = out.File 76 + file_mimi_announce_proto_rawDesc = nil 77 + file_mimi_announce_proto_goTypes = nil 78 + file_mimi_announce_proto_depIdxs = nil 79 + }
+1105
pb/external/mimi/announce/mimi-announce.twirp.go
··· 1 + // Code generated by protoc-gen-twirp v8.1.3, DO NOT EDIT. 2 + // source: mimi-announce.proto 3 + 4 + package announce 5 + 6 + import context "context" 7 + import fmt "fmt" 8 + import http "net/http" 9 + import io "io" 10 + import json "encoding/json" 11 + import strconv "strconv" 12 + import strings "strings" 13 + 14 + import protojson "google.golang.org/protobuf/encoding/protojson" 15 + import proto "google.golang.org/protobuf/proto" 16 + import twirp "github.com/twitchtv/twirp" 17 + import ctxsetters "github.com/twitchtv/twirp/ctxsetters" 18 + 19 + import google_protobuf "google.golang.org/protobuf/types/known/emptypb" 20 + import protofeed "xeiaso.net/v4/pb/external/protofeed" 21 + 22 + import bytes "bytes" 23 + import errors "errors" 24 + import path "path" 25 + import url "net/url" 26 + 27 + // Version compatibility assertion. 28 + // If the constant is not defined in the package, that likely means 29 + // the package needs to be updated to work with this generated code. 30 + // See https://twitchtv.github.io/twirp/docs/version_matrix.html 31 + const _ = twirp.TwirpPackageMinVersion_8_1_0 32 + 33 + // ================== 34 + // Announce Interface 35 + // ================== 36 + 37 + type Announce interface { 38 + Announce(context.Context, *protofeed.Item) (*google_protobuf.Empty, error) 39 + } 40 + 41 + // ======================== 42 + // Announce Protobuf Client 43 + // ======================== 44 + 45 + type announceProtobufClient struct { 46 + client HTTPClient 47 + urls [1]string 48 + interceptor twirp.Interceptor 49 + opts twirp.ClientOptions 50 + } 51 + 52 + // NewAnnounceProtobufClient creates a Protobuf client that implements the Announce interface. 53 + // It communicates using Protobuf and can be configured with a custom HTTPClient. 54 + func NewAnnounceProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Announce { 55 + if c, ok := client.(*http.Client); ok { 56 + client = withoutRedirects(c) 57 + } 58 + 59 + clientOpts := twirp.ClientOptions{} 60 + for _, o := range opts { 61 + o(&clientOpts) 62 + } 63 + 64 + // Using ReadOpt allows backwards and forwards compatibility with new options in the future 65 + literalURLs := false 66 + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) 67 + var pathPrefix string 68 + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { 69 + pathPrefix = "/twirp" // default prefix 70 + } 71 + 72 + // Build method URLs: <baseURL>[<prefix>]/<package>.<Service>/<Method> 73 + serviceURL := sanitizeBaseURL(baseURL) 74 + serviceURL += baseServicePath(pathPrefix, "within.website.x.mimi.announce", "Announce") 75 + urls := [1]string{ 76 + serviceURL + "Announce", 77 + } 78 + 79 + return &announceProtobufClient{ 80 + client: client, 81 + urls: urls, 82 + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), 83 + opts: clientOpts, 84 + } 85 + } 86 + 87 + func (c *announceProtobufClient) Announce(ctx context.Context, in *protofeed.Item) (*google_protobuf.Empty, error) { 88 + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.mimi.announce") 89 + ctx = ctxsetters.WithServiceName(ctx, "Announce") 90 + ctx = ctxsetters.WithMethodName(ctx, "Announce") 91 + caller := c.callAnnounce 92 + if c.interceptor != nil { 93 + caller = func(ctx context.Context, req *protofeed.Item) (*google_protobuf.Empty, error) { 94 + resp, err := c.interceptor( 95 + func(ctx context.Context, req interface{}) (interface{}, error) { 96 + typedReq, ok := req.(*protofeed.Item) 97 + if !ok { 98 + return nil, twirp.InternalError("failed type assertion req.(*protofeed.Item) when calling interceptor") 99 + } 100 + return c.callAnnounce(ctx, typedReq) 101 + }, 102 + )(ctx, req) 103 + if resp != nil { 104 + typedResp, ok := resp.(*google_protobuf.Empty) 105 + if !ok { 106 + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") 107 + } 108 + return typedResp, err 109 + } 110 + return nil, err 111 + } 112 + } 113 + return caller(ctx, in) 114 + } 115 + 116 + func (c *announceProtobufClient) callAnnounce(ctx context.Context, in *protofeed.Item) (*google_protobuf.Empty, error) { 117 + out := new(google_protobuf.Empty) 118 + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) 119 + if err != nil { 120 + twerr, ok := err.(twirp.Error) 121 + if !ok { 122 + twerr = twirp.InternalErrorWith(err) 123 + } 124 + callClientError(ctx, c.opts.Hooks, twerr) 125 + return nil, err 126 + } 127 + 128 + callClientResponseReceived(ctx, c.opts.Hooks) 129 + 130 + return out, nil 131 + } 132 + 133 + // ==================== 134 + // Announce JSON Client 135 + // ==================== 136 + 137 + type announceJSONClient struct { 138 + client HTTPClient 139 + urls [1]string 140 + interceptor twirp.Interceptor 141 + opts twirp.ClientOptions 142 + } 143 + 144 + // NewAnnounceJSONClient creates a JSON client that implements the Announce interface. 145 + // It communicates using JSON and can be configured with a custom HTTPClient. 146 + func NewAnnounceJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Announce { 147 + if c, ok := client.(*http.Client); ok { 148 + client = withoutRedirects(c) 149 + } 150 + 151 + clientOpts := twirp.ClientOptions{} 152 + for _, o := range opts { 153 + o(&clientOpts) 154 + } 155 + 156 + // Using ReadOpt allows backwards and forwards compatibility with new options in the future 157 + literalURLs := false 158 + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) 159 + var pathPrefix string 160 + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { 161 + pathPrefix = "/twirp" // default prefix 162 + } 163 + 164 + // Build method URLs: <baseURL>[<prefix>]/<package>.<Service>/<Method> 165 + serviceURL := sanitizeBaseURL(baseURL) 166 + serviceURL += baseServicePath(pathPrefix, "within.website.x.mimi.announce", "Announce") 167 + urls := [1]string{ 168 + serviceURL + "Announce", 169 + } 170 + 171 + return &announceJSONClient{ 172 + client: client, 173 + urls: urls, 174 + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), 175 + opts: clientOpts, 176 + } 177 + } 178 + 179 + func (c *announceJSONClient) Announce(ctx context.Context, in *protofeed.Item) (*google_protobuf.Empty, error) { 180 + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.mimi.announce") 181 + ctx = ctxsetters.WithServiceName(ctx, "Announce") 182 + ctx = ctxsetters.WithMethodName(ctx, "Announce") 183 + caller := c.callAnnounce 184 + if c.interceptor != nil { 185 + caller = func(ctx context.Context, req *protofeed.Item) (*google_protobuf.Empty, error) { 186 + resp, err := c.interceptor( 187 + func(ctx context.Context, req interface{}) (interface{}, error) { 188 + typedReq, ok := req.(*protofeed.Item) 189 + if !ok { 190 + return nil, twirp.InternalError("failed type assertion req.(*protofeed.Item) when calling interceptor") 191 + } 192 + return c.callAnnounce(ctx, typedReq) 193 + }, 194 + )(ctx, req) 195 + if resp != nil { 196 + typedResp, ok := resp.(*google_protobuf.Empty) 197 + if !ok { 198 + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") 199 + } 200 + return typedResp, err 201 + } 202 + return nil, err 203 + } 204 + } 205 + return caller(ctx, in) 206 + } 207 + 208 + func (c *announceJSONClient) callAnnounce(ctx context.Context, in *protofeed.Item) (*google_protobuf.Empty, error) { 209 + out := new(google_protobuf.Empty) 210 + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) 211 + if err != nil { 212 + twerr, ok := err.(twirp.Error) 213 + if !ok { 214 + twerr = twirp.InternalErrorWith(err) 215 + } 216 + callClientError(ctx, c.opts.Hooks, twerr) 217 + return nil, err 218 + } 219 + 220 + callClientResponseReceived(ctx, c.opts.Hooks) 221 + 222 + return out, nil 223 + } 224 + 225 + // ======================= 226 + // Announce Server Handler 227 + // ======================= 228 + 229 + type announceServer struct { 230 + Announce 231 + interceptor twirp.Interceptor 232 + hooks *twirp.ServerHooks 233 + pathPrefix string // prefix for routing 234 + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response 235 + jsonCamelCase bool // JSON fields are serialized as lowerCamelCase rather than keeping the original proto names 236 + } 237 + 238 + // NewAnnounceServer builds a TwirpServer that can be used as an http.Handler to handle 239 + // HTTP requests that are routed to the right method in the provided svc implementation. 240 + // The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). 241 + func NewAnnounceServer(svc Announce, opts ...interface{}) TwirpServer { 242 + serverOpts := newServerOpts(opts) 243 + 244 + // Using ReadOpt allows backwards and forwards compatibility with new options in the future 245 + jsonSkipDefaults := false 246 + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) 247 + jsonCamelCase := false 248 + _ = serverOpts.ReadOpt("jsonCamelCase", &jsonCamelCase) 249 + var pathPrefix string 250 + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { 251 + pathPrefix = "/twirp" // default prefix 252 + } 253 + 254 + return &announceServer{ 255 + Announce: svc, 256 + hooks: serverOpts.Hooks, 257 + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), 258 + pathPrefix: pathPrefix, 259 + jsonSkipDefaults: jsonSkipDefaults, 260 + jsonCamelCase: jsonCamelCase, 261 + } 262 + } 263 + 264 + // writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. 265 + // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) 266 + func (s *announceServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { 267 + writeError(ctx, resp, err, s.hooks) 268 + } 269 + 270 + // handleRequestBodyError is used to handle error when the twirp server cannot read request 271 + func (s *announceServer) handleRequestBodyError(ctx context.Context, resp http.ResponseWriter, msg string, err error) { 272 + if context.Canceled == ctx.Err() { 273 + s.writeError(ctx, resp, twirp.NewError(twirp.Canceled, "failed to read request: context canceled")) 274 + return 275 + } 276 + if context.DeadlineExceeded == ctx.Err() { 277 + s.writeError(ctx, resp, twirp.NewError(twirp.DeadlineExceeded, "failed to read request: deadline exceeded")) 278 + return 279 + } 280 + s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) 281 + } 282 + 283 + // AnnouncePathPrefix is a convenience constant that may identify URL paths. 284 + // Should be used with caution, it only matches routes generated by Twirp Go clients, 285 + // with the default "/twirp" prefix and default CamelCase service and method names. 286 + // More info: https://twitchtv.github.io/twirp/docs/routing.html 287 + const AnnouncePathPrefix = "/twirp/within.website.x.mimi.announce.Announce/" 288 + 289 + func (s *announceServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { 290 + ctx := req.Context() 291 + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.mimi.announce") 292 + ctx = ctxsetters.WithServiceName(ctx, "Announce") 293 + ctx = ctxsetters.WithResponseWriter(ctx, resp) 294 + 295 + var err error 296 + ctx, err = callRequestReceived(ctx, s.hooks) 297 + if err != nil { 298 + s.writeError(ctx, resp, err) 299 + return 300 + } 301 + 302 + if req.Method != "POST" { 303 + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) 304 + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) 305 + return 306 + } 307 + 308 + // Verify path format: [<prefix>]/<package>.<Service>/<Method> 309 + prefix, pkgService, method := parseTwirpPath(req.URL.Path) 310 + if pkgService != "within.website.x.mimi.announce.Announce" { 311 + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) 312 + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) 313 + return 314 + } 315 + if prefix != s.pathPrefix { 316 + msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path) 317 + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) 318 + return 319 + } 320 + 321 + switch method { 322 + case "Announce": 323 + s.serveAnnounce(ctx, resp, req) 324 + return 325 + default: 326 + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) 327 + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) 328 + return 329 + } 330 + } 331 + 332 + func (s *announceServer) serveAnnounce(ctx context.Context, resp http.ResponseWriter, req *http.Request) { 333 + header := req.Header.Get("Content-Type") 334 + i := strings.Index(header, ";") 335 + if i == -1 { 336 + i = len(header) 337 + } 338 + switch strings.TrimSpace(strings.ToLower(header[:i])) { 339 + case "application/json": 340 + s.serveAnnounceJSON(ctx, resp, req) 341 + case "application/protobuf": 342 + s.serveAnnounceProtobuf(ctx, resp, req) 343 + default: 344 + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) 345 + twerr := badRouteError(msg, req.Method, req.URL.Path) 346 + s.writeError(ctx, resp, twerr) 347 + } 348 + } 349 + 350 + func (s *announceServer) serveAnnounceJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { 351 + var err error 352 + ctx = ctxsetters.WithMethodName(ctx, "Announce") 353 + ctx, err = callRequestRouted(ctx, s.hooks) 354 + if err != nil { 355 + s.writeError(ctx, resp, err) 356 + return 357 + } 358 + 359 + d := json.NewDecoder(req.Body) 360 + rawReqBody := json.RawMessage{} 361 + if err := d.Decode(&rawReqBody); err != nil { 362 + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) 363 + return 364 + } 365 + reqContent := new(protofeed.Item) 366 + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} 367 + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { 368 + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) 369 + return 370 + } 371 + 372 + handler := s.Announce.Announce 373 + if s.interceptor != nil { 374 + handler = func(ctx context.Context, req *protofeed.Item) (*google_protobuf.Empty, error) { 375 + resp, err := s.interceptor( 376 + func(ctx context.Context, req interface{}) (interface{}, error) { 377 + typedReq, ok := req.(*protofeed.Item) 378 + if !ok { 379 + return nil, twirp.InternalError("failed type assertion req.(*protofeed.Item) when calling interceptor") 380 + } 381 + return s.Announce.Announce(ctx, typedReq) 382 + }, 383 + )(ctx, req) 384 + if resp != nil { 385 + typedResp, ok := resp.(*google_protobuf.Empty) 386 + if !ok { 387 + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") 388 + } 389 + return typedResp, err 390 + } 391 + return nil, err 392 + } 393 + } 394 + 395 + // Call service method 396 + var respContent *google_protobuf.Empty 397 + func() { 398 + defer ensurePanicResponses(ctx, resp, s.hooks) 399 + respContent, err = handler(ctx, reqContent) 400 + }() 401 + 402 + if err != nil { 403 + s.writeError(ctx, resp, err) 404 + return 405 + } 406 + if respContent == nil { 407 + s.writeError(ctx, resp, twirp.InternalError("received a nil *google_protobuf.Empty and nil error while calling Announce. nil responses are not supported")) 408 + return 409 + } 410 + 411 + ctx = callResponsePrepared(ctx, s.hooks) 412 + 413 + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} 414 + respBytes, err := marshaler.Marshal(respContent) 415 + if err != nil { 416 + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) 417 + return 418 + } 419 + 420 + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) 421 + resp.Header().Set("Content-Type", "application/json") 422 + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) 423 + resp.WriteHeader(http.StatusOK) 424 + 425 + if n, err := resp.Write(respBytes); err != nil { 426 + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) 427 + twerr := twirp.NewError(twirp.Unknown, msg) 428 + ctx = callError(ctx, s.hooks, twerr) 429 + } 430 + callResponseSent(ctx, s.hooks) 431 + } 432 + 433 + func (s *announceServer) serveAnnounceProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { 434 + var err error 435 + ctx = ctxsetters.WithMethodName(ctx, "Announce") 436 + ctx, err = callRequestRouted(ctx, s.hooks) 437 + if err != nil { 438 + s.writeError(ctx, resp, err) 439 + return 440 + } 441 + 442 + buf, err := io.ReadAll(req.Body) 443 + if err != nil { 444 + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) 445 + return 446 + } 447 + reqContent := new(protofeed.Item) 448 + if err = proto.Unmarshal(buf, reqContent); err != nil { 449 + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) 450 + return 451 + } 452 + 453 + handler := s.Announce.Announce 454 + if s.interceptor != nil { 455 + handler = func(ctx context.Context, req *protofeed.Item) (*google_protobuf.Empty, error) { 456 + resp, err := s.interceptor( 457 + func(ctx context.Context, req interface{}) (interface{}, error) { 458 + typedReq, ok := req.(*protofeed.Item) 459 + if !ok { 460 + return nil, twirp.InternalError("failed type assertion req.(*protofeed.Item) when calling interceptor") 461 + } 462 + return s.Announce.Announce(ctx, typedReq) 463 + }, 464 + )(ctx, req) 465 + if resp != nil { 466 + typedResp, ok := resp.(*google_protobuf.Empty) 467 + if !ok { 468 + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") 469 + } 470 + return typedResp, err 471 + } 472 + return nil, err 473 + } 474 + } 475 + 476 + // Call service method 477 + var respContent *google_protobuf.Empty 478 + func() { 479 + defer ensurePanicResponses(ctx, resp, s.hooks) 480 + respContent, err = handler(ctx, reqContent) 481 + }() 482 + 483 + if err != nil { 484 + s.writeError(ctx, resp, err) 485 + return 486 + } 487 + if respContent == nil { 488 + s.writeError(ctx, resp, twirp.InternalError("received a nil *google_protobuf.Empty and nil error while calling Announce. nil responses are not supported")) 489 + return 490 + } 491 + 492 + ctx = callResponsePrepared(ctx, s.hooks) 493 + 494 + respBytes, err := proto.Marshal(respContent) 495 + if err != nil { 496 + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) 497 + return 498 + } 499 + 500 + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) 501 + resp.Header().Set("Content-Type", "application/protobuf") 502 + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) 503 + resp.WriteHeader(http.StatusOK) 504 + if n, err := resp.Write(respBytes); err != nil { 505 + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) 506 + twerr := twirp.NewError(twirp.Unknown, msg) 507 + ctx = callError(ctx, s.hooks, twerr) 508 + } 509 + callResponseSent(ctx, s.hooks) 510 + } 511 + 512 + func (s *announceServer) ServiceDescriptor() ([]byte, int) { 513 + return twirpFileDescriptor0, 0 514 + } 515 + 516 + func (s *announceServer) ProtocGenTwirpVersion() string { 517 + return "v8.1.3" 518 + } 519 + 520 + // PathPrefix returns the base service path, in the form: "/<prefix>/<package>.<Service>/" 521 + // that is everything in a Twirp route except for the <Method>. This can be used for routing, 522 + // for example to identify the requests that are targeted to this service in a mux. 523 + func (s *announceServer) PathPrefix() string { 524 + return baseServicePath(s.pathPrefix, "within.website.x.mimi.announce", "Announce") 525 + } 526 + 527 + // ===== 528 + // Utils 529 + // ===== 530 + 531 + // HTTPClient is the interface used by generated clients to send HTTP requests. 532 + // It is fulfilled by *(net/http).Client, which is sufficient for most users. 533 + // Users can provide their own implementation for special retry policies. 534 + // 535 + // HTTPClient implementations should not follow redirects. Redirects are 536 + // automatically disabled if *(net/http).Client is passed to client 537 + // constructors. See the withoutRedirects function in this file for more 538 + // details. 539 + type HTTPClient interface { 540 + Do(req *http.Request) (*http.Response, error) 541 + } 542 + 543 + // TwirpServer is the interface generated server structs will support: they're 544 + // HTTP handlers with additional methods for accessing metadata about the 545 + // service. Those accessors are a low-level API for building reflection tools. 546 + // Most people can think of TwirpServers as just http.Handlers. 547 + type TwirpServer interface { 548 + http.Handler 549 + 550 + // ServiceDescriptor returns gzipped bytes describing the .proto file that 551 + // this service was generated from. Once unzipped, the bytes can be 552 + // unmarshalled as a 553 + // google.golang.org/protobuf/types/descriptorpb.FileDescriptorProto. 554 + // 555 + // The returned integer is the index of this particular service within that 556 + // FileDescriptorProto's 'Service' slice of ServiceDescriptorProtos. This is a 557 + // low-level field, expected to be used for reflection. 558 + ServiceDescriptor() ([]byte, int) 559 + 560 + // ProtocGenTwirpVersion is the semantic version string of the version of 561 + // twirp used to generate this file. 562 + ProtocGenTwirpVersion() string 563 + 564 + // PathPrefix returns the HTTP URL path prefix for all methods handled by this 565 + // service. This can be used with an HTTP mux to route Twirp requests. 566 + // The path prefix is in the form: "/<prefix>/<package>.<Service>/" 567 + // that is, everything in a Twirp route except for the <Method> at the end. 568 + PathPrefix() string 569 + } 570 + 571 + func newServerOpts(opts []interface{}) *twirp.ServerOptions { 572 + serverOpts := &twirp.ServerOptions{} 573 + for _, opt := range opts { 574 + switch o := opt.(type) { 575 + case twirp.ServerOption: 576 + o(serverOpts) 577 + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument 578 + twirp.WithServerHooks(o)(serverOpts) 579 + case nil: // backwards compatibility, allow nil value for the argument 580 + continue 581 + default: 582 + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) 583 + } 584 + } 585 + return serverOpts 586 + } 587 + 588 + // WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). 589 + // Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. 590 + // If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) 591 + func WriteError(resp http.ResponseWriter, err error) { 592 + writeError(context.Background(), resp, err, nil) 593 + } 594 + 595 + // writeError writes Twirp errors in the response and triggers hooks. 596 + func writeError(ctx context.Context, resp http.ResponseWriter, err error, hooks *twirp.ServerHooks) { 597 + // Convert to a twirp.Error. Non-twirp errors are converted to internal errors. 598 + var twerr twirp.Error 599 + if !errors.As(err, &twerr) { 600 + twerr = twirp.InternalErrorWith(err) 601 + } 602 + 603 + statusCode := twirp.ServerHTTPStatusFromErrorCode(twerr.Code()) 604 + ctx = ctxsetters.WithStatusCode(ctx, statusCode) 605 + ctx = callError(ctx, hooks, twerr) 606 + 607 + respBody := marshalErrorToJSON(twerr) 608 + 609 + resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON 610 + resp.Header().Set("Content-Length", strconv.Itoa(len(respBody))) 611 + resp.WriteHeader(statusCode) // set HTTP status code and send response 612 + 613 + _, writeErr := resp.Write(respBody) 614 + if writeErr != nil { 615 + // We have three options here. We could log the error, call the Error 616 + // hook, or just silently ignore the error. 617 + // 618 + // Logging is unacceptable because we don't have a user-controlled 619 + // logger; writing out to stderr without permission is too rude. 620 + // 621 + // Calling the Error hook would confuse users: it would mean the Error 622 + // hook got called twice for one request, which is likely to lead to 623 + // duplicated log messages and metrics, no matter how well we document 624 + // the behavior. 625 + // 626 + // Silently ignoring the error is our least-bad option. It's highly 627 + // likely that the connection is broken and the original 'err' says 628 + // so anyway. 629 + _ = writeErr 630 + } 631 + 632 + callResponseSent(ctx, hooks) 633 + } 634 + 635 + // sanitizeBaseURL parses the the baseURL, and adds the "http" scheme if needed. 636 + // If the URL is unparsable, the baseURL is returned unchanged. 637 + func sanitizeBaseURL(baseURL string) string { 638 + u, err := url.Parse(baseURL) 639 + if err != nil { 640 + return baseURL // invalid URL will fail later when making requests 641 + } 642 + if u.Scheme == "" { 643 + u.Scheme = "http" 644 + } 645 + return u.String() 646 + } 647 + 648 + // baseServicePath composes the path prefix for the service (without <Method>). 649 + // e.g.: baseServicePath("/twirp", "my.pkg", "MyService") 650 + // 651 + // returns => "/twirp/my.pkg.MyService/" 652 + // 653 + // e.g.: baseServicePath("", "", "MyService") 654 + // 655 + // returns => "/MyService/" 656 + func baseServicePath(prefix, pkg, service string) string { 657 + fullServiceName := service 658 + if pkg != "" { 659 + fullServiceName = pkg + "." + service 660 + } 661 + return path.Join("/", prefix, fullServiceName) + "/" 662 + } 663 + 664 + // parseTwirpPath extracts path components form a valid Twirp route. 665 + // Expected format: "[<prefix>]/<package>.<Service>/<Method>" 666 + // e.g.: prefix, pkgService, method := parseTwirpPath("/twirp/pkg.Svc/MakeHat") 667 + func parseTwirpPath(path string) (string, string, string) { 668 + parts := strings.Split(path, "/") 669 + if len(parts) < 2 { 670 + return "", "", "" 671 + } 672 + method := parts[len(parts)-1] 673 + pkgService := parts[len(parts)-2] 674 + prefix := strings.Join(parts[0:len(parts)-2], "/") 675 + return prefix, pkgService, method 676 + } 677 + 678 + // getCustomHTTPReqHeaders retrieves a copy of any headers that are set in 679 + // a context through the twirp.WithHTTPRequestHeaders function. 680 + // If there are no headers set, or if they have the wrong type, nil is returned. 681 + func getCustomHTTPReqHeaders(ctx context.Context) http.Header { 682 + header, ok := twirp.HTTPRequestHeaders(ctx) 683 + if !ok || header == nil { 684 + return nil 685 + } 686 + copied := make(http.Header) 687 + for k, vv := range header { 688 + if vv == nil { 689 + copied[k] = nil 690 + continue 691 + } 692 + copied[k] = make([]string, len(vv)) 693 + copy(copied[k], vv) 694 + } 695 + return copied 696 + } 697 + 698 + // newRequest makes an http.Request from a client, adding common headers. 699 + func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { 700 + req, err := http.NewRequest("POST", url, reqBody) 701 + if err != nil { 702 + return nil, err 703 + } 704 + req = req.WithContext(ctx) 705 + if customHeader := getCustomHTTPReqHeaders(ctx); customHeader != nil { 706 + req.Header = customHeader 707 + } 708 + req.Header.Set("Accept", contentType) 709 + req.Header.Set("Content-Type", contentType) 710 + req.Header.Set("Twirp-Version", "v8.1.3") 711 + return req, nil 712 + } 713 + 714 + // JSON serialization for errors 715 + type twerrJSON struct { 716 + Code string `json:"code"` 717 + Msg string `json:"msg"` 718 + Meta map[string]string `json:"meta,omitempty"` 719 + } 720 + 721 + // marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body. 722 + // If serialization fails, it will use a descriptive Internal error instead. 723 + func marshalErrorToJSON(twerr twirp.Error) []byte { 724 + // make sure that msg is not too large 725 + msg := twerr.Msg() 726 + if len(msg) > 1e6 { 727 + msg = msg[:1e6] 728 + } 729 + 730 + tj := twerrJSON{ 731 + Code: string(twerr.Code()), 732 + Msg: msg, 733 + Meta: twerr.MetaMap(), 734 + } 735 + 736 + buf, err := json.Marshal(&tj) 737 + if err != nil { 738 + buf = []byte("{\"type\": \"" + twirp.Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback 739 + } 740 + 741 + return buf 742 + } 743 + 744 + // errorFromResponse builds a twirp.Error from a non-200 HTTP response. 745 + // If the response has a valid serialized Twirp error, then it's returned. 746 + // If not, the response status code is used to generate a similar twirp 747 + // error. See twirpErrorFromIntermediary for more info on intermediary errors. 748 + func errorFromResponse(resp *http.Response) twirp.Error { 749 + statusCode := resp.StatusCode 750 + statusText := http.StatusText(statusCode) 751 + 752 + if isHTTPRedirect(statusCode) { 753 + // Unexpected redirect: it must be an error from an intermediary. 754 + // Twirp clients don't follow redirects automatically, Twirp only handles 755 + // POST requests, redirects should only happen on GET and HEAD requests. 756 + location := resp.Header.Get("Location") 757 + msg := fmt.Sprintf("unexpected HTTP status code %d %q received, Location=%q", statusCode, statusText, location) 758 + return twirpErrorFromIntermediary(statusCode, msg, location) 759 + } 760 + 761 + respBodyBytes, err := io.ReadAll(resp.Body) 762 + if err != nil { 763 + return wrapInternal(err, "failed to read server error response body") 764 + } 765 + 766 + var tj twerrJSON 767 + dec := json.NewDecoder(bytes.NewReader(respBodyBytes)) 768 + dec.DisallowUnknownFields() 769 + if err := dec.Decode(&tj); err != nil || tj.Code == "" { 770 + // Invalid JSON response; it must be an error from an intermediary. 771 + msg := fmt.Sprintf("Error from intermediary with HTTP status code %d %q", statusCode, statusText) 772 + return twirpErrorFromIntermediary(statusCode, msg, string(respBodyBytes)) 773 + } 774 + 775 + errorCode := twirp.ErrorCode(tj.Code) 776 + if !twirp.IsValidErrorCode(errorCode) { 777 + msg := "invalid type returned from server error response: " + tj.Code 778 + return twirp.InternalError(msg).WithMeta("body", string(respBodyBytes)) 779 + } 780 + 781 + twerr := twirp.NewError(errorCode, tj.Msg) 782 + for k, v := range tj.Meta { 783 + twerr = twerr.WithMeta(k, v) 784 + } 785 + return twerr 786 + } 787 + 788 + // twirpErrorFromIntermediary maps HTTP errors from non-twirp sources to twirp errors. 789 + // The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. 790 + // Returned twirp Errors have some additional metadata for inspection. 791 + func twirpErrorFromIntermediary(status int, msg string, bodyOrLocation string) twirp.Error { 792 + var code twirp.ErrorCode 793 + if isHTTPRedirect(status) { // 3xx 794 + code = twirp.Internal 795 + } else { 796 + switch status { 797 + case 400: // Bad Request 798 + code = twirp.Internal 799 + case 401: // Unauthorized 800 + code = twirp.Unauthenticated 801 + case 403: // Forbidden 802 + code = twirp.PermissionDenied 803 + case 404: // Not Found 804 + code = twirp.BadRoute 805 + case 429: // Too Many Requests 806 + code = twirp.ResourceExhausted 807 + case 502, 503, 504: // Bad Gateway, Service Unavailable, Gateway Timeout 808 + code = twirp.Unavailable 809 + default: // All other codes 810 + code = twirp.Unknown 811 + } 812 + } 813 + 814 + twerr := twirp.NewError(code, msg) 815 + twerr = twerr.WithMeta("http_error_from_intermediary", "true") // to easily know if this error was from intermediary 816 + twerr = twerr.WithMeta("status_code", strconv.Itoa(status)) 817 + if isHTTPRedirect(status) { 818 + twerr = twerr.WithMeta("location", bodyOrLocation) 819 + } else { 820 + twerr = twerr.WithMeta("body", bodyOrLocation) 821 + } 822 + return twerr 823 + } 824 + 825 + func isHTTPRedirect(status int) bool { 826 + return status >= 300 && status <= 399 827 + } 828 + 829 + // wrapInternal wraps an error with a prefix as an Internal error. 830 + // The original error cause is accessible by github.com/pkg/errors.Cause. 831 + func wrapInternal(err error, prefix string) twirp.Error { 832 + return twirp.InternalErrorWith(&wrappedError{prefix: prefix, cause: err}) 833 + } 834 + 835 + type wrappedError struct { 836 + prefix string 837 + cause error 838 + } 839 + 840 + func (e *wrappedError) Error() string { return e.prefix + ": " + e.cause.Error() } 841 + func (e *wrappedError) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As 842 + func (e *wrappedError) Cause() error { return e.cause } // for github.com/pkg/errors 843 + 844 + // ensurePanicResponses makes sure that rpc methods causing a panic still result in a Twirp Internal 845 + // error response (status 500), and error hooks are properly called with the panic wrapped as an error. 846 + // The panic is re-raised so it can be handled normally with middleware. 847 + func ensurePanicResponses(ctx context.Context, resp http.ResponseWriter, hooks *twirp.ServerHooks) { 848 + if r := recover(); r != nil { 849 + // Wrap the panic as an error so it can be passed to error hooks. 850 + // The original error is accessible from error hooks, but not visible in the response. 851 + err := errFromPanic(r) 852 + twerr := &internalWithCause{msg: "Internal service panic", cause: err} 853 + // Actually write the error 854 + writeError(ctx, resp, twerr, hooks) 855 + // If possible, flush the error to the wire. 856 + f, ok := resp.(http.Flusher) 857 + if ok { 858 + f.Flush() 859 + } 860 + 861 + panic(r) 862 + } 863 + } 864 + 865 + // errFromPanic returns the typed error if the recovered panic is an error, otherwise formats as error. 866 + func errFromPanic(p interface{}) error { 867 + if err, ok := p.(error); ok { 868 + return err 869 + } 870 + return fmt.Errorf("panic: %v", p) 871 + } 872 + 873 + // internalWithCause is a Twirp Internal error wrapping an original error cause, 874 + // but the original error message is not exposed on Msg(). The original error 875 + // can be checked with go1.13+ errors.Is/As, and also by (github.com/pkg/errors).Unwrap 876 + type internalWithCause struct { 877 + msg string 878 + cause error 879 + } 880 + 881 + func (e *internalWithCause) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As 882 + func (e *internalWithCause) Cause() error { return e.cause } // for github.com/pkg/errors 883 + func (e *internalWithCause) Error() string { return e.msg + ": " + e.cause.Error() } 884 + func (e *internalWithCause) Code() twirp.ErrorCode { return twirp.Internal } 885 + func (e *internalWithCause) Msg() string { return e.msg } 886 + func (e *internalWithCause) Meta(key string) string { return "" } 887 + func (e *internalWithCause) MetaMap() map[string]string { return nil } 888 + func (e *internalWithCause) WithMeta(key string, val string) twirp.Error { return e } 889 + 890 + // malformedRequestError is used when the twirp server cannot unmarshal a request 891 + func malformedRequestError(msg string) twirp.Error { 892 + return twirp.NewError(twirp.Malformed, msg) 893 + } 894 + 895 + // badRouteError is used when the twirp server cannot route a request 896 + func badRouteError(msg string, method, url string) twirp.Error { 897 + err := twirp.NewError(twirp.BadRoute, msg) 898 + err = err.WithMeta("twirp_invalid_route", method+" "+url) 899 + return err 900 + } 901 + 902 + // withoutRedirects makes sure that the POST request can not be redirected. 903 + // The standard library will, by default, redirect requests (including POSTs) if it gets a 302 or 904 + // 303 response, and also 301s in go1.8. It redirects by making a second request, changing the 905 + // method to GET and removing the body. This produces very confusing error messages, so instead we 906 + // set a redirect policy that always errors. This stops Go from executing the redirect. 907 + // 908 + // We have to be a little careful in case the user-provided http.Client has its own CheckRedirect 909 + // policy - if so, we'll run through that policy first. 910 + // 911 + // Because this requires modifying the http.Client, we make a new copy of the client and return it. 912 + func withoutRedirects(in *http.Client) *http.Client { 913 + copy := *in 914 + copy.CheckRedirect = func(req *http.Request, via []*http.Request) error { 915 + if in.CheckRedirect != nil { 916 + // Run the input's redirect if it exists, in case it has side effects, but ignore any error it 917 + // returns, since we want to use ErrUseLastResponse. 918 + err := in.CheckRedirect(req, via) 919 + _ = err // Silly, but this makes sure generated code passes errcheck -blank, which some people use. 920 + } 921 + return http.ErrUseLastResponse 922 + } 923 + return &copy 924 + } 925 + 926 + // doProtobufRequest makes a Protobuf request to the remote Twirp service. 927 + func doProtobufRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { 928 + reqBodyBytes, err := proto.Marshal(in) 929 + if err != nil { 930 + return ctx, wrapInternal(err, "failed to marshal proto request") 931 + } 932 + reqBody := bytes.NewBuffer(reqBodyBytes) 933 + if err = ctx.Err(); err != nil { 934 + return ctx, wrapInternal(err, "aborted because context was done") 935 + } 936 + 937 + req, err := newRequest(ctx, url, reqBody, "application/protobuf") 938 + if err != nil { 939 + return ctx, wrapInternal(err, "could not build request") 940 + } 941 + ctx, err = callClientRequestPrepared(ctx, hooks, req) 942 + if err != nil { 943 + return ctx, err 944 + } 945 + 946 + req = req.WithContext(ctx) 947 + resp, err := client.Do(req) 948 + if err != nil { 949 + return ctx, wrapInternal(err, "failed to do request") 950 + } 951 + defer func() { _ = resp.Body.Close() }() 952 + 953 + if err = ctx.Err(); err != nil { 954 + return ctx, wrapInternal(err, "aborted because context was done") 955 + } 956 + 957 + if resp.StatusCode != 200 { 958 + return ctx, errorFromResponse(resp) 959 + } 960 + 961 + respBodyBytes, err := io.ReadAll(resp.Body) 962 + if err != nil { 963 + return ctx, wrapInternal(err, "failed to read response body") 964 + } 965 + if err = ctx.Err(); err != nil { 966 + return ctx, wrapInternal(err, "aborted because context was done") 967 + } 968 + 969 + if err = proto.Unmarshal(respBodyBytes, out); err != nil { 970 + return ctx, wrapInternal(err, "failed to unmarshal proto response") 971 + } 972 + return ctx, nil 973 + } 974 + 975 + // doJSONRequest makes a JSON request to the remote Twirp service. 976 + func doJSONRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { 977 + marshaler := &protojson.MarshalOptions{UseProtoNames: true} 978 + reqBytes, err := marshaler.Marshal(in) 979 + if err != nil { 980 + return ctx, wrapInternal(err, "failed to marshal json request") 981 + } 982 + if err = ctx.Err(); err != nil { 983 + return ctx, wrapInternal(err, "aborted because context was done") 984 + } 985 + 986 + req, err := newRequest(ctx, url, bytes.NewReader(reqBytes), "application/json") 987 + if err != nil { 988 + return ctx, wrapInternal(err, "could not build request") 989 + } 990 + ctx, err = callClientRequestPrepared(ctx, hooks, req) 991 + if err != nil { 992 + return ctx, err 993 + } 994 + 995 + req = req.WithContext(ctx) 996 + resp, err := client.Do(req) 997 + if err != nil { 998 + return ctx, wrapInternal(err, "failed to do request") 999 + } 1000 + 1001 + defer func() { 1002 + cerr := resp.Body.Close() 1003 + if err == nil && cerr != nil { 1004 + err = wrapInternal(cerr, "failed to close response body") 1005 + } 1006 + }() 1007 + 1008 + if err = ctx.Err(); err != nil { 1009 + return ctx, wrapInternal(err, "aborted because context was done") 1010 + } 1011 + 1012 + if resp.StatusCode != 200 { 1013 + return ctx, errorFromResponse(resp) 1014 + } 1015 + 1016 + d := json.NewDecoder(resp.Body) 1017 + rawRespBody := json.RawMessage{} 1018 + if err := d.Decode(&rawRespBody); err != nil { 1019 + return ctx, wrapInternal(err, "failed to unmarshal json response") 1020 + } 1021 + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} 1022 + if err = unmarshaler.Unmarshal(rawRespBody, out); err != nil { 1023 + return ctx, wrapInternal(err, "failed to unmarshal json response") 1024 + } 1025 + if err = ctx.Err(); err != nil { 1026 + return ctx, wrapInternal(err, "aborted because context was done") 1027 + } 1028 + return ctx, nil 1029 + } 1030 + 1031 + // Call twirp.ServerHooks.RequestReceived if the hook is available 1032 + func callRequestReceived(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { 1033 + if h == nil || h.RequestReceived == nil { 1034 + return ctx, nil 1035 + } 1036 + return h.RequestReceived(ctx) 1037 + } 1038 + 1039 + // Call twirp.ServerHooks.RequestRouted if the hook is available 1040 + func callRequestRouted(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { 1041 + if h == nil || h.RequestRouted == nil { 1042 + return ctx, nil 1043 + } 1044 + return h.RequestRouted(ctx) 1045 + } 1046 + 1047 + // Call twirp.ServerHooks.ResponsePrepared if the hook is available 1048 + func callResponsePrepared(ctx context.Context, h *twirp.ServerHooks) context.Context { 1049 + if h == nil || h.ResponsePrepared == nil { 1050 + return ctx 1051 + } 1052 + return h.ResponsePrepared(ctx) 1053 + } 1054 + 1055 + // Call twirp.ServerHooks.ResponseSent if the hook is available 1056 + func callResponseSent(ctx context.Context, h *twirp.ServerHooks) { 1057 + if h == nil || h.ResponseSent == nil { 1058 + return 1059 + } 1060 + h.ResponseSent(ctx) 1061 + } 1062 + 1063 + // Call twirp.ServerHooks.Error if the hook is available 1064 + func callError(ctx context.Context, h *twirp.ServerHooks, err twirp.Error) context.Context { 1065 + if h == nil || h.Error == nil { 1066 + return ctx 1067 + } 1068 + return h.Error(ctx, err) 1069 + } 1070 + 1071 + func callClientResponseReceived(ctx context.Context, h *twirp.ClientHooks) { 1072 + if h == nil || h.ResponseReceived == nil { 1073 + return 1074 + } 1075 + h.ResponseReceived(ctx) 1076 + } 1077 + 1078 + func callClientRequestPrepared(ctx context.Context, h *twirp.ClientHooks, req *http.Request) (context.Context, error) { 1079 + if h == nil || h.RequestPrepared == nil { 1080 + return ctx, nil 1081 + } 1082 + return h.RequestPrepared(ctx, req) 1083 + } 1084 + 1085 + func callClientError(ctx context.Context, h *twirp.ClientHooks, err twirp.Error) { 1086 + if h == nil || h.Error == nil { 1087 + return 1088 + } 1089 + h.Error(ctx, err) 1090 + } 1091 + 1092 + var twirpFileDescriptor0 = []byte{ 1093 + // 174 bytes of a gzipped FileDescriptorProto 1094 + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xce, 0xcd, 0xcc, 0xcd, 1095 + 0xd4, 0x4d, 0xcc, 0xcb, 0xcb, 0x2f, 0xcd, 0x4b, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 1096 + 0x92, 0x2b, 0xcf, 0x2c, 0xc9, 0xc8, 0xcc, 0xd3, 0x2b, 0x4f, 0x4d, 0x2a, 0xce, 0x2c, 0x49, 0xd5, 1097 + 0xab, 0xd0, 0x03, 0xa9, 0xd2, 0x83, 0xa9, 0x92, 0x92, 0x4e, 0xcf, 0xcf, 0x4f, 0xcf, 0x49, 0xd5, 1098 + 0x07, 0xab, 0x4e, 0x2a, 0x4d, 0xd3, 0x4f, 0xcd, 0x2d, 0x28, 0xa9, 0x84, 0x68, 0x96, 0xe2, 0x07, 1099 + 0x53, 0x69, 0xa9, 0xa9, 0x29, 0x10, 0x01, 0x23, 0x47, 0x2e, 0x0e, 0x47, 0xa8, 0x4e, 0x21, 0x53, 1100 + 0x24, 0x36, 0xbf, 0x1e, 0x42, 0xa5, 0x67, 0x49, 0x6a, 0xae, 0x94, 0x98, 0x1e, 0xc4, 0x5c, 0x3d, 1101 + 0x98, 0xb9, 0x7a, 0xae, 0x20, 0x73, 0x95, 0x18, 0x9c, 0x34, 0xa3, 0xd4, 0x2b, 0x52, 0x33, 0x13, 1102 + 0x8b, 0xf3, 0xf5, 0xf2, 0x52, 0x4b, 0xf4, 0xcb, 0x4c, 0xf4, 0x0b, 0x92, 0xf4, 0x53, 0x2b, 0x4a, 1103 + 0x52, 0x8b, 0xf2, 0x12, 0x73, 0xf4, 0x41, 0x6e, 0xd3, 0x87, 0xb9, 0x2d, 0x89, 0x0d, 0xac, 0xd9, 1104 + 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x1b, 0xb2, 0x3f, 0xf2, 0xd9, 0x00, 0x00, 0x00, 1105 + }
+1 -1
pb/external/protofeed/protofeed.pb.go
··· 1 1 // Code generated by protoc-gen-go. DO NOT EDIT. 2 2 // versions: 3 3 // protoc-gen-go v1.32.0 4 - // protoc v4.24.4 4 + // protoc v4.25.3 5 5 // source: protofeed.proto 6 6 7 7 package protofeed