2021-04-11 21:46:06 +00:00
|
|
|
package templates
|
|
|
|
|
2021-04-22 23:02:50 +00:00
|
|
|
import (
|
2021-08-16 04:40:56 +00:00
|
|
|
"fmt"
|
2021-05-04 01:59:45 +00:00
|
|
|
"html/template"
|
2021-05-06 05:57:14 +00:00
|
|
|
"net"
|
2021-06-22 09:50:40 +00:00
|
|
|
"regexp"
|
2021-06-22 17:08:05 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2021-05-04 01:59:45 +00:00
|
|
|
|
2021-04-22 23:02:50 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
|
|
|
"git.handmade.network/hmn/hmn/src/models"
|
|
|
|
)
|
2021-04-11 21:46:06 +00:00
|
|
|
|
2021-05-25 13:12:20 +00:00
|
|
|
func PostToTemplate(p *models.Post, author *models.User, currentTheme string) Post {
|
2021-04-11 21:46:06 +00:00
|
|
|
return Post{
|
2021-05-06 05:57:14 +00:00
|
|
|
ID: p.ID,
|
|
|
|
|
2021-07-30 19:59:48 +00:00
|
|
|
// Urls not set here. They vary per thread type. Set 'em yourself!
|
2021-05-04 01:59:45 +00:00
|
|
|
|
2021-04-11 21:46:06 +00:00
|
|
|
Preview: p.Preview,
|
|
|
|
ReadOnly: p.ReadOnly,
|
2021-04-23 04:07:44 +00:00
|
|
|
|
2021-07-22 02:26:28 +00:00
|
|
|
Author: UserToTemplate(author, currentTheme),
|
2021-05-06 05:57:14 +00:00
|
|
|
// No content. A lot of the time we don't have this handy and don't need it. See AddContentVersion.
|
2021-05-04 01:59:45 +00:00
|
|
|
PostDate: p.PostDate,
|
2021-04-11 21:46:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-04 21:24:48 +00:00
|
|
|
func (p *Post) AddContentVersion(ver models.PostVersion, editor *models.User) {
|
2021-05-06 05:57:14 +00:00
|
|
|
p.Content = template.HTML(ver.TextParsed)
|
2021-07-04 21:24:48 +00:00
|
|
|
p.IP = maybeIp(ver.IP)
|
2021-04-23 04:07:44 +00:00
|
|
|
|
2021-05-06 05:57:14 +00:00
|
|
|
if editor != nil {
|
2021-07-04 21:24:48 +00:00
|
|
|
editorTmpl := UserToTemplate(editor, "theme not required here")
|
2021-05-06 05:57:14 +00:00
|
|
|
p.Editor = &editorTmpl
|
2021-07-04 21:24:48 +00:00
|
|
|
p.EditDate = ver.Date
|
2021-05-06 05:57:14 +00:00
|
|
|
p.EditReason = ver.EditReason
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-06 23:48:43 +00:00
|
|
|
var LifecycleBadgeClasses = map[models.ProjectLifecycle]string{
|
|
|
|
models.ProjectLifecycleUnapproved: "",
|
|
|
|
models.ProjectLifecycleApprovalRequired: "",
|
|
|
|
models.ProjectLifecycleActive: "",
|
|
|
|
models.ProjectLifecycleHiatus: "notice-hiatus",
|
|
|
|
models.ProjectLifecycleDead: "notice-dead",
|
|
|
|
models.ProjectLifecycleLTSRequired: "",
|
|
|
|
models.ProjectLifecycleLTS: "notice-lts",
|
|
|
|
}
|
|
|
|
|
|
|
|
var LifecycleBadgeStrings = map[models.ProjectLifecycle]string{
|
|
|
|
models.ProjectLifecycleUnapproved: "",
|
|
|
|
models.ProjectLifecycleApprovalRequired: "",
|
|
|
|
models.ProjectLifecycleActive: "",
|
|
|
|
models.ProjectLifecycleHiatus: "On Hiatus",
|
|
|
|
models.ProjectLifecycleDead: "Dead",
|
|
|
|
models.ProjectLifecycleLTSRequired: "",
|
|
|
|
models.ProjectLifecycleLTS: "Complete",
|
|
|
|
}
|
|
|
|
|
2021-11-10 04:11:39 +00:00
|
|
|
func ProjectToTemplate(p *models.Project, url string, theme string) Project {
|
2021-06-22 09:50:40 +00:00
|
|
|
logo := p.LogoLight
|
|
|
|
if theme == "dark" {
|
|
|
|
logo = p.LogoDark
|
|
|
|
}
|
2021-04-11 21:46:06 +00:00
|
|
|
return Project{
|
2021-06-06 23:48:43 +00:00
|
|
|
Name: p.Name,
|
|
|
|
Subdomain: p.Subdomain(),
|
|
|
|
Color1: p.Color1,
|
|
|
|
Color2: p.Color2,
|
|
|
|
Url: url,
|
|
|
|
Blurb: p.Blurb,
|
|
|
|
ParsedDescription: template.HTML(p.ParsedDescription),
|
|
|
|
|
|
|
|
Logo: hmnurl.BuildUserFile(logo),
|
|
|
|
|
|
|
|
LifecycleBadgeClass: LifecycleBadgeClasses[p.Lifecycle],
|
|
|
|
LifecycleString: LifecycleBadgeStrings[p.Lifecycle],
|
2021-04-11 21:46:06 +00:00
|
|
|
|
|
|
|
IsHMN: p.IsHMN(),
|
|
|
|
|
2021-11-10 17:13:56 +00:00
|
|
|
HasBlog: p.HasBlog(),
|
|
|
|
HasForum: p.HasForums(),
|
2021-05-31 23:23:04 +00:00
|
|
|
|
|
|
|
DateApproved: p.DateApproved,
|
2021-04-11 21:46:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-12 03:51:07 +00:00
|
|
|
func SessionToTemplate(s *models.Session) Session {
|
|
|
|
return Session{
|
|
|
|
CSRFToken: s.CSRFToken,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-04 01:59:45 +00:00
|
|
|
func ThreadToTemplate(t *models.Thread) Thread {
|
|
|
|
return Thread{
|
|
|
|
Title: t.Title,
|
|
|
|
Locked: t.Locked,
|
|
|
|
Sticky: t.Sticky,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-22 09:50:40 +00:00
|
|
|
func UserAvatarUrl(u *models.User, currentTheme string) string {
|
2021-05-31 23:23:04 +00:00
|
|
|
if currentTheme == "" {
|
|
|
|
currentTheme = "light"
|
|
|
|
}
|
2021-04-22 23:02:50 +00:00
|
|
|
avatar := ""
|
2021-07-22 02:26:28 +00:00
|
|
|
if u != nil && u.Avatar != nil && len(*u.Avatar) > 0 {
|
2021-05-31 23:23:04 +00:00
|
|
|
avatar = hmnurl.BuildUserFile(*u.Avatar)
|
2021-05-25 13:12:20 +00:00
|
|
|
} else {
|
|
|
|
avatar = hmnurl.BuildTheme("empty-avatar.svg", currentTheme, true)
|
2021-04-22 23:02:50 +00:00
|
|
|
}
|
2021-06-22 09:50:40 +00:00
|
|
|
return avatar
|
|
|
|
}
|
2021-04-22 23:02:50 +00:00
|
|
|
|
2021-06-22 09:50:40 +00:00
|
|
|
func UserToTemplate(u *models.User, currentTheme string) User {
|
2021-07-22 02:26:28 +00:00
|
|
|
if u == nil {
|
|
|
|
return User{
|
|
|
|
Name: "Deleted user",
|
|
|
|
AvatarUrl: UserAvatarUrl(u, currentTheme),
|
|
|
|
}
|
|
|
|
}
|
2021-06-22 09:50:40 +00:00
|
|
|
|
|
|
|
email := ""
|
|
|
|
if u.ShowEmail {
|
|
|
|
// TODO(asaf): Always show email to admins
|
|
|
|
email = u.Email
|
|
|
|
}
|
2021-04-22 23:02:50 +00:00
|
|
|
|
2021-04-11 21:46:06 +00:00
|
|
|
return User{
|
2021-07-22 02:16:10 +00:00
|
|
|
ID: u.ID,
|
|
|
|
Username: u.Username,
|
|
|
|
Email: email,
|
|
|
|
IsStaff: u.IsStaff,
|
2021-04-17 00:01:13 +00:00
|
|
|
|
2021-08-17 05:18:04 +00:00
|
|
|
Name: u.BestName(),
|
2021-08-27 17:58:52 +00:00
|
|
|
Bio: u.Bio,
|
2021-04-22 23:02:50 +00:00
|
|
|
Blurb: u.Blurb,
|
|
|
|
Signature: u.Signature,
|
2021-06-22 09:50:40 +00:00
|
|
|
DateJoined: u.DateJoined,
|
|
|
|
AvatarUrl: UserAvatarUrl(u, currentTheme),
|
|
|
|
ProfileUrl: hmnurl.BuildUserProfile(u.Username),
|
2021-04-17 00:01:13 +00:00
|
|
|
|
2021-08-08 20:05:52 +00:00
|
|
|
DarkTheme: u.DarkTheme,
|
|
|
|
Timezone: u.Timezone,
|
2021-04-17 00:01:13 +00:00
|
|
|
|
|
|
|
CanEditLibrary: u.CanEditLibrary,
|
|
|
|
DiscordSaveShowcase: u.DiscordSaveShowcase,
|
|
|
|
DiscordDeleteSnippetOnMessageDelete: u.DiscordDeleteSnippetOnMessageDelete,
|
2021-04-11 21:46:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-27 17:58:52 +00:00
|
|
|
// An online site/service for which we recognize the link
|
|
|
|
type LinkService struct {
|
|
|
|
Name string
|
|
|
|
IconName string
|
|
|
|
Regex *regexp.Regexp
|
2021-06-22 09:50:40 +00:00
|
|
|
}
|
|
|
|
|
2021-08-27 17:58:52 +00:00
|
|
|
var LinkServices = []LinkService{
|
|
|
|
{
|
|
|
|
Name: "YouTube",
|
|
|
|
IconName: "youtube",
|
|
|
|
Regex: regexp.MustCompile(`youtube\.com/(c/)?(?P<userdata>[\w/-]+)$`),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "Twitter",
|
|
|
|
IconName: "twitter",
|
|
|
|
Regex: regexp.MustCompile(`twitter\.com/(?P<userdata>\w+)$`),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "GitHub",
|
|
|
|
IconName: "github",
|
|
|
|
Regex: regexp.MustCompile(`github\.com/(?P<userdata>[\w/-]+)$`),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "Twitch",
|
|
|
|
IconName: "twitch",
|
|
|
|
Regex: regexp.MustCompile(`twitch\.tv/(?P<userdata>[\w/-]+)$`),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "Hitbox",
|
|
|
|
IconName: "hitbox",
|
|
|
|
Regex: regexp.MustCompile(`hitbox\.tv/(?P<userdata>[\w/-]+)$`),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "Patreon",
|
|
|
|
IconName: "patreon",
|
|
|
|
Regex: regexp.MustCompile(`patreon\.com/(?P<userdata>[\w/-]+)$`),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "SoundCloud",
|
|
|
|
IconName: "soundcloud",
|
|
|
|
Regex: regexp.MustCompile(`soundcloud\.com/(?P<userdata>[\w/-]+)$`),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "itch.io",
|
|
|
|
IconName: "itch",
|
|
|
|
Regex: regexp.MustCompile(`(?P<userdata>[\w/-]+)\.itch\.io/?$`),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
func ParseKnownServicesForLink(link *models.Link) (service LinkService, userData string) {
|
|
|
|
for _, svc := range LinkServices {
|
|
|
|
match := svc.Regex.FindStringSubmatch(link.URL)
|
2021-06-22 09:50:40 +00:00
|
|
|
if match != nil {
|
2021-08-27 17:58:52 +00:00
|
|
|
return svc, match[svc.Regex.SubexpIndex("userdata")]
|
2021-06-22 09:50:40 +00:00
|
|
|
}
|
|
|
|
}
|
2021-08-27 17:58:52 +00:00
|
|
|
return LinkService{}, ""
|
2021-06-22 09:50:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func LinkToTemplate(link *models.Link) Link {
|
2021-08-27 17:58:52 +00:00
|
|
|
tlink := Link{
|
|
|
|
Name: link.Name,
|
|
|
|
Url: link.URL,
|
|
|
|
LinkText: link.URL,
|
2021-07-08 07:40:30 +00:00
|
|
|
}
|
2021-08-27 17:58:52 +00:00
|
|
|
|
|
|
|
service, userData := ParseKnownServicesForLink(link)
|
|
|
|
if tlink.Name == "" && service.Name != "" {
|
|
|
|
tlink.Name = service.Name
|
|
|
|
}
|
|
|
|
if service.IconName != "" {
|
|
|
|
tlink.Icon = service.IconName
|
2021-07-08 07:40:30 +00:00
|
|
|
}
|
2021-08-27 17:58:52 +00:00
|
|
|
if userData != "" {
|
|
|
|
tlink.LinkText = userData
|
2021-06-22 09:50:40 +00:00
|
|
|
}
|
2021-08-27 17:58:52 +00:00
|
|
|
|
|
|
|
return tlink
|
2021-06-22 09:50:40 +00:00
|
|
|
}
|
|
|
|
|
2021-06-22 17:08:05 +00:00
|
|
|
func TimelineItemsToJSON(items []TimelineItem) string {
|
|
|
|
// NOTE(asaf): As of 2021-06-22: This only serializes the data necessary for snippet showcase.
|
|
|
|
builder := strings.Builder{}
|
|
|
|
builder.WriteRune('[')
|
|
|
|
for i, item := range items {
|
2021-10-24 20:48:28 +00:00
|
|
|
|
2021-06-22 17:08:05 +00:00
|
|
|
if i > 0 {
|
|
|
|
builder.WriteRune(',')
|
|
|
|
}
|
|
|
|
builder.WriteRune('{')
|
|
|
|
|
|
|
|
builder.WriteString(`"date":`)
|
|
|
|
builder.WriteString(strconv.FormatInt(item.Date.UTC().Unix(), 10))
|
|
|
|
builder.WriteRune(',')
|
|
|
|
|
|
|
|
builder.WriteString(`"description":"`)
|
|
|
|
jsonString := string(item.Description)
|
|
|
|
jsonString = strings.ReplaceAll(jsonString, `\`, `\\`)
|
|
|
|
jsonString = strings.ReplaceAll(jsonString, `"`, `\"`)
|
|
|
|
jsonString = strings.ReplaceAll(jsonString, "\n", "\\n")
|
|
|
|
jsonString = strings.ReplaceAll(jsonString, "\r", "\\r")
|
|
|
|
jsonString = strings.ReplaceAll(jsonString, "\t", "\\t")
|
|
|
|
builder.WriteString(jsonString)
|
|
|
|
builder.WriteString(`",`)
|
|
|
|
|
|
|
|
builder.WriteString(`"owner_name":"`)
|
2021-08-28 17:07:45 +00:00
|
|
|
builder.WriteString(item.OwnerName)
|
2021-06-22 17:08:05 +00:00
|
|
|
builder.WriteString(`",`)
|
|
|
|
|
|
|
|
builder.WriteString(`"owner_avatar":"`)
|
|
|
|
builder.WriteString(item.OwnerAvatarUrl)
|
|
|
|
builder.WriteString(`",`)
|
|
|
|
|
|
|
|
builder.WriteString(`"owner_url":"`)
|
|
|
|
builder.WriteString(item.OwnerUrl)
|
|
|
|
builder.WriteString(`",`)
|
|
|
|
|
|
|
|
builder.WriteString(`"snippet_url":"`)
|
|
|
|
builder.WriteString(item.Url)
|
|
|
|
builder.WriteString(`",`)
|
|
|
|
|
2021-10-23 22:28:06 +00:00
|
|
|
var mediaType TimelineItemMediaType
|
|
|
|
var assetUrl string
|
|
|
|
var thumbnailUrl string
|
|
|
|
var width, height int
|
|
|
|
if len(item.EmbedMedia) > 0 {
|
|
|
|
mediaType = item.EmbedMedia[0].Type
|
|
|
|
assetUrl = item.EmbedMedia[0].AssetUrl
|
|
|
|
thumbnailUrl = item.EmbedMedia[0].ThumbnailUrl
|
|
|
|
width = item.EmbedMedia[0].Width
|
|
|
|
height = item.EmbedMedia[0].Height
|
|
|
|
}
|
|
|
|
|
|
|
|
builder.WriteString(`"media_type":`)
|
|
|
|
builder.WriteString(strconv.Itoa(int(mediaType)))
|
|
|
|
builder.WriteRune(',')
|
|
|
|
|
2021-06-22 17:08:05 +00:00
|
|
|
builder.WriteString(`"width":`)
|
2021-10-23 22:28:06 +00:00
|
|
|
builder.WriteString(strconv.Itoa(width))
|
2021-06-22 17:08:05 +00:00
|
|
|
builder.WriteRune(',')
|
|
|
|
|
|
|
|
builder.WriteString(`"height":`)
|
2021-10-23 22:28:06 +00:00
|
|
|
builder.WriteString(strconv.Itoa(height))
|
2021-06-22 17:08:05 +00:00
|
|
|
builder.WriteRune(',')
|
|
|
|
|
|
|
|
builder.WriteString(`"asset_url":"`)
|
2021-10-23 22:28:06 +00:00
|
|
|
builder.WriteString(assetUrl)
|
|
|
|
builder.WriteString(`",`)
|
|
|
|
|
|
|
|
builder.WriteString(`"thumbnail_url":"`)
|
|
|
|
builder.WriteString(thumbnailUrl)
|
2021-06-22 17:08:05 +00:00
|
|
|
builder.WriteString(`",`)
|
|
|
|
|
|
|
|
builder.WriteString(`"discord_message_url":"`)
|
|
|
|
builder.WriteString(item.DiscordMessageUrl)
|
|
|
|
builder.WriteString(`"`)
|
|
|
|
|
|
|
|
builder.WriteRune('}')
|
|
|
|
}
|
|
|
|
builder.WriteRune(']')
|
|
|
|
return builder.String()
|
|
|
|
}
|
|
|
|
|
2021-10-27 00:45:11 +00:00
|
|
|
func PodcastToTemplate(podcast *models.Podcast, imageFilename string) Podcast {
|
2021-07-23 03:09:46 +00:00
|
|
|
imageUrl := ""
|
|
|
|
if imageFilename != "" {
|
|
|
|
imageUrl = hmnurl.BuildUserFile(imageFilename)
|
|
|
|
}
|
|
|
|
return Podcast{
|
|
|
|
Title: podcast.Title,
|
|
|
|
Description: podcast.Description,
|
|
|
|
Language: podcast.Language,
|
|
|
|
ImageUrl: imageUrl,
|
2021-10-27 00:45:11 +00:00
|
|
|
Url: hmnurl.BuildPodcast(),
|
2021-07-23 03:09:46 +00:00
|
|
|
|
2021-10-27 00:45:11 +00:00
|
|
|
RSSUrl: hmnurl.BuildPodcastRSS(),
|
2021-07-23 03:09:46 +00:00
|
|
|
// TODO(asaf): Move this to the db if we want to support user podcasts
|
|
|
|
AppleUrl: "https://podcasts.apple.com/us/podcast/the-handmade-network-podcast/id1507790631",
|
|
|
|
GoogleUrl: "https://www.google.com/podcasts?feed=aHR0cHM6Ly9oYW5kbWFkZS5uZXR3b3JrL3BvZGNhc3QvcG9kY2FzdC54bWw%3D",
|
|
|
|
SpotifyUrl: "https://open.spotify.com/show/2Nd9NjXscrBbQwYULiYKiU",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-27 00:45:11 +00:00
|
|
|
func PodcastEpisodeToTemplate(episode *models.PodcastEpisode, audioFileSize int64, imageFilename string) PodcastEpisode {
|
2021-07-23 03:09:46 +00:00
|
|
|
imageUrl := ""
|
|
|
|
if imageFilename != "" {
|
|
|
|
imageUrl = hmnurl.BuildUserFile(imageFilename)
|
|
|
|
}
|
|
|
|
return PodcastEpisode{
|
|
|
|
GUID: episode.GUID.String(),
|
|
|
|
Title: episode.Title,
|
|
|
|
Description: episode.Description,
|
|
|
|
DescriptionHtml: template.HTML(episode.DescriptionHtml),
|
|
|
|
EpisodeNumber: episode.EpisodeNumber,
|
2021-10-27 00:45:11 +00:00
|
|
|
Url: hmnurl.BuildPodcastEpisode(episode.GUID.String()),
|
2021-07-23 03:09:46 +00:00
|
|
|
ImageUrl: imageUrl,
|
2021-10-27 00:45:11 +00:00
|
|
|
FileUrl: hmnurl.BuildPodcastEpisodeFile(episode.AudioFile),
|
2021-07-23 03:09:46 +00:00
|
|
|
FileSize: audioFileSize,
|
|
|
|
PublicationDate: episode.PublicationDate,
|
|
|
|
Duration: episode.Duration,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-16 04:40:56 +00:00
|
|
|
func DiscordUserToTemplate(d *models.DiscordUser) DiscordUser {
|
|
|
|
var avatarUrl string // TODO: Default avatar image
|
|
|
|
if d.Avatar != nil {
|
|
|
|
avatarUrl = fmt.Sprintf("https://cdn.discordapp.com/avatars/%s/%s.png", d.UserID, *d.Avatar)
|
|
|
|
}
|
|
|
|
|
|
|
|
return DiscordUser{
|
|
|
|
Username: d.Username,
|
|
|
|
Discriminator: d.Discriminator,
|
|
|
|
Avatar: avatarUrl,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-11 21:46:06 +00:00
|
|
|
func maybeString(s *string) string {
|
|
|
|
if s == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return *s
|
|
|
|
}
|
2021-05-06 05:57:14 +00:00
|
|
|
|
|
|
|
func maybeIp(ip *net.IPNet) string {
|
|
|
|
if ip == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return ip.String()
|
|
|
|
}
|