1package service23import (4 "bytes"5 "database/sql"6 "embed"7 "errors"8 "fmt"9 "html/template"10 "io"11 "io/fs"12 "net/http"13 "path/filepath"14 "slices"15 "strconv"1617 "git.lin.moe/go/mlisting/config"18 "git.lin.moe/go/mlisting/storage"19 humanize "github.com/dustin/go-humanize"20 mbox "github.com/emersion/go-mbox"21)2223//go:embed templates/http24var httpFiles embed.FS2526type HTTPMux struct {27 *http.ServeMux28 storage storage.Storage29 logger config.Logger3031 templates map[string]*template.Template32}3334func NewHTTPMux(cfg *config.Config, st storage.Storage, l config.Logger) *HTTPMux {35 httpMux := http.NewServeMux()36 mux := new(HTTPMux)37 mux.storage = st38 mux.logger = l39 mux.ServeMux = httpMux4041 httpMux.HandleFunc(cfg.HttpRealUrl("/list/{list_addr}/"), mux.listIndex)42 httpMux.HandleFunc(cfg.HttpRealUrl("/list/{list_addr}/{msg_id}/"), mux.msgThread)43 httpMux.HandleFunc(cfg.HttpRealUrl("/list/{list_addr}/{msg_id}/mbox/"), mux.msgMbox)4445 cssFS, _ := fs.Sub(httpFiles, "templates/http")46 httpMux.Handle(cfg.HttpRealUrl("/css/"), http.StripPrefix(cfg.HttpRealUrl("/"), http.FileServer(http.FS(cssFS))))47 httpMux.HandleFunc(cfg.HttpRealUrl("/list/"), mux.lists)48 httpMux.HandleFunc(cfg.HttpRealUrl("/"), mux.index)4950 var tmpl_map = map[string]string{51 "listIndex": "list_index.tpl",52 "msgThread": "msg_thread.tpl",53 "lists": "lists.tpl",54 }5556 mux.templates = make(map[string]*template.Template)57 var t_funcmap = template.FuncMap{58 "timediff": humanize.Time,59 "realurl": cfg.HttpRealUrl,60 "addr_subscribe": func(addr string) string {61 return (&Address{Address: addr}).WithAction(CMD_SUBSCRIBE).String()62 },63 }6465 var err error66 for k, tpl := range tmpl_map {67 mux.templates[k], err = template.New(tpl).68 Funcs(t_funcmap).69 ParseFS(httpFiles, fmt.Sprintf("templates/http/%s", tpl))70 if err != nil {71 panic(err)72 }73 }7475 mux.templates["index"], err =76 template.New(filepath.Base(cfg.Http.IndexPage)).77 Funcs(t_funcmap).78 ParseFiles(cfg.Http.IndexPage)79 if err != nil {80 panic(err)81 }8283 return mux84}8586func (m *HTTPMux) msgThread(w http.ResponseWriter, r *http.Request) {87 var (88 ctx = r.Context()89 addr = r.PathValue("list_addr")90 msgid = r.PathValue("msg_id")91 l = m.logger92 st = m.storage93 )9495 list, err := st.GetList(ctx, addr)96 if err != nil {97 if errors.Is(err, sql.ErrNoRows) {98 l.Debug("not found list", "list", addr)99 http.NotFound(w, r)100 return101 }102 l.Error("failed to load list",103 "err", err,104 "list", addr)105106 code := http.StatusInternalServerError107 http.Error(w, http.StatusText(code), code)108 return109 }110 if list.DefaultPerm()&storage.PERM_BROWSE == 0 {111 code := http.StatusUnauthorized112 http.Error(w, http.StatusText(code), code)113 return114 }115116 msg, err := list.Message(ctx, msgid)117 if err != nil {118 if errors.Is(err, sql.ErrNoRows) {119 l.Debug("not found message", "list", addr)120 http.NotFound(w, r)121 return122 }123 l.Error("failed to load message",124 "err", err,125 "message-id", msgid,126 "list", addr)127128 code := http.StatusInternalServerError129 http.Error(w, http.StatusText(code), code)130 return131 }132 submsgs, err := msg.SubMessages(ctx, true)133 if err != nil && !errors.Is(err, sql.ErrNoRows) {134 l.Error("failed to load sub message message",135 "err", err,136 "message-id", msgid,137 "list", addr)138139 code := http.StatusInternalServerError140 http.Error(w, http.StatusText(code), code)141 return142 }143 slices.Reverse(submsgs)144 m.templates["msgThread"].Execute(w, struct {145 storage.Message146 List storage.List147 Parent storage.Message148 SubMessages []storage.Message149 }{msg, list, msg.Parent(r.Context()), submsgs})150}151152func (m *HTTPMux) msgMbox(w http.ResponseWriter, r *http.Request) {153 var (154 ctx = r.Context()155 addr = r.PathValue("list_addr")156 msgid = r.PathValue("msg_id")157 l = m.logger158 st = m.storage159 )160161 list, err := st.GetList(ctx, addr)162 if err != nil {163 if errors.Is(err, sql.ErrNoRows) {164 l.Debug("not found list", "list", addr)165 http.NotFound(w, r)166 return167 }168 l.Error("failed to load list",169 "err", err,170 "list", addr)171172 code := http.StatusInternalServerError173 http.Error(w, http.StatusText(code), code)174 return175 }176 if list.DefaultPerm()&storage.PERM_BROWSE == 0 {177 code := http.StatusUnauthorized178 http.Error(w, http.StatusText(code), code)179 return180 }181 msg, err := list.Message(ctx, msgid)182 if err != nil {183 if errors.Is(err, sql.ErrNoRows) {184 l.Debug("not found message", "list", addr)185 http.NotFound(w, r)186 return187 }188 l.Error("failed to load message",189 "err", err,190 "message-id", msgid,191 "list", addr)192193 code := http.StatusInternalServerError194 http.Error(w, http.StatusText(code), code)195 return196 }197 submsgs, err := msg.SubMessages(ctx, true)198 if err != nil && !errors.Is(err, sql.ErrNoRows) {199 l.Error("failed to load sub message message",200 "err", err,201 "message-id", msgid,202 "list", addr)203204 code := http.StatusInternalServerError205 http.Error(w, http.StatusText(code), code)206 return207 }208 msgs := append(submsgs, msg)209 slices.Reverse(msgs)210211 respbuf := bytes.NewBuffer(nil)212 mbox_writer := mbox.NewWriter(respbuf)213 defer mbox_writer.Close()214 for _, msg := range msgs {215 var buf = bytes.NewBuffer(nil)216 entity := msg.Entity()217 entity.WriteTo(buf)218219 msg_writer, err := mbox_writer.CreateMessage(msg.Header().Get("From"), msg.CreateAt())220 if err != nil {221 l.Error("create mbox mseesage writer error", "err", err, "parent-msgid", msgid)222 code := http.StatusInternalServerError223 http.Error(w, http.StatusText(code), code)224 return225 }226 if _, err := io.Copy(msg_writer, buf); err != nil {227 l.Error("write mbox messsage failed", "err", err, "parent-msgid", msgid)228 code := http.StatusInternalServerError229 http.Error(w, http.StatusText(code), code)230 return231 }232 }233 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.mbox\"", msgid))234 w.Header().Set("Content-Type", "application/mbox")235 w.WriteHeader(http.StatusOK)236 io.Copy(w, respbuf)237}238239func (m *HTTPMux) listIndex(w http.ResponseWriter, r *http.Request) {240 const COUNT_PER_PAGE = 50241 var (242 ctx = r.Context()243 addr = r.PathValue("list_addr")244 pageStr = r.URL.Query().Get("p")245 search = r.URL.Query().Get("s")246 l = m.logger247 st = m.storage248 )249 page, _ := strconv.Atoi(pageStr)250 list, err := st.GetList(ctx, addr)251 if err != nil {252 if errors.Is(err, sql.ErrNoRows) {253 l.Debug("not found list", "list", addr)254 http.NotFound(w, r)255 return256 }257 l.Error("failed to load list",258 "err", err,259 "list", addr)260261 code := http.StatusInternalServerError262 http.Error(w, http.StatusText(code), code)263 return264 }265 if list.DefaultPerm()&storage.PERM_BROWSE == 0 {266 code := http.StatusUnauthorized267 http.Error(w, http.StatusText(code), code)268 return269 }270271 isheader := (search == "")272 msgs, total, err := list.Messages(ctx, isheader, search, uint(page)*COUNT_PER_PAGE, COUNT_PER_PAGE)273 if err != nil && !errors.Is(err, sql.ErrNoRows) {274 l.Error("load messages error",275 "err", err,276 "list", addr)277 code := http.StatusInternalServerError278 http.Error(w, http.StatusText(code), code)279 return280 }281 nextP := page + 1282 if total <= int64(page+1)*COUNT_PER_PAGE {283 nextP = -1284 }285 prevP := page - 1286 if prevP < 0 {287 prevP = -1288 }289290 m.templates["listIndex"].Execute(w, struct {291 storage.List292 Messages []storage.Message293 Search string294 NextPage int295 PrevPage int296 }{list, msgs, search, nextP, prevP})297}298299func (m *HTTPMux) lists(w http.ResponseWriter, r *http.Request) {300 var (301 ctx = r.Context()302 st = m.storage303 l = m.logger304 )305 lists, err := st.Lists(ctx)306 if err != nil && !errors.Is(err, sql.ErrNoRows) {307 l.Error("load lists error", "err", err)308 }309 public_lists := []storage.List{}310 for _, list := range lists {311 if list.DefaultPerm()&storage.PERM_BROWSE == 0 {312 continue313 }314 public_lists = append(public_lists, list)315 }316 m.templates["lists"].Execute(w, struct {317 Lists []storage.List318 }{public_lists})319}320321func (m *HTTPMux) index(w http.ResponseWriter, r *http.Request) {322 var (323 ctx = r.Context()324 st = m.storage325 l = m.logger326 )327328 lists, err := st.Lists(ctx)329 if err != nil && !errors.Is(err, sql.ErrNoRows) {330 l.Error("load lists error", "err", err)331 }332 public_lists := []storage.List{}333 for _, list := range lists {334 if list.DefaultPerm()&storage.PERM_BROWSE == 0 {335 continue336 }337 public_lists = append(public_lists, list)338 }339340 m.templates["index"].Execute(w, struct {341 Lists []storage.List342 }{public_lists})343}