package db
import (
"database/sql"
"fmt"
"html/template"
"io/ioutil"
"os"
"regexp"
"strings"
"time"
"github.com/FChannel0/FChannel-Server/activitypub"
"github.com/FChannel0/FChannel-Server/config"
"github.com/FChannel0/FChannel-Server/util"
_ "github.com/lib/pq"
)
type NewsItem struct {
Title string
Content template.HTML
Time int
}
func Connect() error {
host := config.DBHost
port := config.DBPort
user := config.DBUser
password := config.DBPassword
dbname := config.DBName
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s "+
"dbname=%s sslmode=disable", host, port, user, password, dbname)
_db, err := sql.Open("postgres", psqlInfo)
if err != nil {
return util.MakeError(err, "Connect")
}
if err := _db.Ping(); err != nil {
return util.MakeError(err, "Connect")
}
config.Log.Println("Successfully connected DB")
config.DB = _db
return nil
}
func Close() error {
err := config.DB.Close()
return util.MakeError(err, "Close")
}
func RunDatabaseSchema() error {
query, err := ioutil.ReadFile("databaseschema.psql")
if err != nil {
return util.MakeError(err, "RunDatabaseSchema")
}
_, err = config.DB.Exec(string(query))
return util.MakeError(err, "RunDatabaseSchema")
}
func CreateNewBoard(actor activitypub.Actor) (activitypub.Actor, error) {
if _, err := activitypub.GetActorFromDB(actor.Id); err == nil {
return activitypub.Actor{}, util.MakeError(err, "CreateNewBoardDB")
} else {
query := `insert into actor (type, id, name, preferedusername, inbox, outbox, following, followers, summary, restricted) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`
_, err := config.DB.Exec(query, actor.Type, actor.Id, actor.Name, actor.PreferredUsername, actor.Inbox, actor.Outbox, actor.Following, actor.Followers, actor.Summary, actor.Restricted)
if err != nil {
return activitypub.Actor{}, util.MakeError(err, "CreateNewBoardDB")
}
config.Log.Println("board added")
for _, e := range actor.AuthRequirement {
query = `insert into actorauth (type, board) values ($1, $2)`
if _, err := config.DB.Exec(query, e, actor.Name); err != nil {
return activitypub.Actor{}, util.MakeError(err, "CreateNewBoardDB")
}
}
{
var verify util.Verify
verify.Type = "admin"
verify.Identifier = actor.Id
if err := actor.CreateVerification(verify); err != nil {
return activitypub.Actor{}, util.MakeError(err, "CreateNewBoardDB")
}
}
{
var verify util.Verify
verify.Type = "janitor"
verify.Identifier = actor.Id
if err := actor.CreateVerification(verify); err != nil {
return activitypub.Actor{}, util.MakeError(err, "CreateNewBoardDB")
}
}
activitypub.CreatePem(actor)
if actor.Name != "main" {
var nObject activitypub.ObjectBase
var nActivity activitypub.Activity
nActor, err := activitypub.GetActorFromDB(config.Domain)
if err != nil {
return actor, util.MakeError(err, "CreateNewBoardDB")
}
nActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams"
nActivity.Type = "Follow"
nActivity.Actor = &nActor
nActivity.Object = &nObject
mActor, err := activitypub.GetActorFromDB(actor.Id)
if err != nil {
return actor, util.MakeError(err, "CreateNewBoardDB")
}
nActivity.Object.Actor = mActor.Id
nActivity.To = append(nActivity.To, actor.Id)
activityRequest := nActivity.AcceptFollow()
if _, err := activityRequest.SetActorFollowing(); err != nil {
return actor, util.MakeError(err, "CreateNewBoardDB")
}
if err := activityRequest.MakeRequestInbox(); err != nil {
return actor, util.MakeError(err, "CreateNewBoardDB")
}
}
}
return actor, nil
}
func RemovePreviewFromFile(id string) error {
var href string
query := `select href from activitystream where id in (select preview from activitystream where id=$1)`
if err := config.DB.QueryRow(query, id).Scan(&href); err != nil {
return nil
}
href = strings.Replace(href, config.Domain+"/", "", 1)
if href != "static/notfound.png" {
if _, err := os.Stat(href); err != nil {
return util.MakeError(err, "RemovePreviewFromFile")
}
err := os.Remove(href)
return util.MakeError(err, "RemovePreviewFromFile")
}
obj := activitypub.ObjectBase{Id: id}
err := obj.DeletePreview()
return util.MakeError(err, "RemovePreviewFromFile")
}
//if limit less than 1 return all news items
func GetNews(limit int) ([]NewsItem, error) {
var news []NewsItem
var query string
var rows *sql.Rows
var err error
if limit > 0 {
query = `select title, content, time from newsItem order by time desc limit $1`
rows, err = config.DB.Query(query, limit)
} else {
query = `select title, content, time from newsItem order by time desc`
rows, err = config.DB.Query(query)
}
if err != nil {
return news, util.MakeError(err, "GetNews")
}
defer rows.Close()
for rows.Next() {
var content string
n := NewsItem{}
if err := rows.Scan(&n.Title, &content, &n.Time); err != nil {
return news, util.MakeError(err, "GetNews")
}
content = strings.ReplaceAll(content, "\n", "
")
n.Content = template.HTML(content)
news = append(news, n)
}
return news, nil
}
func GetNewsItem(timestamp int) (NewsItem, error) {
var news NewsItem
var content string
query := `select title, content, time from newsItem where time=$1 limit 1`
if err := config.DB.QueryRow(query, timestamp).Scan(&news.Title, &content, &news.Time); err != nil {
return news, util.MakeError(err, "GetNewsItem")
}
content = strings.ReplaceAll(content, "\n", "
")
news.Content = template.HTML(content)
return news, nil
}
func DeleteNewsItem(timestamp int) error {
query := `delete from newsItem where time=$1`
_, err := config.DB.Exec(query, timestamp)
return util.MakeError(err, "DeleteNewsItem")
}
func WriteNews(news NewsItem) error {
query := `insert into newsItem (title, content, time) values ($1, $2, $3)`
_, err := config.DB.Exec(query, news.Title, news.Content, time.Now().Unix())
return util.MakeError(err, "WriteNews")
}
func AddInstanceToInactive(instance string) error {
var timeStamp string
query := `select timestamp from inactive where instance=$1`
if err := config.DB.QueryRow(query, instance).Scan(&timeStamp); err != nil {
query := `insert into inactive (instance, timestamp) values ($1, $2)`
_, err := config.DB.Exec(query, instance, time.Now().UTC().Format(time.RFC3339))
return util.MakeError(err, "AddInstanceToInactive")
}
if !IsInactiveTimestamp(timeStamp) {
return nil
}
query = `delete from follower where follower like $1`
if _, err := config.DB.Exec(query, "%"+instance+"%"); err != nil {
return util.MakeError(err, "AddInstanceToInactive")
}
err := DeleteInstanceFromInactive(instance)
return util.MakeError(err, "AddInstanceToInactive")
}
func DeleteInstanceFromInactive(instance string) error {
query := `delete from inactive where instance=$1`
_, err := config.DB.Exec(query, instance)
return util.MakeError(err, "DeleteInstanceFromInactive")
}
func IsInactiveTimestamp(timeStamp string) bool {
stamp, _ := time.Parse(time.RFC3339, timeStamp)
if time.Now().UTC().Sub(stamp).Hours() > 48 {
return true
}
return false
}
func IsReplyToOP(op string, link string) (string, bool, error) {
var id string
if op == link {
return link, true, nil
}
re := regexp.MustCompile(`f(\w+)\-`)
match := re.FindStringSubmatch(link)
if len(match) > 0 {
re := regexp.MustCompile(`(.+)\-`)
link = re.ReplaceAllString(link, "")
link = "%" + match[1] + "/" + link
}
query := `select id from replies where id like $1 and inreplyto=$2`
if err := config.DB.QueryRow(query, link, op).Scan(&id); err != nil {
return op, false, nil
}
return id, id != "", nil
}
func GetReplyOP(link string) (string, error) {
var id string
query := `select id from replies where id in (select inreplyto from replies where id=$1) and inreplyto=''`
if err := config.DB.QueryRow(query, link).Scan(&id); err != nil {
return "", nil
}
return id, nil
}
func CheckInactive() {
for true {
CheckInactiveInstances()
time.Sleep(24 * time.Hour)
}
}
func CheckInactiveInstances() (map[string]string, error) {
var rows *sql.Rows
var err error
instances := make(map[string]string)
query := `select following from following`
if rows, err = config.DB.Query(query); err != nil {
return instances, util.MakeError(err, "CheckInactiveInstances")
}
defer rows.Close()
for rows.Next() {
var instance string
if err := rows.Scan(&instance); err != nil {
return instances, util.MakeError(err, "CheckInactiveInstances")
}
instances[instance] = instance
}
query = `select follower from follower`
if rows, err = config.DB.Query(query); err != nil {
return instances, util.MakeError(err, "CheckInactiveInstances")
}
defer rows.Close()
for rows.Next() {
var instance string
if err := rows.Scan(&instance); err != nil {
return instances, util.MakeError(err, "CheckInactiveInstances")
}
instances[instance] = instance
}
re := regexp.MustCompile(config.Domain + `(.+)?`)
for _, e := range instances {
actor, err := activitypub.GetActor(e)
if err != nil {
return instances, util.MakeError(err, "CheckInactiveInstances")
}
if actor.Id == "" && !re.MatchString(e) {
if err := AddInstanceToInactive(e); err != nil {
return instances, util.MakeError(err, "CheckInactiveInstances")
}
} else {
if err := DeleteInstanceFromInactive(e); err != nil {
return instances, util.MakeError(err, "CheckInactiveInstances")
}
}
}
return instances, nil
}
func GetAdminAuth() (string, string, error) {
var code string
var identifier string
query := `select identifier, code from boardaccess where board=$1 and type='admin'`
if err := config.DB.QueryRow(query, config.Domain).Scan(&identifier, &code); err != nil {
return "", "", nil
}
return code, identifier, nil
}
func IsHashBanned(hash string) (bool, error) {
var h string
query := `select hash from bannedmedia where hash=$1`
_ = config.DB.QueryRow(query, hash).Scan(&h)
return h == hash, nil
}
func PrintAdminAuth() error {
code, identifier, err := GetAdminAuth()
if err != nil {
return util.MakeError(err, "PrintAdminAuth")
}
config.Log.Println("Mod key: " + config.Key)
config.Log.Println("Admin Login: " + identifier + ", Code: " + code)
return nil
}
func InitInstance() error {
if config.InstanceName != "" {
if _, err := CreateNewBoard(*activitypub.CreateNewActor("", config.InstanceName, config.InstanceSummary, config.AuthReq, false)); err != nil {
return util.MakeError(err, "InitInstance")
}
if config.PublicIndexing == "true" {
// TODO: comment out later
//AddInstanceToIndex(config.Domain)
}
}
return nil
}