package panel import ( "database/sql" "errors" "log" "net/http" "strconv" "time" "github.com/Azareal/Gosora/common" "github.com/Azareal/Gosora/common/phrases" "github.com/Azareal/Gosora/query_gen" ) // TODO: Move this to another file, probably common/pages.go type AnalyticsTimeRange struct { Quantity int Unit string Slices int SliceWidth int Range string } func analyticsTimeRange(rawTimeRange string) (timeRange AnalyticsTimeRange, err error) { timeRange.Quantity = 6 timeRange.Unit = "hour" timeRange.Slices = 12 timeRange.SliceWidth = 60 * 30 timeRange.Range = "six-hours" switch rawTimeRange { case "one-month": timeRange.Quantity = 30 timeRange.Unit = "day" timeRange.Slices = 30 timeRange.SliceWidth = 60 * 60 * 24 timeRange.Range = "one-month" case "one-week": timeRange.Quantity = 7 timeRange.Unit = "day" timeRange.Slices = 14 timeRange.SliceWidth = 60 * 60 * 12 timeRange.Range = "one-week" case "two-days": // Two days is experimental timeRange.Quantity = 2 timeRange.Unit = "day" timeRange.Slices = 24 timeRange.SliceWidth = 60 * 60 * 2 timeRange.Range = "two-days" case "one-day": timeRange.Quantity = 1 timeRange.Unit = "day" timeRange.Slices = 24 timeRange.SliceWidth = 60 * 60 timeRange.Range = "one-day" case "twelve-hours": timeRange.Quantity = 12 timeRange.Slices = 24 timeRange.Range = "twelve-hours" case "six-hours", "": timeRange.Range = "six-hours" default: return timeRange, errors.New("Unknown time range") } return timeRange, nil } func analyticsTimeRangeToLabelList(timeRange AnalyticsTimeRange) (revLabelList []int64, labelList []int64, viewMap map[int64]int64) { viewMap = make(map[int64]int64) var currentTime = time.Now().Unix() for i := 1; i <= timeRange.Slices; i++ { var label = currentTime - int64(i*timeRange.SliceWidth) revLabelList = append(revLabelList, label) viewMap[label] = 0 } for _, value := range revLabelList { labelList = append(labelList, value) } return revLabelList, labelList, viewMap } func analyticsRowsToViewMap(rows *sql.Rows, labelList []int64, viewMap map[int64]int64) (map[int64]int64, error) { defer rows.Close() for rows.Next() { var count int64 var createdAt time.Time err := rows.Scan(&count, &createdAt) if err != nil { return viewMap, err } var unixCreatedAt = createdAt.Unix() // TODO: Bulk log this if common.Dev.SuperDebug { log.Print("count: ", count) log.Print("createdAt: ", createdAt) log.Print("unixCreatedAt: ", unixCreatedAt) } for _, value := range labelList { if unixCreatedAt > value { viewMap[value] += count break } } } return viewMap, rows.Err() } func PreAnalyticsDetail(w http.ResponseWriter, r *http.Request, user *common.User) (*common.BasePanelPage, common.RouteError) { basePage, ferr := buildBasePage(w, r, user, "analytics", "analytics") if ferr != nil { return nil, ferr } basePage.AddSheet("chartist/chartist.min.css") basePage.AddScript("chartist/chartist.min.js") basePage.AddScript("analytics.js") return basePage, nil } func AnalyticsViews(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) common.DebugLog("in panel.AnalyticsViews") // TODO: Add some sort of analytics store / iterator? rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, createdAt").Where("route = ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } var viewList []int64 var viewItems []common.PanelAnalyticsItem for _, value := range revLabelList { viewList = append(viewList, viewMap[value]) viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) } graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} return renderTemplate("panel_analytics_views", w, r, basePage.Header, &pi) } func AnalyticsRouteViews(w http.ResponseWriter, r *http.Request, user common.User, route string) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) common.DebugLog("in panel.AnalyticsRouteViews") // TODO: Validate the route is valid rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, createdAt").Where("route = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(route) if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } var viewList []int64 var viewItems []common.PanelAnalyticsItem for _, value := range revLabelList { viewList = append(viewList, viewMap[value]) viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) } graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) pi := common.PanelAnalyticsRoutePage{basePage, common.SanitiseSingleLine(route), graph, viewItems, timeRange.Range} return renderTemplate("panel_analytics_route_views", w, r, basePage.Header, &pi) } func AnalyticsAgentViews(w http.ResponseWriter, r *http.Request, user common.User, agent string) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) // ? Only allow valid agents? The problem with this is that agents wind up getting renamed and it would take a migration to get them all up to snuff agent = common.SanitiseSingleLine(agent) common.DebugLog("in panel.AnalyticsAgentViews") // TODO: Verify the agent is valid rows, err := qgen.NewAcc().Select("viewchunks_agents").Columns("count, createdAt").Where("browser = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(agent) if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } var viewList []int64 for _, value := range revLabelList { viewList = append(viewList, viewMap[value]) } graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) friendlyAgent, ok := phrases.GetUserAgentPhrase(agent) if !ok { friendlyAgent = agent } pi := common.PanelAnalyticsAgentPage{basePage, agent, friendlyAgent, graph, timeRange.Range} return renderTemplate("panel_analytics_agent_views", w, r, basePage.Header, &pi) } func AnalyticsForumViews(w http.ResponseWriter, r *http.Request, user common.User, sfid string) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) fid, err := strconv.Atoi(sfid) if err != nil { return common.LocalError("Invalid integer", w, r, user) } common.DebugLog("in panel.AnalyticsForumViews") // TODO: Verify the agent is valid rows, err := qgen.NewAcc().Select("viewchunks_forums").Columns("count, createdAt").Where("forum = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(fid) if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } var viewList []int64 for _, value := range revLabelList { viewList = append(viewList, viewMap[value]) } graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) forum, err := common.Forums.Get(fid) if err != nil { return common.InternalError(err, w, r) } pi := common.PanelAnalyticsAgentPage{basePage, sfid, forum.Name, graph, timeRange.Range} return renderTemplate("panel_analytics_forum_views", w, r, basePage.Header, &pi) } func AnalyticsSystemViews(w http.ResponseWriter, r *http.Request, user common.User, system string) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) system = common.SanitiseSingleLine(system) common.DebugLog("in panel.AnalyticsSystemViews") // TODO: Verify the OS name is valid rows, err := qgen.NewAcc().Select("viewchunks_systems").Columns("count, createdAt").Where("system = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(system) if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } var viewList []int64 for _, value := range revLabelList { viewList = append(viewList, viewMap[value]) } graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) friendlySystem, ok := phrases.GetOSPhrase(system) if !ok { friendlySystem = system } pi := common.PanelAnalyticsAgentPage{basePage, system, friendlySystem, graph, timeRange.Range} return renderTemplate("panel_analytics_system_views", w, r, basePage.Header, &pi) } func AnalyticsLanguageViews(w http.ResponseWriter, r *http.Request, user common.User, lang string) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) lang = common.SanitiseSingleLine(lang) common.DebugLog("in panel.AnalyticsLanguageViews") // TODO: Verify the language code is valid rows, err := qgen.NewAcc().Select("viewchunks_langs").Columns("count, createdAt").Where("lang = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(lang) if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } var viewList []int64 for _, value := range revLabelList { viewList = append(viewList, viewMap[value]) } graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) friendlyLang, ok := phrases.GetHumanLangPhrase(lang) if !ok { friendlyLang = lang } pi := common.PanelAnalyticsAgentPage{basePage, lang, friendlyLang, graph, timeRange.Range} return renderTemplate("panel_analytics_lang_views", w, r, basePage.Header, &pi) } func AnalyticsReferrerViews(w http.ResponseWriter, r *http.Request, user common.User, domain string) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) common.DebugLog("in panel.AnalyticsReferrerViews") // TODO: Verify the agent is valid rows, err := qgen.NewAcc().Select("viewchunks_referrers").Columns("count, createdAt").Where("domain = ?").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query(domain) if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } var viewList []int64 for _, value := range revLabelList { viewList = append(viewList, viewMap[value]) } graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) pi := common.PanelAnalyticsAgentPage{basePage, common.SanitiseSingleLine(domain), "", graph, timeRange.Range} return renderTemplate("panel_analytics_referrer_views", w, r, basePage.Header, &pi) } func AnalyticsTopics(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) common.DebugLog("in panel.AnalyticsTopics") rows, err := qgen.NewAcc().Select("topicchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } var viewList []int64 var viewItems []common.PanelAnalyticsItem for _, value := range revLabelList { viewList = append(viewList, viewMap[value]) viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) } graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} return renderTemplate("panel_analytics_topics", w, r, basePage.Header, &pi) } func AnalyticsPosts(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := PreAnalyticsDetail(w, r, &user) if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } revLabelList, labelList, viewMap := analyticsTimeRangeToLabelList(timeRange) common.DebugLog("in panel.AnalyticsPosts") rows, err := qgen.NewAcc().Select("postchunks").Columns("count, createdAt").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } viewMap, err = analyticsRowsToViewMap(rows, labelList, viewMap) if err != nil { return common.InternalError(err, w, r) } var viewList []int64 var viewItems []common.PanelAnalyticsItem for _, value := range revLabelList { viewList = append(viewList, viewMap[value]) viewItems = append(viewItems, common.PanelAnalyticsItem{Time: value, Count: viewMap[value]}) } graph := common.PanelTimeGraph{Series: viewList, Labels: labelList} common.DebugLogf("graph: %+v\n", graph) pi := common.PanelAnalyticsPage{basePage, graph, viewItems, timeRange.Range} return renderTemplate("panel_analytics_posts", w, r, basePage.Header, &pi) } func analyticsRowsToNameMap(rows *sql.Rows) (map[string]int, error) { nameMap := make(map[string]int) defer rows.Close() for rows.Next() { var count int var name string err := rows.Scan(&count, &name) if err != nil { return nameMap, err } // TODO: Bulk log this if common.Dev.SuperDebug { log.Print("count: ", count) log.Print("name: ", name) } nameMap[name] += count } return nameMap, rows.Err() } func AnalyticsForums(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } rows, err := qgen.NewAcc().Select("viewchunks_forums").Columns("count, forum").Where("forum != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } forumMap, err := analyticsRowsToNameMap(rows) if err != nil { return common.InternalError(err, w, r) } // TODO: Sort this slice var forumItems []common.PanelAnalyticsAgentsItem for sfid, count := range forumMap { fid, err := strconv.Atoi(sfid) if err != nil { return common.InternalError(err, w, r) } forum, err := common.Forums.Get(fid) if err != nil { return common.InternalError(err, w, r) } forumItems = append(forumItems, common.PanelAnalyticsAgentsItem{ Agent: sfid, FriendlyAgent: forum.Name, Count: count, }) } pi := common.PanelAnalyticsAgentsPage{basePage, forumItems, timeRange.Range} return renderTemplate("panel_analytics_forums", w, r, basePage.Header, &pi) } func AnalyticsRoutes(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } rows, err := qgen.NewAcc().Select("viewchunks").Columns("count, route").Where("route != ''").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } routeMap, err := analyticsRowsToNameMap(rows) if err != nil { return common.InternalError(err, w, r) } // TODO: Sort this slice var routeItems []common.PanelAnalyticsRoutesItem for route, count := range routeMap { routeItems = append(routeItems, common.PanelAnalyticsRoutesItem{ Route: route, Count: count, }) } pi := common.PanelAnalyticsRoutesPage{basePage, routeItems, timeRange.Range} return renderTemplate("panel_analytics_routes", w, r, basePage.Header, &pi) } func AnalyticsAgents(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } rows, err := qgen.NewAcc().Select("viewchunks_agents").Columns("count, browser").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } agentMap, err := analyticsRowsToNameMap(rows) if err != nil { return common.InternalError(err, w, r) } // TODO: Sort this slice var agentItems []common.PanelAnalyticsAgentsItem for agent, count := range agentMap { aAgent, ok := phrases.GetUserAgentPhrase(agent) if !ok { aAgent = agent } agentItems = append(agentItems, common.PanelAnalyticsAgentsItem{ Agent: agent, FriendlyAgent: aAgent, Count: count, }) } pi := common.PanelAnalyticsAgentsPage{basePage, agentItems, timeRange.Range} return renderTemplate("panel_analytics_agents", w, r, basePage.Header, &pi) } func AnalyticsSystems(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } rows, err := qgen.NewAcc().Select("viewchunks_systems").Columns("count, system").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } osMap, err := analyticsRowsToNameMap(rows) if err != nil { return common.InternalError(err, w, r) } // TODO: Sort this slice var systemItems []common.PanelAnalyticsAgentsItem for system, count := range osMap { sSystem, ok := phrases.GetOSPhrase(system) if !ok { sSystem = system } systemItems = append(systemItems, common.PanelAnalyticsAgentsItem{ Agent: system, FriendlyAgent: sSystem, Count: count, }) } pi := common.PanelAnalyticsAgentsPage{basePage, systemItems, timeRange.Range} return renderTemplate("panel_analytics_systems", w, r, basePage.Header, &pi) } func AnalyticsLanguages(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } rows, err := qgen.NewAcc().Select("viewchunks_langs").Columns("count, lang").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } langMap, err := analyticsRowsToNameMap(rows) if err != nil { return common.InternalError(err, w, r) } // TODO: Can we de-duplicate these analytics functions further? // TODO: Sort this slice var langItems []common.PanelAnalyticsAgentsItem for lang, count := range langMap { lLang, ok := phrases.GetHumanLangPhrase(lang) if !ok { lLang = lang } langItems = append(langItems, common.PanelAnalyticsAgentsItem{ Agent: lang, FriendlyAgent: lLang, Count: count, }) } pi := common.PanelAnalyticsAgentsPage{basePage, langItems, timeRange.Range} return renderTemplate("panel_analytics_langs", w, r, basePage.Header, &pi) } func AnalyticsReferrers(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { basePage, ferr := buildBasePage(w, r, &user, "analytics", "analytics") if ferr != nil { return ferr } timeRange, err := analyticsTimeRange(r.FormValue("timeRange")) if err != nil { return common.LocalError(err.Error(), w, r, user) } rows, err := qgen.NewAcc().Select("viewchunks_referrers").Columns("count, domain").DateCutoff("createdAt", timeRange.Quantity, timeRange.Unit).Query() if err != nil && err != sql.ErrNoRows { return common.InternalError(err, w, r) } refMap, err := analyticsRowsToNameMap(rows) if err != nil { return common.InternalError(err, w, r) } // TODO: Sort this slice var refItems []common.PanelAnalyticsAgentsItem for domain, count := range refMap { refItems = append(refItems, common.PanelAnalyticsAgentsItem{ Agent: common.SanitiseSingleLine(domain), Count: count, }) } pi := common.PanelAnalyticsAgentsPage{basePage, refItems, timeRange.Range} return renderTemplate("panel_analytics_referrers", w, r, basePage.Header, &pi) }