384 lines
15 KiB
Go
384 lines
15 KiB
Go
|
////////////////////////
|
||
|
// //
|
||
|
// Indigo //
|
||
|
// The Miiverse clone //
|
||
|
// that will end all //
|
||
|
// other Miiverse //
|
||
|
// clones, for real //
|
||
|
// this time. //
|
||
|
// //
|
||
|
// Lead Devs: PF2M, //
|
||
|
// Seth/EnergeticBark //
|
||
|
// //
|
||
|
// Developers: Ben, //
|
||
|
// triangles.py, jod, //
|
||
|
// & Chance/SRGNation //
|
||
|
// //
|
||
|
// Artwork: Spicy & //
|
||
|
// Inverse & Gnarly //
|
||
|
// //
|
||
|
// Marketing: Pip //
|
||
|
// //
|
||
|
// Testing: Mippy ♥ //
|
||
|
// //
|
||
|
// https://github.com //
|
||
|
// /PF2M/Indigo //
|
||
|
// //
|
||
|
////////////////////////
|
||
|
|
||
|
package main
|
||
|
|
||
|
// Import dependencies.
|
||
|
import (
|
||
|
// Internals
|
||
|
"database/sql"
|
||
|
"encoding/json"
|
||
|
"html/template"
|
||
|
"log"
|
||
|
"net"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
|
||
|
// "user" is already defined in types
|
||
|
osUser "os/user"
|
||
|
"strconv"
|
||
|
|
||
|
"regexp"
|
||
|
|
||
|
// Externals
|
||
|
"github.com/NYTimes/gziphandler"
|
||
|
_ "github.com/go-sql-driver/mysql"
|
||
|
"github.com/gorilla/csrf"
|
||
|
"github.com/gorilla/mux"
|
||
|
"github.com/gorilla/websocket"
|
||
|
"github.com/oschwald/geoip2-golang"
|
||
|
"github.com/russross/blackfriday/v2"
|
||
|
)
|
||
|
|
||
|
// Initialize some variables.
|
||
|
var db *sql.DB
|
||
|
var err error
|
||
|
var clients = make(map[*websocket.Conn]*wsSession)
|
||
|
var settings config
|
||
|
var admin adminConfig
|
||
|
var youtube *regexp.Regexp
|
||
|
var spotify *regexp.Regexp
|
||
|
var soundcloud *regexp.Regexp
|
||
|
var symbols *regexp.Regexp
|
||
|
var emotes *regexp.Regexp
|
||
|
var renderer *blackfriday.HTMLRenderer
|
||
|
var geoip *geoip2.Reader
|
||
|
var isGeoIPEnabled bool
|
||
|
|
||
|
// Configure the upgrader.
|
||
|
var upgrader = websocket.Upgrader{
|
||
|
CheckOrigin: func(r *http.Request) bool {
|
||
|
// Todo: Add to this if necessary
|
||
|
return true
|
||
|
},
|
||
|
EnableCompression: true,
|
||
|
}
|
||
|
|
||
|
// Define the templates.
|
||
|
var templates *template.Template
|
||
|
|
||
|
// Redirect HTTP requests to HTTPS if properly configured.
|
||
|
func redirect(w http.ResponseWriter, r *http.Request) {
|
||
|
http.Redirect(w, r, "https://"+r.Host+r.URL.Path, http.StatusTemporaryRedirect)
|
||
|
}
|
||
|
|
||
|
// Now let's start the main function!
|
||
|
func main() {
|
||
|
// Fetch the site's settings from JSON files.
|
||
|
settings = getSettings()
|
||
|
adminJSON, err := os.ReadFile("admin.json")
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
err = json.Unmarshal(adminJSON, &admin)
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Connect to the database.
|
||
|
db, err = sql.Open("mysql", settings.DB.Username+":"+settings.DB.Password+"@tcp("+settings.DB.Host+")/"+settings.DB.Name+"?parseTime=true&loc=US%2FEastern&charset=utf8mb4,utf8")
|
||
|
if err != nil {
|
||
|
log.Printf("[err]: unable to connect to the database...\n")
|
||
|
log.Printf(" %v\n", err)
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
|
||
|
// Ping the database to make sure we connected properly.
|
||
|
err = db.Ping()
|
||
|
if err != nil {
|
||
|
log.Printf("[err]: unable to ping the database...\n")
|
||
|
log.Printf(" %v\n", err)
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
_, err = db.Exec("SET CHARACTER SET utf8mb4")
|
||
|
if err != nil {
|
||
|
log.Printf("[err]: unable to set the character set...\n")
|
||
|
log.Printf(" %v\n", err)
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
_, err = db.Exec("SET collation_connection = utf8mb4_bin")
|
||
|
if err != nil {
|
||
|
log.Printf("[err]: unable to set the connection collation...\n")
|
||
|
log.Printf(" %v\n", err)
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
|
||
|
// Initialize some regex.
|
||
|
youtube, _ = regexp.Compile("(?:youtube\\.com/\\S*(?:(?:/e(?:mbed))?/|watch/?\\?(?:\\S*?&?v=))|youtu\\.be/)([a-zA-Z0-9_-]{6,11})")
|
||
|
spotify, _ = regexp.Compile("(?:embed\\.|open\\.)(?:spotify\\.com/)(?:track/|\\?uri=spotify:track:)((\\w|-){22})")
|
||
|
soundcloud, _ = regexp.Compile("(soundcloud\\.com|snd\\.sc)(.*)")
|
||
|
symbols, _ = regexp.Compile("(\\|\\\\|`|\\*|{|}|\\[|\\](|)|\\+|-|!|_|>|\\n|&|:|<)")
|
||
|
emotes, err = regexp.Compile(":([^ :]+):")
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Initialize Markdown renderer.
|
||
|
renderer = blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
|
||
|
Flags: 2 | 4 | 128,
|
||
|
})
|
||
|
|
||
|
// Initialize GeoIP if a database is present.
|
||
|
isGeoIPEnabled = false
|
||
|
if _, err = os.Stat("geoip.mmdb"); err == nil {
|
||
|
geoip, err = geoip2.Open("geoip.mmdb")
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
defer geoip.Close()
|
||
|
isGeoIPEnabled = true
|
||
|
}
|
||
|
|
||
|
// Wipe the online statuses of all the users and delete all session keys (necessary after crashes, shutdowns, etc.)
|
||
|
db.QueryRow("UPDATE users SET online = 0").Scan()
|
||
|
db.QueryRow("TRUNCATE TABLE sessions").Scan()
|
||
|
|
||
|
// Close the database connection after this function exits.
|
||
|
defer db.Close()
|
||
|
|
||
|
// initialize the templates by parsing everything from the views directory recursively
|
||
|
var tmplFiles []string
|
||
|
err = filepath.Walk("views", func(path string, info os.FileInfo, err error) error {
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// exclude non-html files
|
||
|
if !info.IsDir() && filepath.Ext(path) == ".html" {
|
||
|
// feel free to instead make this directly build the template
|
||
|
tmplFiles = append(tmplFiles, path)
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
if err != nil {
|
||
|
log.Fatal("could not add or find templates (they are stored in views, is this accessible?): ", err)
|
||
|
}
|
||
|
|
||
|
templates = template.Must(template.ParseFiles(tmplFiles...))
|
||
|
|
||
|
// make the directory for the local image provider if it doesn't exist
|
||
|
if settings.ImageHost.Provider == "local" {
|
||
|
// check if the error is specifically os.IsNotExist
|
||
|
if _, err := os.Stat(settings.ImageHost.ImageEndpoint); os.IsNotExist(err) {
|
||
|
// should make it in this working directory
|
||
|
err = os.MkdirAll(settings.ImageHost.ImageEndpoint, 0755)
|
||
|
if err != nil {
|
||
|
log.Println("could not make \""+settings.ImageHost.ImageEndpoint+"\" directory for local image host:", err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Set up CSRF.
|
||
|
CSRF := csrf.Protect([]byte(settings.CSRFSecret), csrf.FieldName("csrfmiddlewaretoken"), csrf.Path("/"), csrf.Secure(settings.SSL.Enabled))
|
||
|
|
||
|
// Initialize routes.
|
||
|
r := mux.NewRouter()
|
||
|
|
||
|
// functions that don't useLogin or requireLogin,
|
||
|
// they don't necessarily not access the user
|
||
|
// but just do it independently, not utilizing the CurrentUser
|
||
|
|
||
|
// Index route.
|
||
|
r.HandleFunc("/", useLogin(index)).Methods("GET")
|
||
|
|
||
|
// Auth routes.
|
||
|
r.HandleFunc("/signup", signup).Methods("GET", "POST")
|
||
|
r.HandleFunc("/login", login).Methods("GET", "POST")
|
||
|
r.HandleFunc("/logout", logout).Methods("POST")
|
||
|
r.HandleFunc("/reset", useLogin(resetPassword)).Methods("GET", "POST").Queries("token", "{token}")
|
||
|
r.HandleFunc("/reset", useLogin(showResetPassword)).Methods("GET", "POST")
|
||
|
|
||
|
// User routes.
|
||
|
r.HandleFunc("/users", requireLogin(showUserSearch)).Methods("GET").Queries("query", "{username}")
|
||
|
r.HandleFunc("/users/{username}", useLogin(showUser)).Methods("GET")
|
||
|
r.HandleFunc("/users/{username}/posts", useLogin(showUserPosts)).Methods("GET")
|
||
|
r.HandleFunc("/users/{username}/comments", useLogin(showUserComments)).Methods("GET")
|
||
|
r.HandleFunc("/users/{username}/yeahs", useLogin(showUserYeahs)).Methods("GET")
|
||
|
r.HandleFunc("/users/{username}/friends", useLogin(showFriends)).Methods("GET")
|
||
|
r.HandleFunc("/users/{username}/following", useLogin(showFollowing)).Methods("GET")
|
||
|
r.HandleFunc("/users/{username}/followers", useLogin(showFollowers)).Methods("GET")
|
||
|
r.HandleFunc("/users/{username}/favorites", useLogin(showFavorites)).Methods("GET")
|
||
|
r.HandleFunc("/users/{username}/friend_new", requireLogin(newFriendRequest)).Methods("POST")
|
||
|
r.HandleFunc("/users/{username}/friend_accept", requireLogin(acceptFriendRequest)).Methods("POST")
|
||
|
r.HandleFunc("/users/{username}/friend_reject", requireLogin(rejectFriendRequest)).Methods("POST")
|
||
|
r.HandleFunc("/users/{username}/friend_cancel", requireLogin(cancelFriendRequest)).Methods("POST")
|
||
|
r.HandleFunc("/users/{username}/friend_delete", requireLogin(deleteFriend)).Methods("POST")
|
||
|
r.HandleFunc("/users/{username}/follow", requireLogin(createFollow)).Methods("POST")
|
||
|
r.HandleFunc("/users/{username}/unfollow", requireLogin(deleteFollow)).Methods("POST")
|
||
|
r.HandleFunc("/users/{username}/violators", requireLogin(reportUser)).Methods("POST")
|
||
|
r.HandleFunc("/users/{username}/block", requireLogin(blockUser)).Methods("POST")
|
||
|
r.HandleFunc("/users/{username}/unblock", requireLogin(unblockUser)).Methods("POST")
|
||
|
|
||
|
// Post routes.
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}", useLogin(showPost)).Methods("GET")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/yeah", requireLogin(createPostYeah)).Methods("POST")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/yeahu", requireLogin(deletePostYeah)).Methods("POST")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/comments", useLogin(showAllComments)).Methods("GET")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/comments", requireLogin(createComment)).Methods("POST")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/favorite", requireLogin(favoritePost)).Methods("POST")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/unfavorite", requireLogin(unfavoritePost)).Methods("POST")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/violations", requireLogin(reportPost)).Methods("POST")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/vote", requireLogin(voteOnPoll)).Methods("POST")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/edit", requireLogin(editPost)).Methods("POST")
|
||
|
r.HandleFunc("/posts/{id:[0-9]+}/delete", requireLogin(deletePost)).Methods("POST")
|
||
|
|
||
|
// Comment routes.
|
||
|
r.HandleFunc("/comments/{id:[0-9]+}", useLogin(showComment)).Methods("GET")
|
||
|
r.HandleFunc("/comments/{id:[0-9]+}/yeah", requireLogin(createCommentYeah)).Methods("POST")
|
||
|
r.HandleFunc("/comments/{id:[0-9]+}/yeahu", requireLogin(deleteCommentYeah)).Methods("POST")
|
||
|
r.HandleFunc("/comments/{id:[0-9]+}/violations", requireLogin(reportComment)).Methods("POST")
|
||
|
r.HandleFunc("/comments/{id:[0-9]+}/edit", requireLogin(editComment)).Methods("POST")
|
||
|
r.HandleFunc("/comments/{id:[0-9]+}/delete", requireLogin(deleteComment)).Methods("POST")
|
||
|
|
||
|
// Community routes.
|
||
|
r.HandleFunc("/communities/all", useLogin(showAllCommunities)).Methods("GET")
|
||
|
r.HandleFunc("/communities/recent", requireLogin(showRecentCommunities)).Methods("GET")
|
||
|
r.HandleFunc("/communities/search", useLogin(showCommunitySearch)).Methods("GET").Queries("query", "{search}")
|
||
|
r.HandleFunc("/communities/{id:[0-9]+}", useLogin(showCommunity)).Methods("GET")
|
||
|
r.HandleFunc("/communities/{id:[0-9]+}/hot", useLogin(showPopularPosts)).Methods("GET")
|
||
|
r.HandleFunc("/communities/{id:[0-9]+}/posts", requireLogin(createPost)).Methods("POST")
|
||
|
r.HandleFunc("/communities/{id:[0-9]+}/favorite", requireLogin(addCommunityFavorite)).Methods("POST")
|
||
|
r.HandleFunc("/communities/{id:[0-9]+}/unfavorite", requireLogin(deleteCommunityFavorite)).Methods("POST")
|
||
|
|
||
|
// Activiy Feed route.
|
||
|
r.HandleFunc("/activity", requireLogin(showActivityFeed)).Methods("GET")
|
||
|
|
||
|
// Message routes.
|
||
|
r.HandleFunc("/messages", requireLogin(showMessages)).Methods("GET")
|
||
|
r.HandleFunc("/messages", requireLogin(sendMessage)).Methods("POST")
|
||
|
r.HandleFunc("/messages/{id:[0-9]+}/delete", requireLogin(deleteMessage)).Methods("POST")
|
||
|
r.HandleFunc("/messages/{username}", requireLogin(showConversation)).Methods("GET")
|
||
|
r.HandleFunc("/conversations/{id:[0-9]+}", requireLogin(showGroupChat)).Methods("GET")
|
||
|
r.HandleFunc("/conversations/create", requireLogin(showCreateGroupChat)).Methods("GET")
|
||
|
r.HandleFunc("/conversations/create", requireLogin(createGroupChat)).Methods("POST")
|
||
|
r.HandleFunc("/conversations/{id:[0-9]+}/edit", requireLogin(showEditGroupChat)).Methods("GET")
|
||
|
r.HandleFunc("/conversations/{id:[0-9]+}/edit", requireLogin(editGroupChat)).Methods("POST")
|
||
|
r.HandleFunc("/conversations/{id:[0-9]+}/leave", requireLogin(leaveGroupChat)).Methods("POST")
|
||
|
r.HandleFunc("/conversations/{id:[0-9]+}/delete", requireLogin(deleteGroupChat)).Methods("POST")
|
||
|
|
||
|
// Notification routes.
|
||
|
r.HandleFunc("/check_update.json", requireLogin(getNotificationCounts)).Methods("GET")
|
||
|
r.HandleFunc("/notifications", requireLogin(showNotifications)).Methods("GET")
|
||
|
r.HandleFunc("/notifications/friend_requests", requireLogin(showFriendRequests)).Methods("GET")
|
||
|
|
||
|
// Settings routes.
|
||
|
r.HandleFunc("/settings/profile", requireLogin(showProfileSettings)).Methods("GET")
|
||
|
r.HandleFunc("/settings/profile", requireLogin(editProfileSettings)).Methods("POST")
|
||
|
r.HandleFunc("/region", requireLogin(getRegion)).Methods("POST")
|
||
|
r.HandleFunc("/miis", getMii).Methods("POST")
|
||
|
r.HandleFunc("/migrate/{id:[0-9]+}", requireLogin(migratePosts)).Methods("POST")
|
||
|
r.HandleFunc("/rollback/{id:[0-9]+}", requireLogin(rollbackImport)).Methods("POST")
|
||
|
r.HandleFunc("/settings/account", requireLogin(showAccountSettings)).Methods("GET")
|
||
|
r.HandleFunc("/settings/account", requireLogin(editAccountSettings)).Methods("POST")
|
||
|
r.HandleFunc("/blocked", requireLogin(showBlocked)).Methods("GET")
|
||
|
|
||
|
// Help page routes.
|
||
|
r.HandleFunc("/help/rules", useLogin(showRulesPage)).Methods("GET")
|
||
|
r.HandleFunc("/help/faq", useLogin(showFAQPage)).Methods("GET")
|
||
|
r.HandleFunc("/help/legal", useLogin(showLegalPage)).Methods("GET")
|
||
|
r.HandleFunc("/help/contact", useLogin(showContactPage)).Methods("GET")
|
||
|
|
||
|
// Image upload route.
|
||
|
r.HandleFunc("/upload", uploadImage).Methods("POST")
|
||
|
|
||
|
// Admin routes.
|
||
|
r.HandleFunc("/admin", requireLogin(showAdminDashboard)).Methods("GET")
|
||
|
r.HandleFunc("/reports/{id:[0-9]+}/ignore", requireLogin(reportIgnore)).Methods("POST")
|
||
|
r.HandleFunc("/admin/manage", requireLogin(showAdminManagerList)).Methods("GET")
|
||
|
r.HandleFunc("/admin/manage/bantemp", requireLogin(adminBanUser)).Methods("POST")
|
||
|
r.HandleFunc("/admin/manage/unbantemp", requireLogin(adminUnbanUser)).Methods("POST")
|
||
|
//r.HandleFunc("/admin/manage/{table}", requireLogin(showAdminManager)).Methods("GET")
|
||
|
//r.HandleFunc("/admin/manage/{table}/{id:[0-9]+}", requireLogin(showAdminEditor)).Methods("GET", "POST")
|
||
|
r.HandleFunc("/admin/settings", requireLogin(showAdminSettings)).Methods("GET", "POST")
|
||
|
r.HandleFunc("/admin/audit_log", requireLogin(showAdminAuditLog)).Methods("GET")
|
||
|
|
||
|
// Websocket route.
|
||
|
r.HandleFunc("/ws", requireLogin(handleConnections)).Methods("GET")
|
||
|
|
||
|
// Add a 404 page.
|
||
|
r.NotFoundHandler = useLogin(handle404)
|
||
|
|
||
|
// Serve static assets.
|
||
|
r.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("assets"))))
|
||
|
// serve images as /images even though this can be changed
|
||
|
r.PathPrefix("/images/").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir("images"))))
|
||
|
|
||
|
if !settings.CSRFProtectDisable {
|
||
|
r.Use(CSRF)
|
||
|
}
|
||
|
if settings.GzipEnabled {
|
||
|
r.Use(gziphandler.GzipHandler)
|
||
|
}
|
||
|
|
||
|
// Tell the http server to handle routing with the router we just made.
|
||
|
http.Handle("/", r)
|
||
|
// Tell the person who started this that we are starting the server.
|
||
|
log.Printf("listening on " + settings.Port)
|
||
|
|
||
|
// Start the server.
|
||
|
if settings.ListenSocket {
|
||
|
// remove tha socket first or else
|
||
|
os.Remove(settings.Port)
|
||
|
|
||
|
unixListener, err := net.Listen("unix", settings.Port)
|
||
|
if err != nil {
|
||
|
log.Fatal("cannot listen on unix socket: ", err)
|
||
|
}
|
||
|
|
||
|
// set socket owner but only if the value is not blank
|
||
|
if settings.SocketOwner != "" {
|
||
|
socketUser, err := osUser.Lookup(settings.SocketOwner)
|
||
|
if err != nil {
|
||
|
log.Fatal("could not look up user so that we can change the owner of the unix socket so that we can listen on it:\n", err)
|
||
|
}
|
||
|
// should probably handle errors here
|
||
|
uidInt, _ := strconv.Atoi(socketUser.Uid)
|
||
|
gidInt, _ := strconv.Atoi(socketUser.Gid)
|
||
|
err = os.Chown(settings.Port, uidInt, gidInt)
|
||
|
if err != nil {
|
||
|
log.Fatal("could not change socket owner", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
err = http.Serve(unixListener, nil) // Just serve HTTP requests.
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
} else {
|
||
|
if settings.SSL.Enabled && settings.Port != ":80" {
|
||
|
go http.ListenAndServe(":80", http.HandlerFunc(redirect)) // Redirect HTTP requests to the HTTPS site.
|
||
|
err = http.ListenAndServeTLS(settings.Port, settings.SSL.Certificate, settings.SSL.Key, nil)
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
} else {
|
||
|
log.Fatal(http.ListenAndServe(settings.Port, nil))
|
||
|
}
|
||
|
}
|
||
|
}
|