fedilogue/restapi/restapi.go

430 lines
12 KiB
Go
Raw Permalink Normal View History

2021-02-13 02:49:36 +00:00
package main
import (
2021-06-30 22:28:53 +00:00
"context"
"encoding/json"
2021-02-13 02:49:36 +00:00
"fmt"
"log"
"net/http"
"strconv"
2021-06-30 22:28:53 +00:00
"time"
"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/opts"
"github.com/go-echarts/go-echarts/v2/types"
2021-02-13 02:49:36 +00:00
"github.com/jackc/pgx/v4"
)
2023-07-13 16:29:18 +00:00
type InstanceStatsJson struct {
Timestamp time.Time `json:"timestamp"`
2023-07-13 17:18:29 +00:00
Instance string `json:"timestamp"`
2023-07-13 16:29:18 +00:00
ActivitiesCount int `json:"activitiescount"`
ActorsCount int `json:"actorscount"`
}
2023-07-13 17:18:29 +00:00
type ActorStatsJson struct {
Timestamp time.Time `json:"timestamp"`
Actor string `json:"actor"`
ActivitiesCount int `json:"activitiescount"`
}
2023-07-13 17:57:03 +00:00
type GlobalStatsJson struct {
Timestamp time.Time `json:"timestamp"`
ActivitiesCount int `json:"activitiescount"`
ActorsCount int `json:"actorscount"`
}
2021-12-19 21:36:10 -05:00
var metricsText string
2021-06-30 22:28:53 +00:00
func enableCors(w *http.ResponseWriter) {
(*w).Header().Set("Access-Control-Allow-Origin", "*")
}
2025-01-30 02:11:26 +00:00
func setJsonType(w *http.ResponseWriter) {
(*w).Header().Set("Content-Type", "application/json")
}
2021-12-18 04:34:04 +00:00
func runMetrics() {
2021-12-19 21:36:10 -05:00
hashtagtotal := runTrendingHashtags()
wordstotal := make(map[string]interface{})
2021-12-19 21:36:10 -05:00
2021-12-18 04:34:04 +00:00
totalJson := make(map[string]interface{})
totalJson["hashtags"] = hashtagtotal
2021-12-19 21:36:10 -05:00
totalJson["words"] = wordstotal
totalJson["datetime"] = time.Now().UTC()
2021-12-17 00:45:52 -05:00
2021-12-18 04:34:04 +00:00
data, err := json.Marshal(totalJson)
if err != nil {
2023-10-18 00:10:15 -04:00
log.Fatalf("error marshaling combined activity 1: %v\n", err)
2021-12-17 00:45:52 -05:00
}
2021-12-19 21:36:10 -05:00
metricsText = string(data)
2021-12-18 04:34:04 +00:00
}
2021-06-30 22:28:53 +00:00
2021-12-19 21:36:10 -05:00
func runTrendingHashtags() map[string]interface{} {
sql := `WITH taglist AS (SELECT DISTINCT unnest(hashtags) AS tag, activities.document->>'attributedTo' AS attributed
FROM activities JOIN actors ON activities.document->>'attributedTo'=actors.document->>'id'
WHERE actors.bot=False AND activities.identifiedAt > NOW() - INTERVAL '30 MINUTES')
SELECT tag, COUNT(*) FROM taglist GROUP BY tag ORDER BY count DESC LIMIT 100;`
2021-06-30 22:28:53 +00:00
2021-12-18 04:34:04 +00:00
rows, err := pool.Query(context.Background(), sql)
if err != nil {
panic(err)
}
2021-06-30 22:28:53 +00:00
2023-07-13 16:29:18 +00:00
hashtagitems := make([]interface{}, 0)
2021-12-18 04:34:04 +00:00
hashcount := 0
for rows.Next() {
var hashtag string
var count int
2021-06-30 22:28:53 +00:00
2021-12-18 04:34:04 +00:00
err = rows.Scan(&hashtag, &count)
2021-06-30 22:28:53 +00:00
if err != nil {
2021-12-18 04:34:04 +00:00
panic(err)
2021-06-30 22:28:53 +00:00
}
2021-12-18 04:34:04 +00:00
hashtagitem := make(map[string]interface{})
hashtagitem["hashtag"] = hashtag
hashtagitem["count"] = count
hashtagitems = append(hashtagitems, hashtagitem)
hashcount = hashcount + 1
2021-06-30 22:28:53 +00:00
}
2021-12-18 04:34:04 +00:00
rows.Close()
2021-12-17 00:45:52 -05:00
2023-07-13 16:29:18 +00:00
hashtagtotal := make(map[string]interface{})
2021-12-18 04:34:04 +00:00
hashtagtotal["count"] = hashcount
hashtagtotal["items"] = hashtagitems
2021-12-17 00:45:52 -05:00
2021-12-18 04:34:04 +00:00
return hashtagtotal
2021-12-17 00:45:52 -05:00
}
2021-12-19 21:36:10 -05:00
func runTrendingWords() map[string]interface{} {
2021-12-19 21:36:10 -05:00
sql := `WITH popular_words AS (
select word FROM ts_stat(
'
SELECT to_tsvector(''simple'', normalized) FROM activities
LEFT JOIN actors ON activities.document->>''attributedTo''=actors.document->>''id''
WHERE activities.identifiedat > current_timestamp - interval ''60 minutes''
AND actors.bot=false
'
)
WHERE length(word) > 3
AND NOT word in (SELECT word FROM stopwords)
ORDER BY ndoc DESC LIMIT 100)
SELECT concat_ws(' ', a1.word, a2.word) phrase, count(*)
FROM popular_words AS a1
CROSS JOIN popular_words AS a2
CROSS JOIN activities
WHERE normalized ilike format('%%%s %s%%', a1.word, a2.word)
AND identifiedat > current_timestamp - interval '60 minutes'
GROUP BY 1
HAVING count(*) > 1
2022-01-02 10:52:31 -05:00
ORDER BY 2 DESC LIMIT 20;`
2021-12-19 21:36:10 -05:00
rows, err := pool.Query(context.Background(), sql)
if err != nil {
panic(err)
}
2023-07-13 16:29:18 +00:00
trendingitems := make([]interface{}, 0)
2021-12-19 21:36:10 -05:00
trendingcount := 0
for rows.Next() {
var trendingword string
var count int
err = rows.Scan(&trendingword, &count)
if err != nil {
panic(err)
}
trendingitem := make(map[string]interface{})
trendingitem["trending"] = trendingword
trendingitem["count"] = count
trendingitems = append(trendingitems, trendingitem)
trendingcount = trendingcount + 1
}
rows.Close()
2023-07-13 16:29:18 +00:00
trendingwordtotal := make(map[string]interface{})
2021-12-19 21:36:10 -05:00
trendingwordtotal["count"] = trendingcount
trendingwordtotal["items"] = trendingitems
return trendingwordtotal
}
2021-12-18 04:34:04 +00:00
// GET handlers
func getTrending(w http.ResponseWriter, r *http.Request) {
2021-12-17 00:45:52 -05:00
enableCors(&w)
2025-01-30 02:11:26 +00:00
setJsonType(&w)
2021-12-19 21:36:10 -05:00
fmt.Fprintf(w, "%s", metricsText)
2021-12-17 00:45:52 -05:00
}
2021-12-18 04:34:04 +00:00
func getSearch(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
2025-01-30 02:11:26 +00:00
setJsonType(&w)
searchkeys, exists_search := r.URL.Query()["s"]
offsetkeys, exists_offset := r.URL.Query()["o"]
2021-02-13 02:49:36 +00:00
var err error
var rows pgx.Rows
var searchKey string
if exists_search {
2021-02-13 02:49:36 +00:00
searchKey = searchkeys[0]
}
var offsetKey int
if exists_offset {
offsetKey, _ = strconv.Atoi(offsetkeys[0])
} else {
offsetKey = -1
}
2021-02-13 02:49:36 +00:00
if exists_search && searchKey != "" {
if offsetKey == -1 {
2025-01-30 02:11:26 +00:00
queryString := "SELECT activities.id, activities.document, actors.document, activities.instance FROM activities as activities INNER JOIN actors as actors ON activities.document->>'actor' = actors.document->>'id' WHERE activities.normalized_tsvector @@ plainto_tsquery($1) ORDER BY activities.id DESC LIMIT 10"
log.Println("Running query: \"", queryString, "\" ", searchKey)
rows, err = pool.Query(context.Background(), queryString, searchKey)
} else {
2025-01-30 02:11:26 +00:00
queryString := "SELECT activities.id, activities.document, actors.document, activities.instance, FROM activities as activities INNER JOIN actors as actors ON activities.document->>'actor' = actors.document->>'id' WHERE activities.normalized_tsvector @@ plainto_tsquery($1) AND activities.id < $2 ORDER BY activities.id DESC LIMIT 10"
log.Println("Running query: \"", queryString, "\" ", searchKey, offsetKey)
rows, err = pool.Query(context.Background(), queryString, searchKey, offsetKey)
}
2021-02-13 02:49:36 +00:00
} else {
if offsetKey == -1 {
2025-01-30 02:11:26 +00:00
queryString := "SELECT activities.id, activities.document, actors.document, activities.instance FROM activities as activities INNER JOIN actors as actors ON activities.document->>'actor' = actors.document->>'id' ORDER BY activities.id DESC LIMIT 10"
log.Println("Running query: \"", queryString, "\" ", queryString)
rows, err = pool.Query(context.Background(), queryString)
} else {
2025-01-30 02:11:26 +00:00
queryString := "SELECT activities.id, activities.document, actors.document, activities.instance FROM activities as activities INNER JOIN actors as actors ON activities.document->>'actor' = actors.document->>'id' AND activities.id < $1 ORDER BY activities.id DESC LIMIT 10"
log.Println("Running query: \"", queryString, "\" ", offsetKey)
rows, err = pool.Query(context.Background(), queryString, offsetKey)
}
2021-02-13 02:49:36 +00:00
}
if err != nil {
panic(err)
}
defer rows.Close()
var earliestid int
earliestid = 0
2021-02-13 02:49:36 +00:00
var activitiesJson []map[string]json.RawMessage
for rows.Next() {
var id int
var activityRaw string
var actorRaw string
2023-10-18 00:10:15 -04:00
var instance string
2021-02-13 02:49:36 +00:00
var activityJson map[string]json.RawMessage
2023-10-18 00:10:15 -04:00
err = rows.Scan(&id, &activityRaw, &actorRaw, &instance)
2021-02-13 02:49:36 +00:00
if err != nil {
panic(err)
}
err := json.Unmarshal([]byte(activityRaw), &activityJson)
if err != nil {
fmt.Println(err)
}
if earliestid == 0 {
earliestid = id
} else if earliestid > id {
earliestid = id
}
2021-02-13 02:49:36 +00:00
2023-10-18 00:10:15 -04:00
// Add the instance string
rawInstance, err := json.Marshal(instance)
if err != nil {
fmt.Println("Error marshaling instance string:", err)
return
}
activityJson["instance"] = json.RawMessage(rawInstance)
2021-02-13 02:49:36 +00:00
activityJson["actor"] = json.RawMessage(actorRaw)
activitiesJson = append(activitiesJson, activityJson)
}
requestData := make(map[string]int)
requestData["earliestid"] = earliestid
2021-06-30 22:28:53 +00:00
totalJson := make(map[string]interface{})
totalJson["requestdata"] = requestData
totalJson["activities"] = activitiesJson
data, err := json.Marshal(totalJson)
2021-02-13 02:49:36 +00:00
if err != nil {
2023-10-18 00:10:15 -04:00
log.Fatalf("error marshaling combined activity 2: %v\n", err)
2021-02-13 02:49:36 +00:00
}
fmt.Fprintf(w, "%s", data)
}
2023-07-13 16:29:18 +00:00
func getInstanceStats(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
2025-01-30 02:11:26 +00:00
setJsonType(&w)
2023-07-13 16:29:18 +00:00
instanceKeys, exists := r.URL.Query()["i"]
var instance string
if exists {
instance = instanceKeys[0]
}
instancestatsjson := &InstanceStatsJson{}
instancestatsjson.Timestamp = time.Now()
2023-07-13 17:18:29 +00:00
instancestatsjson.Instance = instance
2023-07-13 16:29:18 +00:00
if exists && instance != "" {
var activitiescount int
selectActivities := pool.QueryRow(context.Background(), "SELECT count(*) FROM activities WHERE instance = $1", instance)
err := selectActivities.Scan(&activitiescount)
if err != nil {
fmt.Println("Error ", err)
return
}
instancestatsjson.ActivitiesCount = activitiescount
var actorscount int
selectActors := pool.QueryRow(context.Background(), "SELECT count(*) FROM actors WHERE instance = $1", instance)
err = selectActors.Scan(&actorscount)
if err != nil {
fmt.Println("Error ", err)
return
}
instancestatsjson.ActorsCount = actorscount
}
bytearray, _ := json.Marshal(instancestatsjson)
stringarray := string(bytearray)
fmt.Fprintf(w, "%s", stringarray)
}
2023-07-13 17:18:29 +00:00
func getActorStats(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
2025-01-30 02:11:26 +00:00
setJsonType(&w)
2023-07-13 17:18:29 +00:00
actorKeys, exists := r.URL.Query()["a"]
var actor string
if exists {
actor = actorKeys[0]
}
actorstatsjson := &ActorStatsJson{}
actorstatsjson.Timestamp = time.Now()
actorstatsjson.Actor = actor
if exists && actor != "" {
var actorscount int
selectActivities := pool.QueryRow(context.Background(), "SELECT count(*) FROM activities WHERE document->>'attributedTo' = $1", actor)
err := selectActivities.Scan(&actorscount)
if err != nil {
fmt.Println("Error ", err)
return
}
actorstatsjson.ActivitiesCount = actorscount
}
bytearray, _ := json.Marshal(actorstatsjson)
stringarray := string(bytearray)
fmt.Fprintf(w, "%s", stringarray)
}
2023-07-13 17:57:03 +00:00
func getGlobalStats(w http.ResponseWriter, r *http.Request) {
enableCors(&w)
2025-01-30 02:11:26 +00:00
setJsonType(&w)
2023-07-13 17:57:03 +00:00
globalstatsjson := &GlobalStatsJson{}
globalstatsjson.Timestamp = time.Now()
var activitiescount int
selectActivities := pool.QueryRow(context.Background(), "SELECT count(*) FROM activities")
err := selectActivities.Scan(&activitiescount)
if err != nil {
fmt.Println("Error ", err)
return
}
globalstatsjson.ActivitiesCount = activitiescount
var actorscount int
selectActors := pool.QueryRow(context.Background(), "SELECT count(*) FROM actors")
err = selectActors.Scan(&actorscount)
if err != nil {
fmt.Println("Error ", err)
return
}
globalstatsjson.ActorsCount = actorscount
bytearray, _ := json.Marshal(globalstatsjson)
stringarray := string(bytearray)
fmt.Fprintf(w, "%s", stringarray)
}
func generateLineItems(counts []int) []opts.LineData {
items := make([]opts.LineData, 0)
for i := 0; i < len(counts); i++ {
items = append(items, opts.LineData{Value: counts[i]})
}
return items
}
func getGlobalGraph(w http.ResponseWriter, r *http.Request) {
rows, err := pool.Query(context.Background(), `select date_trunc('hour', identifiedat) as "HOURTIME", COUNT(*) from activities WHERE date_trunc('hour', identifiedat) > NOW() - interval '24 hour' AND date_trunc('hour', identifiedat) < date_trunc('hour', NOW()) group by "HOURTIME"`)
if err != nil {
panic(err)
}
defer rows.Close()
var timestamp time.Time
var count int
dates := []string{}
counts := []int{}
for rows.Next() {
err = rows.Scan(&timestamp, &count)
if err != nil {
panic(err)
}
dates = append(dates, timestamp.Format("2006-01-02 15:04:05"))
counts = append(counts, count)
}
line := charts.NewLine()
line.SetGlobalOptions(
charts.WithInitializationOpts(opts.Initialization{Theme: types.ThemeWesteros}),
charts.WithTitleOpts(opts.Title{
Title: "Posts Across the Fediverse",
}))
line.SetXAxis(dates).
AddSeries("Post Count", generateLineItems(counts)).
SetSeriesOptions(charts.WithLineChartOpts(opts.LineChart{Smooth: opts.Bool(false)}))
line.Render(w)
}
2021-02-13 02:49:36 +00:00
func main() {
pool = getDbPool()
2021-12-19 21:36:10 -05:00
metricsText = "[]"
2021-12-18 04:34:04 +00:00
go func() {
for {
runMetrics()
2021-12-19 21:36:10 -05:00
time.Sleep(10 * time.Minute)
2021-12-18 04:34:04 +00:00
}
}()
2021-06-30 22:28:53 +00:00
2021-12-18 04:34:04 +00:00
http.HandleFunc("/api/v1/search", getSearch)
http.HandleFunc("/api/v1/trending", getTrending)
2023-07-13 16:29:18 +00:00
http.HandleFunc("/api/v1/instance/stats", getInstanceStats)
2023-07-13 17:18:29 +00:00
http.HandleFunc("/api/v1/actor/stats", getActorStats)
2023-07-13 17:57:03 +00:00
http.HandleFunc("/api/v1/global/stats", getGlobalStats)
http.HandleFunc("/api/v1/global/graph", getGlobalGraph)
2021-06-30 22:28:53 +00:00
log.Print("Starting HTTP inbox on port http://0.0.0.0:6431")
log.Fatal(http.ListenAndServe("0.0.0.0:6431", nil))
2021-02-13 02:49:36 +00:00
}