// Various utility functions used in Indigo. package main import ( // Internals "database/sql" "encoding/json" "fmt" "html/template" "io/ioutil" "math" "math/rand" "net" "net/http" "regexp" "strconv" "strings" "time" "unicode/utf8" // Externals "github.com/gorilla/csrf" "github.com/gorilla/websocket" sessions "github.com/kataras/go-sessions/v3" "github.com/microcosm-cc/bluemonday" "github.com/russross/blackfriday/v2" ) // Inititialize sessions and other variables. Used in almost every page that uses HTML, and even some that don't. func doSession(w http.ResponseWriter, r *http.Request) (user, bool) { session := sessions.Start(w, r) currentUser := user{} ip := getIP(r) timezone, err := r.Cookie("timezone") if err != nil || len(timezone.Value) == 0 { timezone = &http.Cookie{Name: "timezone", Value: getTimezone(ip), Expires: time.Now().Add(365 * 24 * time.Hour)} http.SetCookie(w, timezone) } currentUser.Timezone = timezone.Value host, _, _ := net.SplitHostPort(ip) cidr := getCIDR(host, "1") cidr2 := getCIDR(host, "2") var banLength time.Time db.QueryRow("SELECT until FROM bans WHERE (cidr = 0 AND ip = ?) OR (cidr = 1 AND ip = ?) OR (cidr = 2 AND ip = ?)", host, cidr, cidr2).Scan(&banLength) if int64(banLength.Unix()) != -62135596800 { success := showBan(w, currentUser, banLength) if success { return currentUser, false } } if len(session.GetString("username")) != 0 { currentUser = QueryUser(session.GetString("username"), currentUser.Timezone) if len(currentUser.Theme) > 0 { currentUser.ThemeColors = strings.Split(currentUser.Theme, ",") } currentUser.Avatar = getAvatar(currentUser.Avatar, currentUser.HasMii, 0) db.QueryRow("SELECT until FROM bans WHERE user = ?", currentUser.ID).Scan(&banLength) if int64(banLength.Unix()) != -62135596800 { success := showBan(w, currentUser, banLength) if success { return currentUser, false } } } else { indigoAuth, err := r.Cookie("indigo-auth") if err == nil && len(indigoAuth.Value) > 0 { var username string db.QueryRow("SELECT username FROM login_tokens LEFT JOIN users ON user = users.id WHERE value = ?", &indigoAuth.Value).Scan(&username) if len(username) > 0 { currentUser = QueryUser(username, currentUser.Timezone) if len(currentUser.Theme) > 0 { currentUser.ThemeColors = strings.Split(currentUser.Theme, ",") } currentUser.Avatar = getAvatar(currentUser.Avatar, currentUser.HasMii, 0) session.Set("username", currentUser.Username) session.Set("user_id", currentUser.ID) stmt, err := db.Prepare("INSERT INTO sessions (id, user) VALUES (?, ?)") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return currentUser, false } stmt.Exec(session.ID(), currentUser.ID) stmt.Close() db.QueryRow("SELECT until FROM bans WHERE user = ?", currentUser.ID).Scan(&banLength) if int64(banLength.Unix()) != -62135596800 { success := showBan(w, currentUser, banLength) if success { return currentUser, false } } } else { if settings.ForceLogins && r.URL.Path != "/reset" { http.Redirect(w, r, "/login", 301) return currentUser, false } return currentUser, true } } else { if settings.ForceLogins && r.URL.Path != "/reset" { http.Redirect(w, r, "/login", 301) return currentUser, false } return currentUser, true } } currentUser.CSRFToken = csrf.Token(r) currentUser.LightMode = getLightMode(w, r) if r.Header.Get("X-PJAX") == "" { var friendRequests int db.QueryRow("SELECT COUNT(*) FROM messages LEFT JOIN conversations ON conversation_id = conversations.id WHERE (source = ? OR target = ?) AND created_by <> ? AND msg_read = 0 AND messages.is_rm = 0 AND conversations.is_rm = 0", currentUser.ID, currentUser.ID, currentUser.ID).Scan(¤tUser.Notifications.Messages) var groupUnread int db.QueryRow("SELECT SUM(unread_messages) FROM group_members WHERE user = ?", currentUser.ID).Scan(&groupUnread) currentUser.Notifications.Messages += groupUnread db.QueryRow("SELECT COUNT(*) FROM notifications WHERE notif_to = ? AND merged IS NULL AND notif_read = 0", currentUser.ID).Scan(¤tUser.Notifications.Notifications) db.QueryRow("SELECT COUNT(*) FROM friend_requests WHERE request_to = ? AND request_read = 0", currentUser.ID).Scan(&friendRequests) currentUser.Notifications.Notifications += friendRequests } return currentUser, true } // Escape the "forbidden keywords" field for regexp. func escapeForbiddenKeywords(regex string) string { if len(regex) == 0 { return "b\bb" // This regex always returns "false", so no posts are ever filtered out if you don't have any reserved words. } regex = regexp.QuoteMeta(regex) split := strings.Split(regex, ",") fixed := []string{} for _, s := range split { if len(s) > 0 { fixed = append(fixed, s) } } regex = strings.Join(fixed, "|") regex = strings.Replace(regex, "\\|", ",", -1) return strings.Replace(regex, "\\\\", "\\", -1) } // Escape Markdown. func escapeMarkdown(text string) string { text = string(symbols.ReplaceAll([]byte(text), []byte("\\$1"))) return text } // Get a CIDR-esque range from an IP. func getCIDR(ip string, cidr string) string { netmasks := strings.Split(ip, ".") netmasks[3] = "0" if cidr == "2" { netmasks[2] = "0" } return strings.Join(netmasks, ".") } // Get the user's light mode status. func getLightMode(w http.ResponseWriter, r *http.Request) bool { lightMode, err := r.Cookie("light") if err != nil || len(lightMode.Value) == 0 { lightMode = &http.Cookie{Name: "light", Value: "false", Expires: time.Unix(253402300799, 0)} http.SetCookie(w, lightMode) } lightModeBool, err := strconv.ParseBool(lightMode.Value) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return false } return lightModeBool } // Get the data of a post's migration. func getPostMigration(migration int, migratedCommunity string) (string, string, string, string) { var migrationImage string var migrationURL string err := db.QueryRow("SELECT image, url FROM migrations WHERE id = ?", migration).Scan(&migrationImage, &migrationURL) if err != nil { fmt.Println("no migrations") fmt.Println(err.Error()) return "https://i.ytimg.com/vi/DkIVqD8pJt8/maxresdefault.jpg", "http://marios-princess-sex.ga/#", "This message should not appear. An error occurred while grabbing the migration data. Check the console.", "https://closed.pizza/s/img/title-icon-default.png" } var communityTitle string var communityIcon string err = db.QueryRow("SELECT title, icon FROM migrated_communities WHERE migrated_id = ? AND migration = ?", migratedCommunity, migration).Scan(&communityTitle, &communityIcon) if err != nil { return migrationImage, migrationURL, "Unknown Community", "/assets/img/title-icon-default.png" } return migrationImage, migrationURL, communityTitle, communityIcon } // Format timestamps in a way that normal people who AREN'T robots can read. func humanTiming(timestamp time.Time, timezone string) string { location, err := time.LoadLocation(timezone) if err != nil { fmt.Println(err.Error()) return err.Error() } timestamp = timestamp.In(location) since := time.Now().In(location).Sub(timestamp).Seconds() if since <= 1 { return "Less than a second ago" } else if since < 2 { return "1 second ago" } else if since < 60 { return strconv.Itoa(int(math.Floor(since))) + " seconds ago" } else if since < 120 { return "1 minute ago" } else if since < 3600 { return strconv.Itoa(int(math.Floor(since/60))) + " minutes ago" } else if since < 7200 { return "1 hour ago" } else if since < 86400 { return strconv.Itoa(int(math.Floor(since/60/60))) + " hours ago" } else if since < 172800 { return "1 day ago" } else if since < 345600 { return strconv.Itoa(int(math.Floor(since/60/60/24))) + " days ago" } else { return timestamp.Format("01/02/2006 3:04 PM") } /* Discord styled timestamp code here. now := time.Now().In(location) if now.Day() == timestamp.Day() && now.Month() == timestamp.Month() && now.Year() == timestamp.Year() { return timestamp.Format("Today at 3:04 PM") } else if now.Day()-1 == timestamp.Day() && now.Month() == timestamp.Month() && now.Year() == timestamp.Year() { return timestamp.Format("Yesterday at 3:04 PM") } else if now.Day()-2 == timestamp.Day() && now.Month() == timestamp.Month() && now.Year() == timestamp.Year() { return timestamp.Format("Last Monday at 3:04 PM") } else { return timestamp.Format("01/02/2006 3:04 PM") } */ } // Send a notification to a user. func createNotif(to int, notif_type int, post string, currentUser int) { notif_read := 0 for client := range clients { if clients[client].UserID == to { if ((notif_type == 0 || notif_type == 2 || notif_type == 3) && clients[client].OnPage == "/posts/"+post) || (notif_type == 1 && clients[client].OnPage == "/comments/"+post) { notif_read = 1 break } } } if notif_type == 0 || notif_type == 1 { var hasYeahNotificationsEnabled bool db.QueryRow("SELECT yeah_notifications FROM users WHERE id = ?", to).Scan(&hasYeahNotificationsEnabled) if !hasYeahNotificationsEnabled { return } } // 0 = post yeah, 1 = reply yeah, 2 = comment on your post, 3 = poster's comment, 4 = follow var check_mergedusernews int if notif_type != 4 { db.QueryRow("SELECT merged FROM notifications WHERE notif_by = ? AND notif_to = ? AND notif_type = ? AND notif_post = ? AND merged IS NOT NULL AND notif_date > NOW() - 28800 ORDER BY notif_date DESC", currentUser, to, notif_type, post).Scan(&check_mergedusernews) } else { db.QueryRow("SELECT merged FROM notifications WHERE notif_by = ? AND notif_to = ? AND notif_type = ? AND merged IS NOT NULL AND notif_date > NOW() - 28800 ORDER BY notif_date DESC", currentUser, to, notif_type).Scan(&check_mergedusernews) } if check_mergedusernews != 0 { stmt, _ := db.Prepare("UPDATE notifications SET notif_read = 0, notif_date = CURRENT_TIMESTAMP WHERE id = ?") stmt.Exec(&check_mergedusernews) stmt.Close() } else { var result_update_newsmergesearch int if notif_type != 4 { db.QueryRow("SELECT id FROM notifications WHERE notif_to = ? AND notif_post = ? AND notif_date > NOW() - 28800 AND notif_type = ? ORDER BY notif_date DESC", to, post, notif_type).Scan(&result_update_newsmergesearch) } else { db.QueryRow("SELECT id FROM notifications WHERE notif_to = ? AND notif_date > NOW() - 28800 AND notif_type = ? ORDER BY notif_date DESC", to, notif_type).Scan(&result_update_newsmergesearch) } if result_update_newsmergesearch != 0 { if notif_type != 4 { stmt, _ := db.Prepare("INSERT INTO notifications(notif_by, notif_to, notif_post, merged, notif_type, notif_read) VALUES (?, ?, ?, ?, ?, ?)") stmt.Exec(currentUser, to, post, result_update_newsmergesearch, notif_type, notif_read) stmt.Close() } else { stmt, _ := db.Prepare("INSERT INTO notifications(notif_by, notif_to, merged, notif_type, notif_read) VALUES (?, ?, ?, ?, ?)") stmt.Exec(currentUser, to, result_update_newsmergesearch, notif_type, notif_read) stmt.Close() } stmt, _ := db.Prepare("UPDATE notifications SET notif_read = ?, notif_date = NOW() WHERE id = ?") stmt.Exec(notif_read, result_update_newsmergesearch) stmt.Close() } else { if notif_type != 4 { stmt, _ := db.Prepare("INSERT INTO notifications(notif_by, notif_to, notif_post, notif_type, notif_read) VALUES (?, ?, ?, ?, ?)") stmt.Exec(currentUser, to, post, notif_type, notif_read) stmt.Close() } else { stmt, _ := db.Prepare("INSERT INTO notifications(notif_by, notif_to, notif_type, notif_read) VALUES (?, ?, ?, ?)") stmt.Exec(currentUser, to, notif_type, notif_read) stmt.Close() } } } if notif_read == 0 { var msg wsMessage msg.Type = "notif" var notifCount int var friendRequests int db.QueryRow("SELECT COUNT(*) FROM notifications WHERE notif_to = ? AND merged IS NULL AND notif_read = 0", &to).Scan(¬ifCount) db.QueryRow("SELECT COUNT(*) FROM friend_requests WHERE request_to = ? AND request_read = 0", to).Scan(&friendRequests) msg.Content = strconv.Itoa(notifCount + friendRequests) for client := range clients { if clients[client].UserID == to { err := client.WriteJSON(msg) if err != nil { client.Close() delete(clients, client) } } } } } // Find a user with a username. func QueryUser(username string, timezone string) user { var users = user{} var role int var lastSeenTime time.Time db.QueryRow("SELECT id, username, nickname, avatar, has_mh, email, password, ip, level, role, online, hide_online, last_seen, hide_last_seen, color, theme, yeah_notifications, websockets_enabled, forbidden_keywords, default_privacy FROM users WHERE username=?", username).Scan(&users.ID, &users.Username, &users.Nickname, &users.Avatar, &users.HasMii, &users.Email, &users.Password, &users.IP, &users.Level, &role, &users.Online, &users.HideOnline, &lastSeenTime, &users.HideLastSeen, &users.Color, &users.Theme, &users.YeahNotifications, &users.WebsocketsEnabled, &users.ForbiddenKeywords, &users.DefaultPrivacy) if role > 0 { db.QueryRow("SELECT image, organization FROM roles WHERE id = ?", role).Scan(&users.Role.Image, &users.Role.Organization) } users.Timezone = timezone users.LastSeen = humanTiming(lastSeenTime, timezone) users.LastSeenUnix = lastSeenTime.Unix() return users } // Send JSON to a websocket. //func sendJSON() // Get an array of posts from an SQL query. func setupPost(row *post, currentUser user, postType int, repostLayer int) *post { row.PosterIcon = getAvatar(row.PosterIcon, row.PosterHasMii, row.Feeling) if row.PosterRoleID > 0 { row.PosterRoleImage = getRoleImage(row.PosterRoleID) } row.CreatedAt = humanTiming(row.CreatedAtTime, currentUser.Timezone) row.CreatedAtUnix = row.CreatedAtTime.Unix() if row.EditedAtTime.Sub(row.CreatedAtTime).Minutes() > 5 { row.EditedAt = humanTiming(row.EditedAtTime, currentUser.Timezone) row.EditedAtUnix = row.EditedAtTime.Unix() } if len(row.MigratedID) == 0 || strings.Contains(row.BodyText, ":markdown:") { row.Body = parseBodyWithLineBreaks(row.BodyText, true, true) } else { row.Body = parseBodyWithLineBreaks(row.BodyText, true, false) } if row.CreatedBy == currentUser.ID { row.ByMe = true } if row.PostType == 2 { row.Poll = getPoll(row.ID, currentUser.ID) } row.Type = postType if row.RepostID > 0 { var repost post if repostLayer < 3 { db.QueryRow("SELECT posts.id, created_by, created_at, edited_at, feeling, body, image, attachment_type, is_spoiler, post_type, url, url_type, pinned, privacy, repost, migration, migrated_id, migrated_community, is_rm_by_admin, communities.id, title, icon, rm, username, nickname, avatar, has_mh, online, hide_online, color, role FROM posts LEFT JOIN communities ON communities.id = community_id LEFT JOIN users ON users.id = created_by WHERE posts.id = ? AND is_rm = 0 AND users.id NOT IN (SELECT if(source = ?, target, source) FROM blocks WHERE (source = ? AND target = users.id) OR (source = users.id AND target = ?)) AND IF(created_by = ?, true, LOWER(body) NOT REGEXP LOWER(?)) AND (privacy = 0 OR (privacy IN (1, 2, 3, 4) AND (SELECT COUNT(*) FROM friendships WHERE source = ? AND target = created_by OR source = created_by AND target = ? LIMIT 1) = 1) OR (privacy IN (1, 3, 5, 6) AND (SELECT COUNT(*) FROM follows WHERE follow_to = created_by AND follow_by = ? LIMIT 1) = 1) OR (privacy IN (1, 2, 5, 7) AND (SELECT COUNT(*) FROM follows WHERE follow_to = ? AND follow_by = created_by) = 1) OR (privacy = 8 AND ? > 0) OR created_by = ?) LIMIT 1", row.RepostID, currentUser.ID, currentUser.ID, currentUser.ID, currentUser.ID, escapeForbiddenKeywords(currentUser.ForbiddenKeywords), currentUser.ID, currentUser.ID, currentUser.ID, currentUser.ID, currentUser.Level, currentUser.ID).Scan(&repost.ID, &repost.CreatedBy, &repost.CreatedAtTime, &repost.EditedAtTime, &repost.Feeling, &repost.BodyText, &repost.Image, &repost.AttachmentType, &repost.IsSpoiler, &repost.PostType, &repost.URL, &repost.URLType, &repost.Pinned, &repost.Privacy, &repost.RepostID, &repost.MigrationID, &repost.MigratedID, &repost.MigratedCommunity, &repost.IsRMByAdmin, &repost.CommunityID, &repost.CommunityName, &repost.CommunityIcon, &repost.CommunityRM, &repost.PosterUsername, &repost.PosterNickname, &repost.PosterIcon, &repost.PosterHasMii, &repost.PosterOnline, &repost.PosterHideOnline, &repost.PosterColor, &repost.PosterRoleID) row.Repost = &repost row.Repost.Type = 3 if len(row.Repost.CommunityName) > 0 { repostLayer = repostLayer + 1 row.Repost = setupPost(row.Repost, currentUser, 3, repostLayer) } } else { repost.Type = 4 repost.ID = row.ID row.Repost = &repost } } if row.MigrationID > 0 { row.MigrationImage, row.MigrationURL, row.CommunityName, row.CommunityIcon = getPostMigration(row.MigrationID, row.MigratedCommunity) } db.QueryRow("SELECT COUNT(*) FROM yeahs WHERE yeah_post = ? AND yeah_by = ? AND on_comment = 0 LIMIT 1", row.ID, currentUser.ID).Scan(&row.Yeahed) row.CanYeah = checkIfCanYeah(currentUser, row.CreatedBy) if row.CommentCount != -1 { db.QueryRow("SELECT COUNT(*) FROM yeahs WHERE yeah_post = ? AND on_comment = 0", row.ID).Scan(&row.YeahCount) db.QueryRow("SELECT COUNT(*) FROM comments WHERE post = ? AND is_rm = 0", row.ID).Scan(&row.CommentCount) if row.CommentCount > 0 && postType != -1 && postType != 3 { row.CommentPreview = getCommentPreview(row.ID, currentUser) } } else { db.QueryRow("SELECT COUNT(*) FROM yeahs WHERE yeah_post = ? AND on_comment = 1", row.ID).Scan(&row.YeahCount) } return row } // Show a ban screen. func showBan(w http.ResponseWriter, currentUser user, banLength time.Time) bool { if time.Now().Sub(banLength).Seconds() > 1 { stmt, _ := db.Prepare("DELETE FROM bans WHERE user = ?") stmt.Exec(currentUser.ID) stmt.Close() return false } else { var data = map[string]interface{}{ "CurrentUser": currentUser, "Length": banLength.Format("01/02/2006 3:04 PM"), "LengthForever": banLength.Year() > 2100, } err := templates.ExecuteTemplate(w, "ban.html", data) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return true } } // Find a profile by user ID. func QueryProfile(id int, timezone string) profile { var profiles = profile{} var createdAtTime time.Time var genderNumber int // Gender is just a number. db.QueryRow("SELECT created_at, nnid, mh, avatar_image, avatar_id, gender, region, comment, nnid_visibility, yeah_visibility, reply_visibility, discord, steam, psn, switch_code, twitter, youtube, allow_friend, favorite FROM profiles WHERE user = ?", id).Scan(&createdAtTime, &profiles.NNID, &profiles.MiiHash, &profiles.AvatarImage, &profiles.AvatarID, &genderNumber, &profiles.Region, &profiles.CommentText, &profiles.NNIDVisibility, &profiles.YeahVisibility, &profiles.ReplyVisibility, &profiles.Discord, &profiles.Steam, &profiles.PSN, &profiles.SwitchCode, &profiles.Twitter, &profiles.YouTube, &profiles.AllowFriend, &profiles.FavoritePostID) profiles.Gender = [6]string{"", "He/him", "She/her", "He/she", "Nonbinary", "They/them"}[genderNumber] profiles.User = id profiles.Comment = parseBodyWithLineBreaks(profiles.CommentText, false, true) profiles.CreatedAt = humanTiming(createdAtTime, timezone) profiles.CreatedAtUnix = createdAtTime.Unix() return profiles } // Find a community by ID. func QueryCommunity(id string, canBeRM bool) community { var communities = community{} if canBeRM { db.QueryRow("SELECT id, title, description, icon, banner, is_featured, permissions, rm FROM communities WHERE id = ?", id).Scan(&communities.ID, &communities.Title, &communities.DescriptionText, &communities.Icon, &communities.Banner, &communities.IsFeatured, &communities.Permissions, &communities.RM) } else { db.QueryRow("SELECT id, title, description, icon, banner, is_featured, permissions, rm FROM communities WHERE id = ? AND rm = 0", id).Scan(&communities.ID, &communities.Title, &communities.DescriptionText, &communities.Icon, &communities.Banner, &communities.IsFeatured, &communities.Permissions, &communities.RM) } communities.Description = parseBodyWithLineBreaks(communities.DescriptionText, false, true) return communities } // Set variables for profile sidebar. func setupProfileSidebar(user user, currentUser user, profileOnPage string) profileSidebar { var sidebar profileSidebar sidebar.Profile = QueryProfile(user.ID, currentUser.Timezone) sidebar.User = user sidebar.CurrentUser = currentUser sidebar.ProfileOnPage = profileOnPage sidebar.Reasons = settings.ReportReasons if len(sidebar.User.Theme) > 0 { sidebar.User.ThemeColors = strings.Split(sidebar.User.Theme, ",") } db.QueryRow("SELECT COUNT(*) FROM follows WHERE follow_to = ? AND follow_by = ? LIMIT 1", user.ID, currentUser.ID).Scan(&sidebar.IsFollowing) var requestTimestamp time.Time _ = db.QueryRow("SELECT COUNT(*), created_at FROM friend_requests WHERE request_to = ? AND request_by = ? GROUP BY created_at", user.ID, currentUser.ID).Scan(&sidebar.FriendStatus, &requestTimestamp) if sidebar.FriendStatus > 0 { sidebar.FriendStatus = 2 sidebar.RequestTime = requestTimestamp.Format("01/02/2006 3:04 PM") } else { db.QueryRow("SELECT COUNT(*) FROM friend_requests WHERE request_to = ? AND request_by = ?", currentUser.ID, user.ID).Scan(&sidebar.FriendStatus) if sidebar.FriendStatus > 0 { sidebar.FriendStatus = 1 var createdAt time.Time db.QueryRow("SELECT id, message, created_at FROM friend_requests WHERE request_to = ? AND request_by = ? ORDER BY friend_requests.id DESC", currentUser.ID, user.ID).Scan(&sidebar.Request.ID, &sidebar.Request.Message, &createdAt) sidebar.Request.CreatedAt = createdAt.Format("01/02/2006 3:04 PM") } else { db.QueryRow("SELECT COUNT(*) FROM friendships WHERE (source = ? AND target = ?) OR (source = ? AND target = ?)", user.ID, currentUser.ID, currentUser.ID, user.ID).Scan(&sidebar.FriendStatus) if sidebar.FriendStatus > 0 { sidebar.FriendStatus = 3 } else { sidebar.FriendStatus = 0 if sidebar.Profile.AllowFriend == 1 { db.QueryRow("SELECT COUNT(*) FROM follows WHERE follow_to = ? AND follow_by = ? LIMIT 1", currentUser.ID, user.ID).Scan(&sidebar.IsFollowingMe) } } } } sidebar.User.Blocked = checkIfBlocked(currentUser.ID, user.ID) sidebar.Profile.FriendCount, sidebar.Profile.FollowingCount, sidebar.Profile.FollowerCount = setupSidebarStatus(user.ID) var banCount int db.QueryRow("SELECT COUNT(*) FROM bans WHERE user = ?", user.ID).Scan(&banCount) if banCount > 0 { if len(sidebar.User.Role.Organization) > 0 { sidebar.User.Role.Organization = "Banned
" + sidebar.User.Role.Organization } else { sidebar.User.Role.Organization = "Banned" } } db.QueryRow("SELECT COUNT(*) FROM posts WHERE created_by = ? AND is_rm = 0", user.ID).Scan(&sidebar.Profile.PostCount) db.QueryRow("SELECT COUNT(*) FROM comments WHERE created_by = ? AND is_rm = 0", user.ID).Scan(&sidebar.Profile.CommentCount) db.QueryRow("SELECT COUNT(*) FROM yeahs WHERE yeah_by = ?", user.ID).Scan(&sidebar.Profile.YeahCount) db.QueryRow("SELECT image FROM posts WHERE id = ?", sidebar.Profile.FavoritePostID).Scan(&sidebar.Profile.FavoritePostImage) favorite_rows, err := db.Query("SELECT communities.id, title, icon FROM communities LEFT JOIN community_favorites ON communities.id = community WHERE favorite_by = ? AND rm = 0 ORDER BY community_favorites.id DESC LIMIT 10", user.ID) if err != nil { fmt.Println("error while getting favorite communities") fmt.Println(err.Error()) return sidebar } var favorites []community for favorite_rows.Next() { var row = community{} err := favorite_rows.Scan(&row.ID, &row.Title, &row.Icon) if err != nil { fmt.Println("error while scanning favorite communities") fmt.Println(err.Error()) } favorites = append(favorites, row) } favorite_rows.Close() sidebar.FavoriteCommunities = favorites return sidebar } // Set friend/following/follower counts for sidebars. func setupSidebarStatus(userID int) (int, int, int) { friendCount, followingCount, followerCount := 0, 0, 0 if userID != 0 { db.QueryRow("SELECT COUNT(*) FROM friendships WHERE source = ? OR target = ?", userID, userID).Scan(&friendCount) db.QueryRow("SELECT COUNT(*) FROM follows WHERE follow_by = ?", userID).Scan(&followingCount) db.QueryRow("SELECT COUNT(*) FROM follows WHERE follow_to = ?", userID).Scan(&followerCount) } return friendCount, followingCount, followerCount } // Cut a string off at 200 characters if needed, and parse Markdown and later emotes. func parseBody(body string, cutoff bool, parseMarkdown bool) template.HTML { // Cut off at 200 characters if cutoff is set. if cutoff && utf8.RuneCountInString(body) > 203 { runes := []rune(body) // What is this, fucking RuneScape!? body = string(runes[0:200]) + "..." } // Parse markdown and sanitize HTML. if parseMarkdown { body = strings.Replace(body, "<3", "\\<3", -1) bodyTemp := blackfriday.Run([]byte(body), blackfriday.WithRenderer(renderer)) if len(bodyTemp) >= 7 { rune2 := []rune(string(bodyTemp)) body = string(rune2[:len(rune2)-1]) } else { body = string(bodyTemp) } } body = bluemonday.UGCPolicy().Sanitize(body) // Parse emotes. matches := emotes.FindAllStringSubmatch(body, settings.EmoteLimit) for _, match := range matches { var image sql.NullString db.QueryRow("SELECT image FROM emotes WHERE name = ?", match[1]).Scan(&image) if image.Valid { if len(image.String) > 0 { body = strings.Replace(body, match[0], "", 1) } else { body = strings.Replace(body, match[0], "", 1) } } } // Return the output. return template.HTML(body) } // Parse a body with parseBody(), and then replace line breaks with
elements. func parseBodyWithLineBreaks(body string, cutoff bool, parseMarkdown bool) template.HTML { bodyHTML := parseBody(body, cutoff, parseMarkdown) body = strings.Replace(string(bodyHTML), "\n", "
", -1) return template.HTML(body) } // Cut a string off at 200 characters. func parseCutoff(body template.HTML) template.HTML { return body } // Cut a string off at 15 characters. Used for notifications and the "View _____'s post for this comment" bar thingy at the top of the comments page. func parsePreview(body string, postType int, isRM bool) string { if isRM { body = "deleted" } else if len(body) == 0 { body = "empty" } else if postType == 1 { body = "handwritten" } else if utf8.RuneCountInString(body) > 18 { runes := []rune(body) body = string(runes[0:15]) + "..." } return body } // Really minimal function to check if the user viewing the page can give a Yeah to a certain post. func checkIfCanYeah(currentUser user, createdBy int) bool { if len(currentUser.Username) == 0 { return false } if createdBy == currentUser.ID { return false } if checkIfEitherBlocked(currentUser.ID, createdBy) { return false } return true } // Generate the name of a group chat from an array of user nicknames. func getGroupName(users []string) string { groupName := "Group chat with " if len(users) < 1 { groupName += "yourself" } else if len(users) == 1 { groupName += users[0] } else if len(users) == 2 { groupName += users[0] + " and " + users[1] } else { for i := 0; i < len(users)-2; i++ { groupName += users[i] + ", " } groupName += users[len(users)-2] + " and " + users[len(users)-1] } return groupName } // Fetch the site's settings from a config.json file. func getSettings() config { var settings config settingsJSON, err := ioutil.ReadFile("config.json") if err != nil { fmt.Println(err.Error()) return settings } err = json.Unmarshal(settingsJSON, &settings) if err != nil { fmt.Println(err.Error()) } settings.ReportReasons = append([]reportReason{{ Name: "Spoiler", Message: "Your post contained spoilers, so it was removed.", Enabled: false, BodyRequired: true, }}, settings.ReportReasons...) return settings } // Generate a login token for autoauth. func generateLoginToken() string { const letterBytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890" const ( letterIdxBits = 6 letterIdxMask = 1<= 0; { if remain == 0 { cache, remain = src.Int63(), letterIdxMax } if idx := int(cache & letterIdxMask); idx < len(letterBytes) { b[i] = letterBytes[idx] i-- } cache >>= letterIdxBits remain-- } return string(b) } // Check if a user is blocking another user. func checkIfBlocked(source int, target int) bool { var isBlocked bool err := db.QueryRow("SELECT COUNT(*) FROM blocks WHERE source = ? AND target = ?", source, target).Scan(&isBlocked) if err != nil { fmt.Println("error while getting blocks") fmt.Println(err.Error()) } return isBlocked } // Check if a user is blocking another user, or vice versa. func checkIfEitherBlocked(source int, target int) bool { var isBlocked bool err := db.QueryRow("SELECT COUNT(*) FROM blocks WHERE (source = ? AND target = ?) OR (source = ? AND target = ?)", source, target, target, source).Scan(&isBlocked) if err != nil { fmt.Println("error while checking if either ballcoks") fmt.Println(err.Error()) } return isBlocked } // Render a user's avatar as a Mii URL with an emotion or return it if it's not a Mii. func getAvatar(avatar string, hasMii bool, feeling int) string { const url = "https://mii-secure.cdn.nintendo.net/%s_%s_face.png" if len(avatar) == 0 { return "/assets/img/anonymous.png" } if hasMii { switch feeling { case 1, 8: return fmt.Sprintf(url, avatar, "happy") case 2, 7: return fmt.Sprintf(url, avatar, "like") case 3, 6: return fmt.Sprintf(url, avatar, "surprised") case 4: return fmt.Sprintf(url, avatar, "frustrated") case 5: return fmt.Sprintf(url, avatar, "puzzled") default: return fmt.Sprintf(url, avatar, "normal") } } return avatar } // Get the database values necessary for showing a comment preview. func getCommentPreview(postID int, currentUser user) comment { var commentPreview comment var timestamp time.Time var editedAt time.Time var role int db.QueryRow("SELECT comments.id, created_at, edited_at, feeling, body, post_type, username, nickname, avatar, has_mh, online, hide_online, color, role FROM comments INNER JOIN users ON users.id = created_by WHERE post = ? AND is_rm = 0 AND is_rm_by_admin = 0 AND is_spoiler = 0 AND (users.id NOT IN (SELECT if(source = ?, target, source) FROM blocks WHERE (source = ? AND target = users.id) OR (source = users.id AND target = ?)) OR ? > 0) AND IF(created_by = ?, true, LOWER(body) NOT REGEXP LOWER(?)) ORDER BY comments.id DESC LIMIT 1", postID, currentUser.ID, currentUser.ID, currentUser.ID, currentUser.Level, currentUser.ID, escapeForbiddenKeywords(currentUser.ForbiddenKeywords)).Scan(&commentPreview.ID, ×tamp, &editedAt, &commentPreview.Feeling, &commentPreview.BodyText, &commentPreview.PostType, &commentPreview.CommenterUsername, &commentPreview.CommenterNickname, &commentPreview.CommenterIcon, &commentPreview.CommenterHasMii, &commentPreview.CommenterOnline, &commentPreview.CommenterHideOnline, &commentPreview.CommenterColor, &role) commentPreview.CommenterIcon = getAvatar(commentPreview.CommenterIcon, commentPreview.CommenterHasMii, commentPreview.Feeling) if role > 0 { commentPreview.CommenterRoleImage = getRoleImage(role) } commentPreview.CreatedAt = humanTiming(timestamp, currentUser.Timezone) commentPreview.CreatedAtUnix = timestamp.Unix() if editedAt.Sub(timestamp).Minutes() > 5 { commentPreview.EditedAt = humanTiming(editedAt, currentUser.Timezone) commentPreview.EditedAtUnix = editedAt.Unix() } commentPreview.Body = parseBody(commentPreview.BodyText, false, true) return commentPreview } // Check if a string violates a user's forbidden keywords. func inForbiddenKeywords(text string, userID int) bool { var forbiddenKeywords string err := db.QueryRow("SELECT forbidden_keywords FROM users WHERE id = ?", userID).Scan(&forbiddenKeywords) if err != nil { fmt.Println("error while getting forbidden keyword") fmt.Println(err) } isMatch, err := regexp.MatchString(escapeForbiddenKeywords(forbiddenKeywords), text) if err != nil { fmt.Println("error while dying") fmt.Println(err) } return isMatch } // Get the current hostname. // TODO: this prints my local server's hostname like 127.0.0.1:8003:8003 // but is it really worth fixing func getHostname(host string) string { hostname := "http" if settings.SSL.Enabled { hostname += "s" } hostname += "://" + host if (settings.Port == ":80" && settings.SSL.Enabled) || (settings.Port == ":443" && !settings.SSL.Enabled) || (settings.Port != ":80" && settings.Port != ":443") { hostname += settings.Port } return hostname } // Get the current user's IP. func getIP(r *http.Request) string { ForwardedForHeader := r.Header.Get("X-Forwarded-For") if settings.Proxy && len(ForwardedForHeader) > 0 { // Proxy sites like Cloudflare mask the IP, so grab that from the headers... if it's set in the settings, that is; otherwise, people could fake this and we'd have an impersonation exploit on our hands. (Looking at you, Seth) //ips := strings.Split(r.Header.Get("X-Forwarded-For"), ", ") //return ips[0] + settings.Port return ForwardedForHeader + ":0" } else { return r.RemoteAddr } } // Get a poll from an ID. func getPoll(pollID int, userID int) poll { var newPoll poll option_rows, err := db.Query("SELECT options.id, name FROM options WHERE post = ?", pollID) if err != nil { fmt.Println("could not get poll") fmt.Println(err.Error()) return newPoll } for option_rows.Next() { var row = option{} option_rows.Scan(&row.ID, &row.Name) user_rows, err := db.Query("SELECT users.id FROM votes LEFT JOIN users ON users.id = user WHERE poll = ? AND option_id = ?", pollID, row.ID) if err != nil { fmt.Println("could not die") fmt.Println(err.Error()) return newPoll } for user_rows.Next() { // OPTIMIZE THIS!!! var currentID int user_rows.Scan(¤tID) if currentID == userID { row.Selected = true newPoll.Selected = true } row.Votes = row.Votes + 1 } user_rows.Close() newPoll.Options = append(newPoll.Options, row) } option_rows.Close() for _, row := range newPoll.Options { newPoll.Votes = newPoll.Votes + row.Votes } for i, row := range newPoll.Options { if row.Votes > 0 { newPoll.Options[i].Percentage = math.Round(row.Votes / newPoll.Votes * 100) } else { row.Percentage = 0 } } newPoll.ID = pollID return newPoll } // Get the image of a role from its ID. func getRoleImage(roleID int) string { var image string db.QueryRow("SELECT image FROM roles WHERE id = ?", roleID).Scan(&image) return image } // Get the image and organization of a role from its ID. func getRoleImageAndOrganization(roleID int) (string, string) { var image string var organization string err := db.QueryRow("SELECT image, organization FROM roles WHERE id = ?", roleID).Scan(&image, &organization) if err != nil && err != sql.ErrNoRows { fmt.Println("role lookup failed: " + err.Error()) } return image, organization } // Get a user's timezone from their IP. func getTimezone(ip string) string { if isGeoIPEnabled == false { return settings.DefaultTimezone } parsedHost, _, _ := net.SplitHostPort(ip) parsedIP := net.ParseIP(parsedHost) if parsedIP == nil { fmt.Println("parsedIP was nil") return settings.DefaultTimezone } city, err := geoip.City(parsedIP) if err != nil { fmt.Println("no city") fmt.Println(err.Error()) return settings.DefaultTimezone } if len(city.Location.TimeZone) > 0 { return city.Location.TimeZone } else { return settings.DefaultTimezone } } // Write to a websocket. func writeWs(session *wsSession, client *websocket.Conn, message wsMessage) error { session.Send <- message return nil }