From 8fb8ccafa3452d4987098ccef5c1c0bf247db555 Mon Sep 17 00:00:00 2001 From: FChannel <=> Date: Wed, 13 Jan 2021 17:09:43 -0800 Subject: initial commit --- .gitignore | 2 + Database.go | 868 +++++++++++++++++++++++++++++++++++++++++++++++ Follow.go | 222 ++++++++++++ OutboxPost.go | 545 ++++++++++++++++++++++++++++++ README.md | 0 activityPubStruct.go | 191 +++++++++++ databaseschema.psql | 135 ++++++++ main.go | 937 +++++++++++++++++++++++++++++++++++++++++++++++++++ outboxGet.go | 120 +++++++ verification.go | 456 +++++++++++++++++++++++++ 10 files changed, 3476 insertions(+) create mode 100644 .gitignore create mode 100644 Database.go create mode 100644 Follow.go create mode 100644 OutboxPost.go create mode 100644 README.md create mode 100644 activityPubStruct.go create mode 100644 databaseschema.psql create mode 100644 main.go create mode 100644 outboxGet.go create mode 100644 verification.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b4a572 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*~ +public/ diff --git a/Database.go b/Database.go new file mode 100644 index 0000000..6c3f711 --- /dev/null +++ b/Database.go @@ -0,0 +1,868 @@ +package main + +import "fmt" +import "database/sql" +import _ "github.com/lib/pq" +import "time" +import "os" +import "strings" + +func GetActorFromDB(db *sql.DB, id string) Actor { + + query := fmt.Sprintf("SELECT type, id, name, preferedusername, inbox, outbox, following, followers, restricted, summary from actor where id='%s'", id) + rows, err := db.Query(query) + + defer rows.Close() + + var nActor Actor + + if err != nil { + return nActor + } + + defer rows.Close() + for rows.Next() { + err = rows.Scan(&nActor.Type, &nActor.Id, &nActor.Name, &nActor.PreferredUsername, &nActor.Inbox, &nActor.Outbox, &nActor.Following, &nActor.Followers, &nActor.Restricted, &nActor.Summary) + CheckError(err, "error with actor from db scan ") + } + + nActor.AtContext.Context = "https://www.w3.org/ns/activitystreams" + + return nActor +} + +func CreateNewBoardDB(db *sql.DB, actor Actor) Actor{ + + query := fmt.Sprintf("INSERT INTO actor (type, id, name, preferedusername, inbox, outbox, following, followers, summary) values ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", actor.Type, actor.Id, actor.Name, actor.PreferredUsername, actor.Inbox, actor.Outbox, actor.Following, actor.Followers, actor.Summary) + + _, err := db.Exec(query) + + if err != nil { + fmt.Println("board exists") + } else { + fmt.Println("board added") + for _, e := range actor.AuthRequirement { + query = fmt.Sprintf("INSERT INTO actorauth (type, board) values ('%s', '%s')", e, actor.Name) + _, err := db.Exec(query) + CheckError(err, "auth exists") + } + + var verify Verify + + verify.Identifier = actor.Id + verify.Code = CreateKey(50) + verify.Type = "admin" + + CreateVerification(db, verify) + + verify.Identifier = actor.Id + verify.Code = CreateKey(50) + verify.Type = "janitor" + + CreateVerification(db, verify) + + var nverify Verify + nverify.Board = actor.Id + nverify.Identifier = "admin" + nverify.Type = "admin" + CreateBoardMod(db, nverify) + + nverify.Board = actor.Id + nverify.Identifier = "janitor" + nverify.Type = "janitor" + CreateBoardMod(db, nverify) + + if actor.Name != "main" { + var nActivity Activity + var nActor Actor + var nObject ObjectBase + + nActor.Id = Domain + nObject.Id = actor.Id + + nActivity.Actor = &nActor + nActivity.Object = &nObject + + SetActorFollowDB(db, nActivity, Domain) + } + } + + return actor +} + +func GetBoards(db *sql.DB) []Actor { + + var board []Actor + + query := fmt.Sprintf("select type, id, name, preferedusername, inbox, outbox, following, followers FROM actor") + + rows, err := db.Query(query) + + defer rows.Close() + + if err !=nil{ + panic(err) + } + + defer rows.Close() + + for rows.Next(){ + var actor = new(Actor) + + err = rows.Scan(&actor.Type, &actor.Id, &actor.Name, &actor.PreferredUsername, &actor.Inbox, &actor.Outbox, &actor.Following, &actor.Followers) + + if err !=nil{ + panic(err) + } + + board = append(board, *actor) + } + + return board +} + +func writeObjectToDB(db *sql.DB, obj ObjectBase) ObjectBase { + obj.Id = fmt.Sprintf("%s/%s", obj.Actor.Id, CreateUniqueID(db, obj.Actor.Id)) + if len(obj.Attachment) > 0 { + for i, _ := range obj.Attachment { + obj.Attachment[i].Id = fmt.Sprintf("%s/%s", obj.Actor.Id, CreateUniqueID(db, obj.Actor.Id)) + obj.Attachment[i].Published = time.Now().Format(time.RFC3339) + obj.Attachment[i].Updated = time.Now().Format(time.RFC3339) + writeAttachmentToDB(db, obj.Attachment[i]) + writeActivitytoDBWithAttachment(db, obj, obj.Attachment[i]) + } + } else { + writeActivitytoDB(db, obj) + } + + writeObjectReplyToDB(db, obj) + WriteWalletToDB(db, obj) + + return obj +} + +func WriteObjectUpdatesToDB(db *sql.DB, obj ObjectBase) { + query := fmt.Sprintf("update activitystream set updated='%s' where id='%s'", time.Now().Format(time.RFC3339), obj.Id) + _, e := db.Exec(query) + + if e != nil{ + fmt.Println("error inserting updating inreplyto") + panic(e) + } +} + +func WriteObjectReplyToLocalDB(db *sql.DB, id string, replyto string) { + query := fmt.Sprintf("insert into replies (id, inreplyto) values ('%s', '%s')", id, replyto) + + _, err := db.Exec(query) + + CheckError(err, "Could not insert local reply query") + + query = fmt.Sprintf("select inreplyto from replies where id='%s'", replyto) + + rows, _ := db.Query(query) + + CheckError(err, "Could not query select inreplyto") + + defer rows.Close() + + for rows.Next() { + var val string + rows.Scan(&val) + if val == "" { + updated := time.Now().Format(time.RFC3339) + query := fmt.Sprintf("update activitystream set updated='%s' where id='%s'", updated, replyto) + + _, err := db.Exec(query) + + CheckError(err, "error with updating replyto updated at date") + } + } +} + +func writeObjectReplyToDB(db *sql.DB, obj ObjectBase) { + for i, e := range obj.InReplyTo { + if(i == 0 || IsReplyInThread(db, obj.InReplyTo[0].Id, e.Id)){ + query := fmt.Sprintf("insert into replies (id, inreplyto) values ('%s', '%s')", obj.Id, e.Id) + _, err := db.Exec(query) + + if err != nil{ + fmt.Println("error inserting replies") + panic(err) + } + } + + update := true + for _, e := range obj.Option { + if e == "sage" || e == "nokosage" { + update = false + break + } + } + + if update { + WriteObjectUpdatesToDB(db, e) + } + } +} + + +func WriteWalletToDB(db *sql.DB, obj ObjectBase) { + for _, e := range obj.Option { + if e == "wallet" { + for _, e := range obj.Wallet { + query := fmt.Sprintf("insert into wallet (id, type, address) values ('%s', '%s', '%s')", obj.Id ,e.Type, e.Address) + _, err := db.Exec(query) + + CheckError(err, "error with write wallet query") + } + return + } + } +} + +func writeActivitytoDB(db *sql.DB, obj ObjectBase) { + + obj.Name = EscapeString(obj.Name) + obj.Content = EscapeString(obj.Content) + obj.AttributedTo = EscapeString(obj.AttributedTo) + + query := fmt.Sprintf("insert into activitystream (id, type, name, content, published, updated, attributedto, actor) values ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", obj.Id ,obj.Type, obj.Name, obj.Content, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor.Id) + + _, e := db.Exec(query) + + if e != nil{ + fmt.Println("error inserting new activity") + panic(e) + } +} + +func writeActivitytoDBWithAttachment(db *sql.DB, obj ObjectBase, attachment ObjectBase) { + + obj.Name = EscapeString(obj.Name) + obj.Content = EscapeString(obj.Content) + obj.AttributedTo = EscapeString(obj.AttributedTo) + + query := fmt.Sprintf("insert into activitystream (id, type, name, content, attachment, published, updated, attributedto, actor) values ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", obj.Id ,obj.Type, obj.Name, obj.Content, attachment.Id, obj.Published, obj.Updated, obj.AttributedTo, obj.Actor.Id) + + _, e := db.Exec(query) + + if e != nil{ + fmt.Println("error inserting new activity with attachment") + panic(e) + } +} + +func writeAttachmentToDB(db *sql.DB, obj ObjectBase) { + query := fmt.Sprintf("insert into activitystream (id, type, name, href, published, updated, attributedTo, mediatype, size) values ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d');", obj.Id ,obj.Type, obj.Name, obj.Href, obj.Published, obj.Updated, obj.AttributedTo, obj.MediaType, obj.Size) + + _, e := db.Exec(query) + + if e != nil{ + fmt.Println("error inserting new attachment") + panic(e) + } +} + +func GetActivityFromDB(db *sql.DB, id string) Collection { + var nColl Collection + var result []ObjectBase + + query := fmt.Sprintf("SELECT actor, id, name, content, type, published, updated, attributedto, attachment, actor FROM activitystream WHERE id='%s' ORDER BY updated asc;", id) + + rows, err := db.Query(query) + + defer rows.Close() + + CheckError(err, "error query object from db") + + defer rows.Close() + + for rows.Next(){ + var post ObjectBase + var actor Actor + var attachID string + + err = rows.Scan(&nColl.Actor, &post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &actor.Id) + + CheckError(err, "error scan object into post struct") + + post.Actor = &actor + + post.Replies = GetObjectRepliesDB(db, post) + + post.Replies.TotalItems, post.Replies.TotalImgs = GetObjectRepliesDBCount(db, post) + + post.Attachment = GetObjectAttachment(db, attachID) + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl +} + +func GetObjectFromDB(db *sql.DB, actor Actor) Collection { + var nColl Collection + var result []ObjectBase + + query := fmt.Sprintf("SELECT id, name, content, type, published, updated, attributedto, attachment, actor FROM activitystream WHERE actor='%s' AND id IN (SELECT id FROM replies WHERE inreplyto='') AND type='Note' ORDER BY updated asc;", actor.Id) + + rows, err := db.Query(query) + + defer rows.Close() + + CheckError(err, "error query object from db") + + defer rows.Close() + + for rows.Next(){ + var post ObjectBase + var actor Actor + var attachID string + + err = rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.Updated, &post.AttributedTo, &attachID, &actor.Id) + + CheckError(err, "error scan object into post struct") + + post.Actor = &actor + + post.Replies = GetObjectRepliesDB(db, post) + + post.Replies.TotalItems, post.Replies.TotalImgs = GetObjectRepliesDBCount(db, post) + + post.Attachment = GetObjectAttachment(db, attachID) + + result = append(result, post) + } + + nColl.OrderedItems = result + + return nColl +} + +func GetInReplyToDB(db *sql.DB, parent ObjectBase) []ObjectBase { + var result []ObjectBase + + query := fmt.Sprintf("SELECT inreplyto FROM replies WHERE id ='%s'", parent.Id) + + rows, err := db.Query(query) + + defer rows.Close() + + if err !=nil{ + fmt.Println("error with inreplyto db query") + panic(err) + } + + defer rows.Close() + + for rows.Next() { + var post ObjectBase + + rows.Scan(&post.Id) + + result = append(result, post) + } + + return result +} + + +func GetObjectRepliesDB(db *sql.DB, parent ObjectBase) *CollectionBase { + + var nColl CollectionBase + var result []ObjectBase + + query := fmt.Sprintf("SELECT id, name, content, type, published, attributedto, attachment, actor FROM activitystream WHERE id IN (SELECT id FROM replies WHERE inreplyto='%s') AND type='Note' ORDER BY published asc;", parent.Id) + + rows, err := db.Query(query) + + defer rows.Close() + + if err !=nil{ + fmt.Println("error with replies db query") + panic(err) + } + + defer rows.Close() + + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + + post.InReplyTo = append(post.InReplyTo, parent) + + err = rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.AttributedTo, &attachID, &actor.Id) + + + CheckError(err, "error with replies db scan") + + post.Actor = &actor + + post.Replies = GetObjectRepliesRepliesDB(db, post) + + post.Replies.TotalItems, post.Replies.TotalImgs = GetObjectRepliesDBCount(db, post) + + post.Attachment = GetObjectAttachment(db, attachID) + + result = append(result, post) + } + + nColl.OrderedItems = result + + remoteCollection := GetObjectRepliesRemote(db, parent) + + for _, e := range remoteCollection.OrderedItems { + + nColl.OrderedItems = append(nColl.OrderedItems, e) + + } + + return &nColl +} + +func GetObjectRepliesRemote(db *sql.DB, parent ObjectBase) CollectionBase { + var nColl CollectionBase + var result []ObjectBase + query := fmt.Sprintf("select id from replies where id not in (select id from activitystream) and inreplyto='%s'", parent.Id) + + rows, err := db.Query(query) + + CheckError(err, "could not get remote id query") + + for rows.Next() { + var id string + rows.Scan(&id) + + coll := GetCollectionFromID(id) + + for _, e := range coll.OrderedItems { + result = append(result, e) + } + } + + nColl.OrderedItems = result + + return nColl +} + +func GetObjectRepliesRepliesDB(db *sql.DB, parent ObjectBase) *CollectionBase { + + var nColl CollectionBase + var result []ObjectBase + + query := fmt.Sprintf("SELECT id, name, content, type, published, attributedto, attachment, actor FROM activitystream WHERE id IN (SELECT id FROM replies WHERE inreplyto='%s') AND type='Note' ORDER BY published asc;", parent.Id) + + rows, err := db.Query(query) + + defer rows.Close() + + if err !=nil{ + fmt.Println("error with replies db query") + panic(err) + } + + defer rows.Close() + + for rows.Next() { + var post ObjectBase + var actor Actor + var attachID string + + post.InReplyTo = append(post.InReplyTo, parent) + + err = rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.AttributedTo, &attachID, &actor.Id) + + + CheckError(err, "error with replies replies db scan") + + post.Actor = &actor + + post.Attachment = GetObjectAttachment(db, attachID) + + result = append(result, post) + } + + remoteCollection := GetObjectRepliesRemote(db, parent) + + for _, e := range remoteCollection.OrderedItems { + + nColl.OrderedItems = append(nColl.OrderedItems, e) + + } + + nColl.OrderedItems = result + + return &nColl +} + +func GetObjectRepliesDBCount(db *sql.DB, parent ObjectBase) (int, int) { + + var countId int + var countImg int + + query := fmt.Sprintf("SELECT COUNT(id) FROM replies WHERE inreplyto ='%s' and id in (select id from activitystream where type='Note');", parent.Id) + + rows, err := db.Query(query) + + defer rows.Close() + + if err !=nil{ + fmt.Println("error with replies count db query") + } + + defer rows.Close() + + for rows.Next() { + err = rows.Scan(&countId) + + if err !=nil{ + fmt.Println("error with replies count db scan") + } + } + + query = fmt.Sprintf("SELECT COUNT(attachment) FROM activitystream WHERE id IN (SELECT id FROM replies WHERE inreplyto ='%s') AND attachment != '';", parent.Id) + + rows, err = db.Query(query) + + defer rows.Close() + + if err !=nil{ + fmt.Println("error with replies count db query") + } + + defer rows.Close() + + for rows.Next() { + err = rows.Scan(&countImg) + + if err !=nil{ + fmt.Println("error with replies count db scan") + } + } + + return countId, countImg +} + +func GetObjectAttachment(db *sql.DB, id string) []ObjectBase { + + var attachments []ObjectBase + + query := fmt.Sprintf("SELECT id, type, name, href, mediatype, size, published FROM activitystream WHERE id='%s'", id) + + rows, err := db.Query(query) + + defer rows.Close() + + defer rows.Close() + for rows.Next() { + var attachment = new(ObjectBase) + + err = rows.Scan(&attachment.Id, &attachment.Type, &attachment.Name, &attachment.Href, &attachment.MediaType, &attachment.Size, &attachment.Published) + if err !=nil{ + fmt.Println("error with attachment db query") + panic(err) + } + + attachments = append(attachments, *attachment) + } + + return attachments +} + +func GetObjectPostsTotalDB(db *sql.DB, actor Actor) int{ + + count := 0 + query := fmt.Sprintf("SELECT COUNT(id) FROM activitystream WHERE actor='%s' AND id IN (SELECT id FROM replies WHERE inreplyto='' AND type='Note');", actor.Id) + + rows, err := db.Query(query) + + defer rows.Close() + + if err !=nil{ + fmt.Println("error with posts total db query") + } + + defer rows.Close() + for rows.Next() { + err = rows.Scan(&count) + + CheckError(err, "error with total post db scan") + } + + return count +} + +func GetObjectImgsTotalDB(db *sql.DB, actor Actor) int{ + + count := 0 + query := fmt.Sprintf("SELECT COUNT(attachment) FROM activitystream WHERE actor='%s' AND id IN (SELECT id FROM replies WHERE inreplyto='' AND type='Note' );", actor.Id) + + rows, err := db.Query(query) + + if err !=nil{ + fmt.Println("error with posts total db query") + } + + defer rows.Close() + for rows.Next() { + err = rows.Scan(&count) + + CheckError(err, "error with total post db scan") + } + + return count +} + + +func DeleteAttachmentFromFile(db *sql.DB, id string) { + + var query = fmt.Sprintf("select href, type from activitystream where id in (select attachment from activitystream where id='%s')", id) + + rows, err := db.Query(query) + + if err != nil { + fmt.Println("error query delete attachment") + } + + defer rows.Close() + + for rows.Next() { + var href string + var _type string + err := rows.Scan(&href, &_type) + href = strings.Replace(href, Domain + "/", "", 1) + + if err != nil { + fmt.Println("error scanning delete attachment") + } + + if _type != "Tombstone" { + _, err = os.Stat(href) + CheckError(err, "err removing file from system") + if err == nil { + os.Remove(href) + } + } + + } + + DeleteAttachmentFromDB(db, id) +} + + +func DeleteAttachmentRepliesFromDB(db *sql.DB, id string) { + var query = fmt.Sprintf("select id from activitystream where id (select id from replies where inreplyto='%s');", id) + + rows, err := db.Query(query) + + defer rows.Close() + + if err != nil { + fmt.Println("error query delete attachment replies") + } + + defer rows.Close() + + for rows.Next() { + var attachment string + + err := rows.Scan(&attachment) + + if err != nil { + fmt.Println("error scanning delete attachment") + } + + DeleteAttachmentFromFile(db, attachment) + } +} + + +func DeleteAttachmentFromDB(db *sql.DB, id string) { + datetime := time.Now().Format(time.RFC3339) + + var query = fmt.Sprintf("update activitystream set type='Tombstone', mediatype='image/png', href='%s', name='', content='', attributedto='deleted', updated='%s', deleted='%s' where id in (select attachment from activitystream where id='%s');", Domain + "/public/removed.png", datetime, datetime, id) + + _, err := db.Exec(query) + + CheckError(err, "error with delete attachment") +} + + +func DeleteObjectFromDB(db *sql.DB, id string) { + datetime := time.Now().Format(time.RFC3339) + var query = fmt.Sprintf("update activitystream set type='Tombstone', name='', content='', attributedto='deleted', updated='%s', deleted='%s' where id='%s';", datetime, datetime, id) + + _, err := db.Exec(query) + + CheckError(err, "error with delete object") +} + +func DeleteObjectRepliesFromDB(db *sql.DB, id string) { + datetime := time.Now().Format(time.RFC3339) + var query = fmt.Sprintf("update activitystream set type='Tombstone', name='', content='', attributedto='deleted' updated='%s', deleted='%s' where id in (select id from replies where inreplyto='%s');", datetime, datetime, id) + + _, err := db.Exec(query) + CheckError(err, "error with delete object replies") +} + +func DeleteObject(db *sql.DB, id string) { + + if(!IsIDLocal(db, id)) { + return + } + + DeleteObjectFromDB(db, id) + DeleteReportActivity(db, id) + DeleteAttachmentFromFile(db, id) +} + +func DeleteObjectAndReplies(db *sql.DB, id string) { + + if(!IsIDLocal(db, id)) { + return + } + + DeleteObjectFromDB(db, id) + DeleteReportActivity(db, id) + DeleteAttachmentFromFile(db, id) + DeleteObjectRepliesFromDB(db, id) + DeleteAttachmentRepliesFromDB(db, id) +} + +func GetRandomCaptcha(db *sql.DB) string{ + query := fmt.Sprintf("select identifier from verification where type='captcha' order by random() limit 1") + rows, err := db.Query(query) + + CheckError(err, "could not get captcha") + + var verify string + + rows.Next() + err = rows.Scan(&verify) + + rows.Close() + + CheckError(err, "Could not get verify captcha") + + return verify +} + +func GetCaptchaTotal(db *sql.DB) int{ + query := fmt.Sprintf("select count(*) from verification where type='captcha'") + rows, err := db.Query(query) + + defer rows.Close() + + CheckError(err, "could not get query captcha total") + + var count int + for rows.Next(){ + if err := rows.Scan(&count); err != nil{ + CheckError(err, "could not get captcha total") + } + } + + return count +} + +func GetCaptchaCodeDB(db *sql.DB, verify string) string { + + query := fmt.Sprintf("select code from verification where identifier='%s' limit 1", verify) + rows, err := db.Query(query) + + defer rows.Close() + + CheckError(err, "could not get captcha verifciation") + + var code string + + rows.Next() + err = rows.Scan(&code) + + CheckError(err, "Could not get verification captcha") + + return code +} + +func GetActorAuth(db *sql.DB, actor string) []string { + query := fmt.Sprintf("select type from actorauth where board='%s'", actor) + + rows, err := db.Query(query) + + defer rows.Close() + + CheckError(err, "could not get actor auth") + + var auth []string + + for rows.Next() { + var e string + err = rows.Scan(&e) + + auth = append(auth, e) + + CheckError(err, "could not get actor auth row scan") + } + + return auth +} + +func DeleteCaptchaCodeDB(db *sql.DB, verify string) { + query := fmt.Sprintf("delete from verification where identifier='%s'", verify) + + _, err := db.Exec(query); + + CheckError(err, "could not delete captcah code db") + + os.Remove("./" + verify) +} + +func EscapeString(text string) string { + text = strings.Replace(text, "'", "''", -1) + text = strings.Replace(text, "&", "&", -1) + text = strings.Replace(text, "<", "<", -1) + return text +} + +func GetActorReportedTotal(db *sql.DB, id string) int { + query := fmt.Sprintf("select count(id) from reported where board='%s'", id) + + rows, err := db.Query(query) + + CheckError(err, "error getting actor reported total query") + + defer rows.Close() + + var count int + for rows.Next() { + rows.Scan(&count) + } + + return count +} + +func GetActorReportedDB(db *sql.DB, id string) []ObjectBase { + var nObj []ObjectBase + + query := fmt.Sprintf("select id, count from reported where board='%s'", id) + + rows, err := db.Query(query) + + CheckError(err, "error getting actor reported query") + + defer rows.Close() + + for rows.Next() { + var obj ObjectBase + + rows.Scan(&obj.Id, &obj.Size) + + nObj = append(nObj, obj) + } + + return nObj +} diff --git a/Follow.go b/Follow.go new file mode 100644 index 0000000..475417b --- /dev/null +++ b/Follow.go @@ -0,0 +1,222 @@ +package main + +import "fmt" +import "net/http" +import "database/sql" +import _ "github.com/lib/pq" +import "encoding/json" + +func GetActorFollowing(w http.ResponseWriter, db *sql.DB, id string) { + var following Collection + + following.AtContext.Context = "https://www.w3.org/ns/activitystreams" + following.Type = "Collection" + following.TotalItems, _ = GetActorFollowTotal(db, id) + following.Items, _ = GetActorFollowDB(db, id) + + enc, _ := json.MarshalIndent(following, "", "\t") + w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Write(enc) +} + +func GetActorFollowers(w http.ResponseWriter, db *sql.DB, id string) { + var following Collection + + following.AtContext.Context = "https://www.w3.org/ns/activitystreams" + following.Type = "Collection" + _, following.TotalItems = GetActorFollowTotal(db, id) + _, following.Items = GetActorFollowDB(db, id) + + enc, _ := json.MarshalIndent(following, "", "\t") + w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Write(enc) +} + +func SetActorFollowDB(db *sql.DB, activity Activity, actor string) Activity { + var query string + alreadyFollow := false + following, follower := GetActorFollowDB(db, actor) + + if activity.Actor.Id == actor { + for _, e := range following { + if e.Id == activity.Object.Id { + alreadyFollow = true + } + } + if alreadyFollow { + query = fmt.Sprintf("delete from following where id='%s' and following='%s'", activity.Actor.Id, activity.Object.Id) + activity.Summary = activity.Actor.Id + " Unfollow " + activity.Object.Id + } else { + query = fmt.Sprintf("insert into following (id, following) values ('%s', '%s')", activity.Actor.Id, activity.Object.Id) + activity.Summary = activity.Actor.Id + " Follow " + activity.Object.Id + } + } else { + for _, e := range follower { + if e.Id == activity.Actor.Id { + alreadyFollow = true + } + } + if alreadyFollow { + query = fmt.Sprintf("delete from follower where id='%s' and follower='%s'", activity.Object.Id, activity.Actor.Id) + activity.Summary = activity.Actor.Id + " Unfollow " + activity.Object.Id + } else { + query = fmt.Sprintf("insert into follower (id, follower) values ('%s', '%s')", activity.Object.Id, activity.Actor.Id) + activity.Summary = activity.Actor.Id + " Follow " + activity.Object.Id + } + } + + _, err := db.Exec(query) + + CheckError(err, "error with follow db insert/delete") + + return activity +} + +func GetActorFollowDB(db *sql.DB, id string) ([]ObjectBase, []ObjectBase) { + var followingCollection []ObjectBase + var followerCollection []ObjectBase + + query := fmt.Sprintf("SELECT following FROM following WHERE id='%s'", id) + + rows, err := db.Query(query) + + CheckError(err, "error with following db query") + + defer rows.Close() + + for rows.Next() { + var obj ObjectBase + + err := rows.Scan(&obj.Id) + + CheckError(err, "error with following db scan") + + followingCollection = append(followingCollection, obj) + } + + query = fmt.Sprintf("SELECT follower FROM follower WHERE id='%s'", id) + + rows, err = db.Query(query) + + CheckError(err, "error with followers db query") + + defer rows.Close() + + for rows.Next() { + var obj ObjectBase + + err := rows.Scan(&obj.Id) + + CheckError(err, "error with followers db scan") + + followerCollection = append(followerCollection, obj) + } + + return followingCollection, followerCollection +} + +func GetActorFollowTotal(db *sql.DB, id string) (int, int) { + var following int + var followers int + + query := fmt.Sprintf("SELECT COUNT(following) FROM following WHERE id='%s'", id) + + rows, err := db.Query(query) + + CheckError(err, "error with following total db query") + + defer rows.Close() + + for rows.Next() { + err := rows.Scan(&following) + + CheckError(err, "error with following total db scan") + } + + query = fmt.Sprintf("SELECT COUNT(follower) FROM follower WHERE id='%s'", id) + + rows, err = db.Query(query) + + CheckError(err, "error with followers total db query") + + defer rows.Close() + + for rows.Next() { + err := rows.Scan(&followers) + + CheckError(err, "error with followers total db scan") + } + + return following, followers +} + +func AcceptFollow(activity Activity, actor Actor) Activity { + var accept Activity + var obj ObjectBase + + obj.Type = activity.Type + obj.Actor = activity.Actor + + var nobj NestedObjectBase + obj.Object = &nobj + obj.Object.Id = activity.Object.Id + + accept.AtContext.Context = activity.AtContext.Context + accept.Type = "Accept" + + var nactor Actor + accept.Actor = &nactor + accept.Actor.Id = actor.Id + accept.Object = &obj + accept.To = append(accept.To, activity.Object.Id) + + return accept +} + +func RejectFollow(activity Activity, actor Actor) Activity { + var accept Activity + var obj ObjectBase + + obj.Type = activity.Type + obj.Actor = activity.Actor + obj.Object = new(NestedObjectBase) + obj.Object.Id = activity.Object.Id + + accept.AtContext.Context = activity.AtContext.Context + accept.Type = "Reject" + accept.Actor = &actor + accept.Object = &obj + + return accept +} + +func SetActorFollowingDB(db *sql.DB, activity Activity) Activity{ + var query string + alreadyFollow := false + _, follower := GetActorFollowDB(db, activity.Object.Id) + + for _, e := range follower { + if e.Id == activity.Object.Id { + alreadyFollow = true + } + } + + if alreadyFollow { + query = fmt.Sprintf("delete from follower where id='%s' and follower='%s'", activity.Object.Id, activity.Actor.Id) + activity.Summary = activity.Actor.Id + " Unfollow " + activity.Object.Id + } else { + query = fmt.Sprintf("insert into follower (id, follower) values ('%s', '%s')", activity.Object.Id, activity.Actor.Id) + activity.Summary = activity.Actor.Id + " Follow " + activity.Object.Id + } + + _, err := db.Exec(query) + + if err != nil { + CheckError(err, "error with follow db insert/delete") + activity.Type = "Reject" + return activity + } + + activity.Type = "Accept" + return activity +} diff --git a/OutboxPost.go b/OutboxPost.go new file mode 100644 index 0000000..89173f9 --- /dev/null +++ b/OutboxPost.go @@ -0,0 +1,545 @@ +package main + +import "fmt" +import "net/http" +import "database/sql" +import _ "github.com/lib/pq" +import "encoding/json" +import "reflect" +import "io/ioutil" +import "os" +import "regexp" +import "strings" + +func ParseOutboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { + + var activity Activity + var object ObjectBase + + actor := GetActorFromPath(db, r.URL.Path, "/") + contentType := GetContentType(r.Header.Get("content-type")) + + defer r.Body.Close() + if contentType == "multipart/form-data" || contentType == "application/x-www-form-urlencoded" { + r.ParseMultipartForm(5 << 20) + if(BoardHasAuthType(db, actor.Name, "captcha") && CheckCaptcha(db, r.FormValue("captcha"))) { + f, header, _ := r.FormFile("file") + if(header != nil) { + if(header.Size > (5 << 20)){ + w.WriteHeader(http.StatusRequestEntityTooLarge) + w.Write([]byte("5MB max file size")) + return + } + + contentType, _ := GetFileContentType(f) + + if(!SupportedMIMEType(contentType)) { + w.WriteHeader(http.StatusNotAcceptable) + w.Write([]byte("file type not supported")) + return + } + + } + + var nObj = CreateObject("Note") + nObj = ObjectFromForm(r, db, nObj) + + var act Actor + nObj.Actor = &act + nObj.Actor.Id = Domain + "/" + actor.Name + + delete := regexp.MustCompile("delete:.+") + for _, e := range nObj.Option { + if delete.MatchString(e) { + verification := strings.Replace(e, "delete:", "", 1) + if HasAuth(db, verification, Domain + "/" + actor.Name) { + for _, e := range nObj.InReplyTo { + if IsObjectLocal(db, e.Id) && e.Id != nObj.InReplyTo[len(nObj.InReplyTo) - 1].Id { + DeleteObject(db, e.Id) + nObj.Type = "Delete" + } + } + } + } + } + + if nObj.Type != "Delete" { + nObj = writeObjectToDB(db, nObj) + activity := CreateActivity("Create", nObj) + MakeActivityRequest(activity) + } + + var id string + re := regexp.MustCompile("\\w+$") + op := len(nObj.InReplyTo) - 1 + if op >= 0 { + if nObj.InReplyTo[op].Id == "" { + id = re.FindString(nObj.Id) + } else { + id = re.FindString(nObj.InReplyTo[op].Id) + } + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(id)) + } + + + + } else { + activity = GetActivityFromJson(r, db) + if IsActivityLocal(db, activity) { + switch activity.Type { + case "Create": + if(true) { + object = GetObjectFromActivity(activity) + writeObjectToDB(db, object) + w.WriteHeader(http.StatusCreated) + w.Header().Set("Location", object.Id) + } else { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("")) + } + + case "Follow": + var validActor bool + var validLocalActor bool + + _, validActor = IsValidActor(activity.Object.Id) + validLocalActor = (activity.Actor.Id == actor.Id) || (activity.Object.Id == actor.Id) + verification := GetVerificationByCode(db, activity.Auth) + + var rActivity Activity + + if validActor && validLocalActor && verification.Board == activity.Actor.Id { + rActivity = AcceptFollow(activity, actor) + } else { + rActivity = RejectFollow(activity, actor) + rActivity.Summary = "No valid actor or Actor is not located here" + } + + if rActivity.Type == "Accept" { + rActivity.Summary = SetActorFollowDB(db, activity, actor.Id).Summary + } + + enc, _ := json.MarshalIndent(rActivity, "", "\t") + + if rActivity.Type == "Reject" { + w.WriteHeader(http.StatusBadRequest) + } + + w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Write(enc) + + case "Delete": + fmt.Println("This is a delete") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("could not process activity")) + + case "Note": + fmt.Println("This is a note") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("could not process activity")) + + case "New": + fmt.Println("Added new Board") + name := activity.Object.Actor.Name + prefname := activity.Object.Actor.PreferredUsername + summary := activity.Object.Actor.Summary + restricted := activity.Object.Actor.Restricted + + actor := CreateNewBoardDB(db, *CreateNewActor(name, prefname, summary, authReq, restricted)) + + if actor.Id != "" { + j, _ := json.Marshal(&actor) + w.Write([]byte(j)) + return + } + + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + + default: + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("could not process activity")) + } + } else { + fmt.Println("is NOT activity") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("could not process activity")) + } + } +} + +func ObjectFromJson(r *http.Request, obj ObjectBase) ObjectBase { + body, _ := ioutil.ReadAll(r.Body) + + var respActivity ActivityRaw + + err := json.Unmarshal(body, &respActivity) + + CheckError(err, "error with object from json") + + if HasContextFromJson(respActivity.AtContextRaw.Context) { + var jObj ObjectBase + jObj = GetObjectFromJson(respActivity.ObjectRaw) + jObj.To = GetToFromJson(respActivity.ToRaw) + jObj.Cc = GetToFromJson(respActivity.CcRaw) + } + + return obj +} + +func GetObjectFromJson(obj []byte) ObjectBase { + var generic interface{} + + err := json.Unmarshal(obj, &generic) + + CheckError(err, "error with getting obj from json") + + t := reflect.TypeOf(generic) + + var nObj ObjectBase + if t != nil { + switch t.String() { + case "[]interface {}": + var lObj ObjectBase + var arrContext ObjectArray + err = json.Unmarshal(obj, &arrContext.Object) + CheckError(err, "error with []interface{} oject from json") + if len(arrContext.Object) > 0 { + lObj = arrContext.Object[0] + } + nObj = lObj + break + + case "map[string]interface {}": + var arrContext Object + err = json.Unmarshal(obj, &arrContext.Object) + CheckError(err, "error with object from json") + nObj = *arrContext.Object + break + + case "string": + var lObj ObjectBase + var arrContext ObjectString + err = json.Unmarshal(obj, &arrContext.Object) + CheckError(err, "error with string object from json") + lObj.Id = arrContext.Object + nObj = lObj + break + } + } + + return nObj +} + +func GetActorFromJson(actor []byte) Actor{ + var generic interface{} + var nActor Actor + err := json.Unmarshal(actor, &generic) + + if err != nil { + return nActor + } + + t := reflect.TypeOf(generic) + if t != nil { + switch t.String() { + case "map[string]interface {}": + err = json.Unmarshal(actor, &nActor) + CheckError(err, "error with To []interface{}") + + case "string": + var str string + err = json.Unmarshal(actor, &str) + CheckError(err, "error with To string") + nActor.Id = str + } + + return nActor + } + + return nActor +} + +func GetToFromJson(to []byte) []string { + var generic interface{} + + err := json.Unmarshal(to, &generic) + + if err != nil { + return nil + } + + t := reflect.TypeOf(generic) + + if t != nil { + var nStr []string + switch t.String() { + case "[]interface {}": + err = json.Unmarshal(to, &nStr) + CheckError(err, "error with To []interface{}") + return nStr + + case "string": + var str string + err = json.Unmarshal(to, &str) + CheckError(err, "error with To string") + nStr = append(nStr, str) + return nStr + } + } + + return nil +} + +func HasContextFromJson(context []byte) bool { + var generic interface{} + + err := json.Unmarshal(context, &generic) + + CheckError(err, "error with getting context") + + t := reflect.TypeOf(generic) + + hasContext := false + + switch t.String() { + case "[]interface {}": + var arrContext AtContextArray + err = json.Unmarshal(context, &arrContext.Context) + CheckError(err, "error with []interface{}") + if len(arrContext.Context) > 0 { + if arrContext.Context[0] == "https://www.w3.org/ns/activitystreams" { + hasContext = true + } + } + case "string": + var arrContext AtContextString + err = json.Unmarshal(context, &arrContext.Context) + CheckError(err, "error with string") + if arrContext.Context == "https://www.w3.org/ns/activitystreams" { + hasContext = true + } + } + + return hasContext +} + +func ObjectFromForm(r *http.Request, db *sql.DB, obj ObjectBase) ObjectBase { + + file, header, _ := r.FormFile("file") + + if file != nil { + defer file.Close() + + var tempFile = new(os.File) + obj.Attachment, tempFile = CreateAttachmentObject(file, header) + + defer tempFile.Close(); + + fileBytes, _ := ioutil.ReadAll(file) + + tempFile.Write(fileBytes) + } + + obj.AttributedTo = EscapeString(r.FormValue("name")) + obj.Name = EscapeString(r.FormValue("subject")) + obj.Content = EscapeString(r.FormValue("comment")) + + obj = ParseOptions(r, obj) + + var originalPost ObjectBase + originalPost.Id = EscapeString(r.FormValue("inReplyTo")) + + obj.InReplyTo = append(obj.InReplyTo, originalPost) + + var activity Activity + + activity.To = append(activity.To, originalPost.Id) + + if originalPost.Id != "" { + if !IsActivityLocal(db, activity) { + id := GetActorFromID(originalPost.Id).Id + + obj.To = append(obj.To, GetActor(id).Id) + } + } + + replyingTo := ParseCommentForReplies(r.FormValue("comment")) + + for _, e := range replyingTo { + + has := false + + for _, f := range obj.InReplyTo { + if e.Id == f.Id { + has = true + break + } + } + + if !has { + obj.InReplyTo = append(obj.InReplyTo, e) + + var activity Activity + + activity.To = append(activity.To, e.Id) + + if !IsActivityLocal(db, activity) { + id := GetActorFromID(e.Id).Id + + obj.To = append(obj.To, GetActor(id).Id) + } + } + } + + return obj +} + + +func ParseOptions(r *http.Request, obj ObjectBase) ObjectBase { + options := EscapeString(r.FormValue("options")) + if options != "" { + option := strings.Split(options, ";") + email := regexp.MustCompile(".+@.+\\..+") + wallet := regexp.MustCompile("wallet:.+") + delete := regexp.MustCompile("delete:.+") + for _, e := range option { + if e == "noko" { + obj.Option = append(obj.Option, "noko") + } else if e == "sage" { + obj.Option = append(obj.Option, "sage") + } else if e == "nokosage" { + obj.Option = append(obj.Option, "nokosage") + } else if email.MatchString(e) { + obj.Option = append(obj.Option, "email:" + e) + } else if wallet.MatchString(e) { + obj.Option = append(obj.Option, "wallet") + var wallet CryptoCur + value := strings.Split(e, ":") + wallet.Type = value[0] + wallet.Address = value[1] + obj.Wallet = append(obj.Wallet, wallet) + } else if delete.MatchString(e) { + obj.Option = append(obj.Option, e) + } + } + } + + return obj +} + +func GetActivityFromJson(r *http.Request, db *sql.DB) Activity { + body, _ := ioutil.ReadAll(r.Body) + + var respActivity ActivityRaw + + var nActivity Activity + + var nType string + + err := json.Unmarshal(body, &respActivity) + + CheckError(err, "error with activity from json") + + if HasContextFromJson(respActivity.AtContextRaw.Context) { + var jObj ObjectBase + + if respActivity.Type == "Note" { + jObj = GetObjectFromJson(body) + jObj.AtContext.Context = "" + nType = "Create" + } else { + jObj = GetObjectFromJson(respActivity.ObjectRaw) + nType = respActivity.Type + } + + actor := GetActorFromJson(respActivity.ActorRaw) + to := GetToFromJson(respActivity.ToRaw) + cc := GetToFromJson(respActivity.CcRaw) + + jObj.AttributedTo = actor.Id + + nActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" + nActivity.Type = nType + nActivity.Actor = &actor + nActivity.Published = respActivity.Published + nActivity.Auth = respActivity.Auth + + if len(to) > 0 { + nActivity.To = to + } + + if len(cc) > 0 { + nActivity.Cc = cc + } + + nActivity.Name = respActivity.Name + nActivity.Object = &jObj + } + + return nActivity +} + +func CheckCaptcha(db *sql.DB, captcha string) bool { + parts := strings.Split(captcha, ":") + path := "public/" + parts[0] + ".png" + code := GetCaptchaCodeDB(db, path) + + DeleteCaptchaCodeDB(db, path) + CreateNewCaptcha(db) + + if (code == strings.ToUpper(parts[1])) { + return true + } + + return false +} + +func ParseInboxRequest(w http.ResponseWriter, r *http.Request, db *sql.DB) { + activity := GetActivityFromJson(r, db) + + switch(activity.Type) { + case "Create": + for _, e := range activity.Object.InReplyTo { + if IsObjectLocal(db, e.Id) { + WriteObjectReplyToLocalDB(db, activity.Object.Id, e.Id) + } + } + break + + case "Follow": + for _, e := range activity.To { + if IsObjectLocal(db, e) { + nActivity := SetActorFollowingDB(db, activity) + j, _ := json.Marshal(&nActivity) + w.Write([]byte(j)) + } + } + break + } +} + +func MakeActivityFollowingReq(w http.ResponseWriter, r *http.Request, activity Activity) bool { + actor := GetActor(activity.Object.Id) + + resp, err := http.NewRequest("POST", actor.Inbox, nil) + + CheckError(err, "Cannot make new get request to actor inbox for following req") + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + var respActivity Activity + + err = json.Unmarshal(body, &respActivity) + + if respActivity.Type == "Accept" { + return true + } + + return false +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/activityPubStruct.go b/activityPubStruct.go new file mode 100644 index 0000000..81045f6 --- /dev/null +++ b/activityPubStruct.go @@ -0,0 +1,191 @@ +package main + +import "encoding/json" + +type AtContextRaw struct { + Context json.RawMessage `json:"@context,omitempty"` +} + +type ActivityRaw struct { + AtContextRaw + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Summary string `json:"summary,omitempty"` + Auth string `json:"auth,omitempty"` + ToRaw json.RawMessage `json:"to,omitempty"` + BtoRaw json.RawMessage `json:"bto,omitempty"` + CcRaw json.RawMessage `json:"cc,omitempty"` + Published string `json:"published,omitempty"` + ActorRaw json.RawMessage `json:"actor,omitempty"` + ObjectRaw json.RawMessage `json:"object,omitempty"` +} + +type AtContext struct { + Context string `json:"@context,omitempty"` +} + +type AtContextArray struct { + Context []interface {} `json:"@context,omitempty"` +} + +type AtContextString struct { + Context string `json:"@context,omitempty"` +} + +type ActorString struct { + Actor string `json:"actor,omitempty"` +} + +type ObjectArray struct { + Object []ObjectBase `json:"object,omitempty"` +} + +type Object struct { + Object *ObjectBase `json:"object,omitempty"` +} + +type ObjectString struct { + Object string `json:"object,omitempty"` +} + +type ToArray struct { + To []string `json:"to,omitempty"` +} + +type ToString struct { + To string `json:"to,omitempty"` +} + +type CcArray struct { + Cc []string `json:"cc,omitempty"` +} + +type CcOjectString struct { + Cc string `json:"cc,omitempty"` +} + +type Actor struct { + AtContext + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Inbox string `json:"inbox,omitempty"` + Outbox string `json:"outbox,omitempty"` + Following string `json:"following,omitempty"` + Followers string `json:"followers,omitempty"` + Name string `json:"name,omitempty"` + PreferredUsername string `json:"prefereedUsername,omitempty"` + Summary string `json:"summary,omitempty"` + AuthRequirement []string `json:"authrequirement,omitempty"` + Restricted bool `json:"restricted,omitempty"` +} + +type Activity struct { + AtContext + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Actor *Actor `json:"actor,omitempty"` + Name string `json:"name,omitempty"` + Summary string `json:"summary,omitempty"` + Auth string `json:"auth,omitempty"` + To []string `json:"to, omitempty"` + Bto []string `json:"bto,omitempty"` + Cc []string `json:"cc, omitempty"` + Published string `json:"published,omitempty"` + Object *ObjectBase `json:"object, omitempty"` +} + +type ObjectBase struct { + AtContext + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Option []string `json:"option,omitempty"` + Alias string `json:"alias,omitempty"` + AttributedTo string `json:"attributedTo,omitempty"` + Actor *Actor `json:"actor,omitempty"` + Audience string `json:"audience,omitempty"` + Content string `json:"content,omitempty"` + EndTime string `json:"endTime,omitempty"` + Generator string `json:"generator,omitempty"` + Icon string `json:"icon,omitempty"` + Image string `json:"image,omitempty"` + InReplyTo []ObjectBase `json:"inReplyTo,omitempty"` + Location string `json:"location,omitempty"` + Preview string `json:"preview,omitempty"` + Published string `json:"published,omitempty"` + Updated string `json:"updated,omitempty"` + Object *NestedObjectBase `json:"object,omitempty"` + Attachment []ObjectBase `json:"attachment,omitempty"` + Replies *CollectionBase `json:"replies,omitempty"` + StartTime string `json:"startTime,omitempty"` + Summary string `json:"summary,omitempty"` + Tag []ObjectBase `json:"tag,omitempty"` + Wallet []CryptoCur `json:"wallet,omitempty"` + Deleted string `json:"deleted,omitempty"` + Url []ObjectBase `json:"url,omitempty"` + Href string `json:"href,omitempty"` + To []string `json:"to,omitempty"` + Bto []string `json:"bto,omitempty"` + Cc []string `json:"cc,omitempty"` + Bcc string `json:"Bcc,omitempty"` + MediaType string `json:"mediatype,omitempty"` + Duration string `json:"duration,omitempty"` + Size int64 `json:"size,omitempty"` +} + +type CryptoCur struct { + Type string `json:"type,omitempty"` + Address string `json:"address,omitempty"` +} + +type NestedObjectBase struct { + AtContext + Type string `json:"type,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Alias string `json:"alias,omitempty"` + AttributedTo string `json:"attributedTo,omitempty"` + Actor *Actor `json:"actor,omitempty"` + Audience string `json:"audience,omitempty"` + Content string `json:"content,omitempty"` + EndTime string `json:"endTime,omitempty"` + Generator string `json:"generator,omitempty"` + Icon string `json:"icon,omitempty"` + Image string `json:"image,omitempty"` + InReplyTo []ObjectBase `json:"inReplyTo,omitempty"` + Location string `json:"location,omitempty"` + Preview string `json:"preview,omitempty"` + Published string `json:"published,omitempty"` + Attachment []ObjectBase `json:"attachment,omitempty"` + Replies *CollectionBase `json:"replies,omitempty"` + StartTime string `json:"startTime,omitempty"` + Summary string `json:"summary,omitempty"` + Tag []ObjectBase `json:"tag,omitempty"` + Updated string `json:"updated,omitempty"` + Deleted string `json:"deleted,omitempty"` + Url []ObjectBase `json:"url,omitempty"` + Href string `json:"href,omitempty"` + To []string `json:"to,omitempty"` + Bto []string `json:"bto,omitempty"` + Cc []string `json:"cc,omitempty"` + Bcc string `json:"Bcc,omitempty"` + MediaType string `json:"mediatype,omitempty"` + Duration string `json:"duration,omitempty"` + Size int64 `json:"size,omitempty"` +} + +type CollectionBase struct { + Actor string `json:"actor,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type,omitempty"` + TotalItems int `json:"totalItems,omitempty"` + TotalImgs int `json:"totalImgs,omitempty"` + OrderedItems []ObjectBase `json:"orderedItems,omitempty"` + Items []ObjectBase `json:"items,omitempty"` +} + +type Collection struct { + AtContext + CollectionBase +} diff --git a/databaseschema.psql b/databaseschema.psql new file mode 100644 index 0000000..e4c3fe1 --- /dev/null +++ b/databaseschema.psql @@ -0,0 +1,135 @@ +CREATE TABLE IF NOT EXISTS actor( +type varchar(50) default '', +id varchar(100) UNIQUE PRIMARY KEY, +name varchar(50) default '', +preferedusername varchar(100) default '', +summary varchar(200) default '', +inbox varchar(100) default '', +outbox varchar(100) default '', +following varchar(100) default '', +followers varchar(100) default '', +restricted boolean default false +); + +CREATE TABLE IF NOT EXISTS replies( +id varchar(100), +inreplyto varchar(100) +); + +CREATE TABLE IF NOT EXISTS following( +id varchar(100), +following varchar(100) +); + +CREATE TABLE IF NOT EXISTS follower( +id varchar(100), +follower varchar(100) +); + +CREATE TABLE IF NOT EXISTS verification( +type varchar(50) default '', +identifier varchar(50), +code varchar(50), +created TIMESTAMP default NOW() +); + +CREATE TABLE IF NOT EXISTS reported( +id varchar(100), +count int, +board varchar(100) +); + +CREATE TABLE IF NOT EXISTS verificationcooldown( +code varchar(50), +created TIMESTAMP default NOW() +); + +CREATE TABLE IF NOT EXISTS boardaccess( +identifier varchar(100), +code varchar(50), +board varchar(100), +type varchar(50) +); + +CREATE TABLE IF NOT EXISTS crossverification( +verificationcode varchar(50), +code varchar(50) +); + +CREATE TABLE IF NOT EXISTS actorauth( +type varchar(50), +board varchar(100) +); + +CREATE TABLE IF NOT EXISTS wallet( +id varchar(100) UNIQUE PRIMARY KEY, +type varchar(25), +address varchar(50) +); + +CREATE TABLE IF NOT EXISTS activitystream( +actor varchar(100) default '', +attachment varchar(100) default '', +attributedTo varchar(100) default '', +audience varchar(100) default '', +bcc varchar(100) default '', +bto varchar(100) default '', +cc varchar(100) default '', +context varchar(100) default '', +current varchar(100) default '', +first varchar(100) default '', +generator varchar(100) default '', +icon varchar(100) default '', +id varchar(100) UNIQUE PRIMARY KEY, +image varchar(100) default '', +instrument varchar(100) default '', +last varchar(100) default '', +location varchar(100) default '', +items varchar(100) default '', +oneOf varchar(100) default '', +anyOf varchar(100) default '', +closed varchar(100) default '', +origin varchar(100) default '', +next varchar(100) default '', +object varchar(100), +prev varchar(100) default '', +preview varchar(100) default '', +result varchar(100) default '', +tag varchar(100) default '', +target varchar(100) default '', +type varchar(100) default '', +to_ varchar(100) default '', +url varchar(100) default '', +accuracy varchar(100) default '', +altitude varchar(100) default '', +content varchar(2000) default '', +name varchar(100) default '', +alias varchar(100) default '', +duration varchar(100) default '', +height varchar(100) default '', +href varchar(100) default '', +hreflang varchar(100) default '', +partOf varchar(100) default '', +latitude varchar(100) default '', +longitude varchar(100) default '', +mediaType varchar(100) default '', +endTime varchar(100) default '', +published TIMESTAMP default NOW(), +startTime varchar(100) default '', +radius varchar(100) default '', +rel varchar(100) default '', +startIndex varchar(100) default '', +summary varchar(100) default '', +totalItems varchar(100) default '', +units varchar(100) default '', +updated TIMESTAMP default NOW(), +deleted TIMESTAMP default NULL, +width varchar(100) default '', +subject varchar(100) default '', +relationship varchar(100) default '', +describes varchar(100) default '', +formerType varchar(100) default '', +size int default NULL, +public boolean default false, +CONSTRAINT fk_object FOREIGN KEY (object) REFERENCES activitystream(id) +); \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..7ee8a84 --- /dev/null +++ b/main.go @@ -0,0 +1,937 @@ +package main + +import "fmt" +import "strings" +import "net/http" +import "net/url" +import "database/sql" +import _ "github.com/lib/pq" +import "math/rand" +import "time" +import "regexp" +import "os/exec" +import "bytes" +import "encoding/json" +import "io/ioutil" +import "mime/multipart" +import "os" + +const ( + host = "localhost" + port = 5432 + user = "postgres" + password = "password" + dbname = "fchan_server" +) + +var Port = ":3000" +var TP = "https://" +var Domain = TP + "server.fchan.xyz" + +var authReq = []string{"captcha","email","passphrase"} + +var supportedFiles = []string{"image/gif","image/jpeg","image/png","image/svg+xml","image/webp","image/avif","image/apng","video/mp4","video/ogg","video/webm","audio/mpeg","audio/ogg","audio/wav"} + + +var SiteEmail string //contact@fchan.xyz +var SiteEmailPassword string +var SiteEmailServer string //mail.fchan.xyz +var SiteEmailPort string //587 + +type BoardAccess struct { + boards []string +} + +func main() { + + if _, err := os.Stat("./public"); os.IsNotExist(err) { + os.Mkdir("./public", os.ModeDir) + } + + db := ConnectDB(); + + defer db.Close() + + go MakeCaptchas(db, 100) + + // root actor is used to follow remote feeds that are not local + //name, prefname, summary, auth requirements, restricted + CreateNewBoardDB(db, *CreateNewActor("", "FChan", "FChan is a federated image board instance.", authReq, false)) + + // Allow access to public media folder + fileServer := http.FileServer(http.Dir("./public")) + http.Handle("/public/", http.StripPrefix("/public", neuter(fileServer))) + + // main routing + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){ + path := r.URL.Path + + // remove trailing slash + if path != "/" { + re := regexp.MustCompile(`/$`) + path = re.ReplaceAllString(path, "") + } + + method := r.Method + + actor := GetActorFromPath(db, path, "/") + + var mainActor bool + var mainInbox bool + var mainOutbox bool + var mainFollowing bool + var mainFollowers bool + + var actorMain bool + var actorInbox bool + var actorOutbox bool + var actorFollowing bool + var actorFollowers bool + var actorReported bool + var actorVerification bool + + + if(actor.Id != ""){ + if actor.Name == "main" { + mainActor = (path == "/") + mainInbox = (path == "/inbox") + mainOutbox = (path == "/outbox") + mainFollowing = (path == "/following") + mainFollowers = (path == "/followers") + } else { + actorMain = (path == "/" + actor.Name) + actorInbox = (path == "/" + actor.Name + "/inbox") + actorOutbox = (path == "/" + actor.Name + "/outbox") + actorFollowing = (path == "/" + actor.Name + "/following") + actorFollowers = (path == "/" + actor.Name + "/followers") + actorReported = (path == "/" + actor.Name + "/reported") + actorVerification = (path == "/" + actor.Name + "/verification") + } + } + + + if mainActor { + GetActorInfo(w, db, Domain) + } else if mainInbox { + if method == "POST" { + + } else { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("404 no path")) + } + } else if mainOutbox { + if method == "GET" { + GetActorOutbox(w, r, db) + } else if method == "POST" { + ParseOutboxRequest(w, r, db) + } else { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("404 no path")) + } + } else if mainFollowing { + GetActorFollowing(w, db, Domain) + } else if mainFollowers { + GetActorFollowers(w, db, Domain) + } else if actorMain { + GetActorInfo(w, db, actor.Id) + } else if actorInbox { + if method == "POST" { + ParseInboxRequest(w, r, db) + } else { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("404 no path")) + } + } else if actorOutbox { + if method == "GET" { + GetActorOutbox(w, r, db) + } else if method == "POST" { + ParseOutboxRequest(w, r, db) + } else { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("404 no path")) + } + } else if actorFollowing { + GetActorFollowing(w, db, actor.Id) + } else if actorFollowers { + GetActorFollowers(w, db, actor.Id) + } else if actorReported { + GetActorReported(w, r, db, actor.Id) + } else if actorVerification { + if method == "POST" { + p, _ := url.ParseQuery(r.URL.RawQuery) + if len(p["email"]) > 0 { + email := p["email"][0] + verify := GetVerificationByEmail(db, email) + if verify.Identifier != "" || !IsEmailSetup() { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("400 no path")) + } else { + var nVerify Verify + nVerify.Type = "email" + nVerify.Identifier = email + nVerify.Code = CreateKey(32) + nVerify.Board = actor.Id + CreateVerification(db, nVerify) + SendVerification(nVerify) + w.WriteHeader(http.StatusCreated) + w.Write([]byte("Verification added")) + } + + } else { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("400 no path")) + } + } else { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("400 no path")) + } + } else { + collection := GetCollectionFromPath(db, Domain + "" + path) + if len(collection.OrderedItems) > 0 { + enc, _ := json.MarshalIndent(collection, "", "\t") + w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Write(enc) + } else { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("404 no path")) + } + } + }) + + http.HandleFunc("/getcaptcha", func(w http.ResponseWriter, r *http.Request){ + w.Write([]byte(GetRandomCaptcha(db))) + }) + + http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request){ + values := r.URL.Query().Get("id") + + if len(values) < 1 || !IsIDLocal(db, values) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + return + } + + id := values + DeleteObject(db, id) + w.Write([]byte("")) + + }) + + http.HandleFunc("/deleteattach", func(w http.ResponseWriter, r *http.Request){ + + values := r.URL.Query().Get("id") + + header := r.Header.Get("Authorization") + + auth := strings.Split(header, " ") + + if len(values) < 1 || len(auth) < 2 { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + return + } + + id := values + + if !IsIDLocal(db, id) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + return + } + + actor := GetActorFromPath(db, id, "/") + + if HasAuth(db, auth[1], actor.Id) { + DeleteAttachmentFromFile(db, id) + DeleteAttachmentFromDB(db, id) + w.Write([]byte("")) + return + } + + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + }) + + http.HandleFunc("/report", func(w http.ResponseWriter, r *http.Request){ + + id := r.URL.Query().Get("id") + close := r.URL.Query().Get("close") + + if close == "1" { + if !IsIDLocal(db, id) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + return + } + + reported := DeleteReportActivity(db, id) + if reported { + w.Write([]byte("")) + return + } + } + + reported := ReportActivity(db, id) + + if reported { + w.Write([]byte("")) + return + } + + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + + }) + + http.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request){ + var verify Verify + defer r.Body.Close() + + body, _ := ioutil.ReadAll(r.Body) + + err := json.Unmarshal(body, &verify) + + CheckError(err, "error get verify from json") + + v := GetVerificationByCode(db, verify.Code) + + if v.Identifier == verify.Identifier { + w.Write([]byte(v.Board)) + return + } + + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + }) + + http.ListenAndServe(Port, nil) +} + +func CheckError(e error, m string) error{ + if e != nil { + fmt.Println(m) + panic(e) + } + + return e +} + +func ConnectDB() *sql.DB { + 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) + CheckError(err, "error with db connection") + + err = db.Ping() + + CheckError(err, "error with db ping") + + fmt.Println("Successfully connected DB") + return db +} + +func CreateKey(len int) string { + var key string + str := (CreateTripCode(RandomID(len))) + for i := 0; i < len; i++ { + key += fmt.Sprintf("%c", str[i]) + } + return key +} + +func neuter(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/") { + http.NotFound(w, r) + return + } + + next.ServeHTTP(w, r) + }) +} + +func CreateTripCode(input string) string { + cmd := exec.Command("sha512sum") + cmd.Stdin = strings.NewReader(input) + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + + CheckError(err, "error with create trip code") + + return out.String() +} + +func GetActorFromPath(db *sql.DB, location string, prefix string) Actor { + pattern := fmt.Sprintf("%s([^/\n]+)(/.+)?", prefix) + re := regexp.MustCompile(pattern) + match := re.FindStringSubmatch(location) + + var actor string + + if(len(match) < 1 ) { + actor = "/" + } else { + actor = strings.Replace(match[1], "/", "", -1) + } + + if actor == "/" || actor == "outbox" || actor == "inbox" || actor == "following" || actor == "followers" { + actor = Domain + } else { + actor = Domain + "/" + actor + } + + var nActor Actor + + nActor = GetActorFromDB(db, actor) + + return nActor +} + +func GetContentType(location string) string { + elements := strings.Split(location, ";") + if len(elements) > 0 { + return elements[0] + } else { + return location + } +} + +func RandomID(size int) string { + rand.Seed(time.Now().UnixNano()) + domain := "0123456789ABCDEF" + rng := size + newID := "" + for i := 0; i < rng; i++ { + newID += string(domain[rand.Intn(len(domain))]) + } + + return newID +} + +func CreateUniqueID(db *sql.DB, actor string) string { + var newID string + isUnique := false + for !isUnique { + newID = RandomID(8) + + query := fmt.Sprintf("select id from activitystream where id='%s/%s/%s'", Domain, actor, newID) + + rows, err := db.Query(query) + + CheckError(err, "error with unique id query") + + defer rows.Close() + + var count int = 0 + for rows.Next(){ + count += 1 + } + + if count < 1 { + isUnique = true + } + } + + return newID +} + +func CreateNewActor(board string, prefName string, summary string, authReq []string, restricted bool) *Actor{ + actor := new(Actor) + + var path string + if board == "" { + path = Domain + actor.Name = "main" + } else { + path = Domain + "/" + board + actor.Name = board + } + + actor.Type = "Service" + actor.Id = fmt.Sprintf("%s", path) + actor.Following = fmt.Sprintf("%s/following", actor.Id) + actor.Followers = fmt.Sprintf("%s/followers", actor.Id) + actor.Inbox = fmt.Sprintf("%s/inbox", actor.Id) + actor.Outbox = fmt.Sprintf("%s/outbox", actor.Id) + actor.PreferredUsername = prefName + actor.Restricted = restricted + actor.Summary = summary + actor.AuthRequirement = authReq + + return actor +} + +func GetActorInfo(w http.ResponseWriter, db *sql.DB, id string) { + actor := GetActorFromDB(db, id) + enc, _ := json.MarshalIndent(actor, "", "\t") + w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Write(enc) +} + +func CreateObject(objType string) ObjectBase { + var nObj ObjectBase + + nObj.Type = objType + nObj.Published = time.Now().Format(time.RFC3339) + nObj.Updated = time.Now().Format(time.RFC3339) + + return nObj +} + +func CreateActivity(activityType string, obj ObjectBase) Activity { + var newActivity Activity + + newActivity.AtContext.Context = "https://www.w3.org/ns/activitystreams" + newActivity.Type = activityType + newActivity.Published = obj.Published + newActivity.Actor = obj.Actor + newActivity.Object = &obj + + for _, e := range obj.To { + newActivity.To = append(newActivity.To, e) + } + + for _, e := range obj.Cc { + newActivity.Cc = append(newActivity.Cc, e) + } + + return newActivity +} + +func ProcessActivity(db *sql.DB, activity Activity) { + activityType := activity.Type + + if activityType == "Create" { + for _, e := range activity.To { + if GetActorFromDB(db, e).Id != "" { + fmt.Println("actor is in the database") + } else { + fmt.Println("actor is NOT in the database") + } + } + } else if activityType == "Follow" { + + } else if activityType == "Delete" { + + } +} + +func CreateAttachmentObject(file multipart.File, header *multipart.FileHeader) ([]ObjectBase, *os.File) { + contentType, _ := GetFileContentType(file) + filename := header.Filename + size := header.Size + + re := regexp.MustCompile(`.+/`) + + fileType := re.ReplaceAllString(contentType, "") + + tempFile, _ := ioutil.TempFile("./public", "*." + fileType) + + var nAttachment []ObjectBase + var image ObjectBase + + image.Type = "Attachment" + image.Name = filename + image.Href = Domain + "/" + tempFile.Name() + image.MediaType = contentType + image.Size = size + image.Published = time.Now().Format(time.RFC3339) + + nAttachment = append(nAttachment, image) + + return nAttachment, tempFile +} + +func ParseCommentForReplies(comment string) []ObjectBase { + + re := regexp.MustCompile("(>>)(https://|http://)?(www\\.)?.+\\/\\w+") + match := re.FindAllStringSubmatch(comment, -1) + + var links []string + + for i:= 0; i < len(match); i++ { + str := strings.Replace(match[i][0], ">>", "", 1) + str = strings.Replace(str, "www.", "", 1) + str = strings.Replace(str, "http://", "", 1) + str = TP + "://" + str + links = append(links, str) + } + + var validLinks []ObjectBase + for i:= 0; i < len(links); i++ { + _, isValid := CheckValidActivity(links[i]) + if(isValid) { + var reply = new(ObjectBase) + reply.Id = links[i] + reply.Published = time.Now().Format(time.RFC3339) + validLinks = append(validLinks, *reply) + } + } + + return validLinks +} + + +func CheckValidActivity(id string) (Collection, bool) { + + req, err := http.NewRequest("GET", id, nil) + + if err != nil { + fmt.Println("error with request") + panic(err) + } + + req.Header.Set("Accept", "json/application/activity+json") + + resp, err := http.DefaultClient.Do(req) + + if err != nil { + fmt.Println("error with response") + panic(err) + } + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + var respCollection Collection + + err = json.Unmarshal(body, &respCollection) + + if err != nil { + panic(err) + } + + if respCollection.AtContext.Context == "https://www.w3.org/ns/activitystreams" && respCollection.OrderedItems[0].Id != "" { + return respCollection, true; + } + + return respCollection, false; +} + +func GetActor(id string) Actor { + + var respActor Actor + + req, err := http.NewRequest("GET", id, nil) + + CheckError(err, "error with getting actor req") + + resp, err := http.DefaultClient.Do(req) + + CheckError(err, "error with getting actor resp") + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + err = json.Unmarshal(body, &respActor) + + CheckError(err, "error getting actor from body") + + return respActor +} + +func GetActorCollection(collection string) Collection { + var nCollection Collection + + req, err := http.NewRequest("GET", collection, nil) + + CheckError(err, "error with getting actor collection req " + collection) + + resp, err := http.DefaultClient.Do(req) + + CheckError(err, "error with getting actor collection resp " + collection) + + if resp.StatusCode == 200 { + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + err = json.Unmarshal(body, &nCollection) + + CheckError(err, "error getting actor collection from body " + collection) + } + + return nCollection +} + + +func IsValidActor(id string) (Actor, bool) { + var respCollection Actor + req, err := http.NewRequest("GET", id, nil) + + CheckError(err, "error with valid actor request") + + req.Header.Set("Accept", "json/application/activity+json") + + resp, err := http.DefaultClient.Do(req) + + CheckError(err, "error with valid actor response") + + if resp.StatusCode == 403 { + return respCollection, false; + } + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + err = json.Unmarshal(body, &respCollection) + + if err != nil { + panic(err) + } + + if respCollection.AtContext.Context == "https://www.w3.org/ns/activitystreams" && respCollection.Id != "" && respCollection.Inbox != "" && respCollection.Outbox != "" { + return respCollection, true; + } + + return respCollection, false; +} + + + +func IsActivityLocal(db *sql.DB, activity Activity) bool { + for _, e := range activity.To { + if GetActorFromDB(db, e).Id != "" { + return true + } + } + + for _, e := range activity.Cc { + if GetActorFromDB(db, e).Id != "" { + return true + } + } + + if activity.Actor != nil && GetActorFromDB(db, activity.Actor.Id).Id != "" { + return true + } + + return false +} + +func IsIDLocal(db *sql.DB, id string) bool { + + if GetActivityFromDB(db, id).OrderedItems != nil { + return true + } + + return false +} + +func IsObjectLocal(db *sql.DB, id string) bool { + + query := fmt.Sprintf("select id from activitystream where id='%s'", id) + + rows, err := db.Query(query) + + defer rows.Close() + + if err != nil { + return false + } + + return true +} + +func GetObjectFromActivity(activity Activity) ObjectBase { + return *activity.Object +} + +func MakeCaptchas(db *sql.DB, total int) { + difference := total - GetCaptchaTotal(db) + + for i := 0; i < difference; i++ { + CreateNewCaptcha(db) + } +} + +func GetFileContentType(out multipart.File) (string, error) { + + buffer := make([]byte, 512) + + _, err := out.Read(buffer) + if err != nil { + return "", err + } + + out.Seek(0, 0) + + contentType := http.DetectContentType(buffer) + + return contentType, nil +} + +func IsReplyInThread(db *sql.DB, inReplyTo string, id string) bool { + obj, _ := CheckValidActivity(inReplyTo) + + for _, e := range obj.OrderedItems[0].Replies.OrderedItems { + if e.Id == id { + return true + } + } + return false +} + +func SupportedMIMEType(mime string) bool { + for _, e := range supportedFiles { + if e == mime { + return true + } + } + + return false +} + +func DeleteReportActivity(db *sql.DB, id string) bool { + + query := fmt.Sprintf("delete from reported where id='%s'", id) + + _, err := db.Exec(query) + + if err != nil { + CheckError(err, "error closing reported activity") + return false + } + + return true +} + +func ReportActivity(db *sql.DB, id string) bool { + + if !IsIDLocal(db, id) { + return false + } + + actor := GetActivityFromDB(db, id) + + query := fmt.Sprintf("select count from reported where id='%s'", id) + + rows, err := db.Query(query) + + CheckError(err, "could not select count from reported") + + defer rows.Close() + var count int + for rows.Next() { + rows.Scan(&count) + } + + if count < 1 { + query = fmt.Sprintf("insert into reported (id, count, board) values ('%s', %d, '%s')", id, 1, actor.Actor) + + _, err := db.Exec(query) + + if err != nil { + CheckError(err, "error inserting new reported activity") + return false + } + + } else { + count = count + 1 + query = fmt.Sprintf("update reported set count=%d where id='%s'", count, id) + + _, err := db.Exec(query) + + if err != nil { + CheckError(err, "error updating reported activity") + return false + } + } + + return true +} + +func GetActorReported(w http.ResponseWriter, r *http.Request, db *sql.DB, id string) { + + auth := r.Header.Get("Authorization") + verification := strings.Split(auth, " ") + + if len(verification) < 2 { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + return + } + + if !HasAuth(db, verification[1], id) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("")) + return + } + + var following Collection + + following.AtContext.Context = "https://www.w3.org/ns/activitystreams" + following.Type = "Collection" + following.TotalItems = GetActorReportedTotal(db, id) + following.Items = GetActorReportedDB(db, id) + + enc, _ := json.MarshalIndent(following, "", "\t") + w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Write(enc) +} + +func MakeActivityRequest(activity Activity) { + + j, _ := json.MarshalIndent(activity, "", "\t") + for _, e := range activity.To { + actor := GetActor(e) + + req, err := http.NewRequest("POST", actor.Inbox, bytes.NewBuffer(j)) + + CheckError(err, "error with sending activity req to") + + _, err = http.DefaultClient.Do(req) + + CheckError(err, "error with sending activity resp to") + } +} + +func GetCollectionFromID(id string) Collection { + req, err := http.NewRequest("GET", id, nil) + + CheckError(err, "could not get collection from id req") + + resp, err := http.DefaultClient.Do(req) + + CheckError(err, "could not get collection from id resp") + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + var nColl Collection + + err = json.Unmarshal(body, &nColl) + + CheckError(err, "error getting collection resp from json body") + + return nColl +} + +func GetActorFromID(id string) Actor { + req, err := http.NewRequest("GET", id, nil) + + CheckError(err, "error getting actor from id req") + + resp, err := http.DefaultClient.Do(req) + + CheckError(err, "error getting actor from id resp") + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + var respCollection Collection + + err = json.Unmarshal(body, &respCollection) + + CheckError(err, "error getting actor resp from json body") + + return *respCollection.OrderedItems[0].Actor +} diff --git a/outboxGet.go b/outboxGet.go new file mode 100644 index 0000000..55c63f8 --- /dev/null +++ b/outboxGet.go @@ -0,0 +1,120 @@ +package main + +import "fmt" +import "net/http" +import "database/sql" +import _ "github.com/lib/pq" +import "encoding/json" + +func GetActorOutbox(w http.ResponseWriter, r *http.Request, db *sql.DB) { + + actor := GetActorFromPath(db, r.URL.Path, "/") + var collection Collection + + collection.OrderedItems = GetObjectFromDB(db, actor).OrderedItems + + collection.AtContext.Context = "https://www.w3.org/ns/activitystreams" + collection.Actor = actor.Id + + collection.TotalItems = GetObjectPostsTotalDB(db, actor) + collection.TotalImgs = GetObjectImgsTotalDB(db, actor) + + enc, _ := json.MarshalIndent(collection, "", "\t") + w.Header().Set("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") + w.Write(enc) +} + + + +func GetObjectsFromFollow(actor Actor) []ObjectBase { + var followingCol Collection + var followObj []ObjectBase + followingCol = GetActorCollection(actor.Following) + for _, e := range followingCol.Items { + var followOutbox Collection + var actor Actor + actor = GetActor(e.Id) + followOutbox = GetActorCollection(actor.Outbox) + for _, e := range followOutbox.OrderedItems { + followObj = append(followObj, e) + } + } + return followObj +} + +func GetCollectionFromPath(db *sql.DB, path string) Collection { + + var nColl Collection + var result []ObjectBase + + query := fmt.Sprintf("SELECT id, name, content, type, published, attributedto, attachment, actor FROM activitystream WHERE id='%s' ORDER BY published desc;", path) + + rows, err := db.Query(query) + + CheckError(err, "error query collection path from db") + + defer rows.Close() + + for rows.Next(){ + var actor Actor + var post ObjectBase + var attachID string + + err = rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.AttributedTo, &attachID, &actor.Id) + + CheckError(err, "error scan object into post struct from path") + + post.Actor = &actor + + post.InReplyTo = GetInReplyToDB(db, post) + + post.Replies = GetObjectRepliesDB(db, post) + + post.Replies.TotalItems, post.Replies.TotalImgs = GetObjectRepliesDBCount(db, post) + + post.Attachment = GetObjectAttachment(db, attachID) + + result = append(result, post) + } + + nColl.AtContext.Context = "https://www.w3.org/ns/activitystreams" + + nColl.OrderedItems = result + + return nColl +} + +func GetObjectFromPath(db *sql.DB, path string) ObjectBase{ + + var nObj ObjectBase + var result []ObjectBase + + query := fmt.Sprintf("SELECT id, name, content, type, published, attributedto, attachment, actor FROM activitystream WHERE id='%s' ORDER BY published desc;", path) + + rows, err := db.Query(query) + + CheckError(err, "error query collection path from db") + + defer rows.Close() + + for rows.Next(){ + var post ObjectBase + var attachID string + + err = rows.Scan(&post.Id, &post.Name, &post.Content, &post.Type, &post.Published, &post.AttributedTo, &attachID, &post.Actor) + + CheckError(err, "error scan object into post struct from path") + + post.Replies = GetObjectRepliesDB(db, post) + + post.Replies.TotalItems, post.Replies.TotalImgs = GetObjectRepliesDBCount(db, post) + + post.Attachment = GetObjectAttachment(db, attachID) + + result = append(result, post) + } + + nObj = result[0] + + return nObj +} diff --git a/verification.go b/verification.go new file mode 100644 index 0000000..f233fe6 --- /dev/null +++ b/verification.go @@ -0,0 +1,456 @@ +package main + +import "fmt" +import "database/sql" +import _ "github.com/lib/pq" +import "net/smtp" +import "time" +import "os/exec" +import "os" +import "math/rand" + +type Verify struct { + Type string + Identifier string + Code string + Created string + Board string +} + +type VerifyCooldown struct { + Identifier string + Code string + Time int +} + +func DeleteBoardMod(db *sql.DB, verify Verify) { + query := fmt.Sprintf("select code from boardaccess where identifier='%s' and board='%s'", verify.Identifier, verify.Board) + + rows, err := db.Query(query) + + CheckError(err, "could not select code from boardaccess") + + defer rows.Close() + + var code string + rows.Next() + rows.Scan(&code) + + if code != "" { + query := fmt.Sprintf("delete from crossverification where code='%s'", code) + + + _, err := db.Exec(query) + + CheckError(err, "could not delete code from crossverification") + + query = fmt.Sprintf("delete from boardaccess where identifier='%s' and board='%s'", verify.Identifier, verify.Board) + + _, err = db.Exec(query) + + CheckError(err, "could not delete identifier from boardaccess") + } +} + +func GetBoardMod(db *sql.DB, identifier string) Verify{ + var nVerify Verify + + query := fmt.Sprintf("select code, board, type, identifier from boardaccess where identifier='%s'", identifier) + + rows, err := db.Query(query) + + CheckError(err, "could not select boardaccess query") + + defer rows.Close() + + rows.Next() + rows.Scan(&nVerify.Code, &nVerify.Board, &nVerify.Type, &nVerify.Identifier) + + return nVerify +} + +func CreateBoardMod(db *sql.DB, verify Verify) { + pass := CreateKey(50) + + query := fmt.Sprintf("select code from verification where identifier='%s' and type='%s'", verify.Board, verify.Type) + + rows, err := db.Query(query) + + CheckError(err, "could not select verifcaiton query") + + defer rows.Close() + + var code string + + rows.Next() + rows.Scan(&code) + + if code != "" { + + query := fmt.Sprintf("select identifier from boardaccess where identifier='%s' and board='%s'", verify.Identifier, verify.Board) + + rows, err := db.Query(query) + + CheckError(err, "could not select idenifier from boardaccess") + + defer rows.Close() + + var ident string + rows.Next() + rows.Scan(&ident) + + if ident != verify.Identifier { + + query := fmt.Sprintf("insert into crossverification (verificationcode, code) values ('%s', '%s')", code, pass) + + _, err := db.Exec(query) + + CheckError(err, "could not insert new crossverification") + + query = fmt.Sprintf("insert into boardaccess (identifier, code, board, type) values ('%s', '%s', '%s', '%s')", verify.Identifier, pass, verify.Board, verify.Type) + + _, err = db.Exec(query) + + CheckError(err, "could not insert new boardaccess") + + fmt.Printf("Board access - Board: %s, Identifier: %s, Code: %s\n", verify.Board, verify.Identifier, pass) + } + } +} + +func CreateVerification(db *sql.DB, verify Verify) { + query := fmt.Sprintf("insert into verification (type, identifier, code, created) values ('%s', '%s', '%s', '%s') ", verify.Type, verify.Identifier, verify.Code, time.Now().Format(time.RFC3339)) + + _, err := db.Exec(query) + + CheckError(err, "error creating verify") +} + +func GetVerificationByEmail(db *sql.DB, email string) Verify { + var verify Verify + + query := fmt.Sprintf("select type, identifier, code, board from boardaccess where identifier='%s';", email) + + rows, err := db.Query(query) + + defer rows.Close() + + CheckError(err, "error getting verify by email query") + + defer rows.Close() + + for rows.Next() { + err := rows.Scan(&verify.Type, &verify.Identifier, &verify.Code, &verify.Board) + + CheckError(err, "error getting verify by email scan") + } + + return verify +} + +func GetVerificationByCode(db *sql.DB, code string) Verify { + var verify Verify + + query := fmt.Sprintf("select type, identifier, code, board from boardaccess where code='%s';", code) + + rows, err := db.Query(query) + + defer rows.Close() + + if err != nil { + CheckError(err, "error getting verify by code query") + return verify + } + + for rows.Next() { + err := rows.Scan(&verify.Type, &verify.Identifier, &verify.Code, &verify.Board) + + CheckError(err, "error getting verify by code scan") + } + + return verify +} + +func VerifyCooldownCurrent(db *sql.DB, auth string) VerifyCooldown { + var current VerifyCooldown + + query := fmt.Sprintf("select identifier, code, time from verificationcooldown where code='%s'", auth) + + rows, err := db.Query(query) + + defer rows.Close() + + if err != nil { + + query := fmt.Sprintf("select identifier, code, time from verificationcooldown where identifier='%s'", auth) + + rows, err := db.Query(query) + + defer rows.Close() + + if err != nil { + return current + } + + defer rows.Close() + + for rows.Next() { + err = rows.Scan(¤t.Identifier, ¤t.Code, ¤t.Time) + + CheckError(err, "error scanning current verify cooldown verification") + } + } + + defer rows.Close() + + for rows.Next() { + err = rows.Scan(¤t.Identifier, ¤t.Code, ¤t.Time) + + CheckError(err, "error scanning current verify cooldown code") + } + + return current +} + +func VerifyCooldownAdd(db *sql.DB, verify Verify) { + query := fmt.Sprintf("insert into verficationcooldown (identifier, code) values ('%s', '%s');", verify.Identifier, verify.Code) + + _, err := db.Exec(query) + + CheckError(err, "error adding verify to cooldown") +} + +func VerficationCooldown(db *sql.DB) { + + query := fmt.Sprintf("select identifier, code, time from verificationcooldown") + + rows, err := db.Query(query) + + defer rows.Close() + + CheckError(err, "error with verifiy cooldown query ") + + defer rows.Close() + + for rows.Next() { + var verify VerifyCooldown + err = rows.Scan(&verify.Identifier, &verify.Code, &verify.Time) + + CheckError(err, "error with verifiy cooldown scan ") + + nTime := verify.Time - 1; + + query = fmt.Sprintf("update set time='%s' where identifier='%s'", nTime, verify.Identifier) + + _, err := db.Exec(query) + + CheckError(err, "error with update cooldown query") + + VerficationCooldownRemove(db) + } +} + +func VerficationCooldownRemove(db *sql.DB) { + query := fmt.Sprintf("delete from verificationcooldown where time < 1;") + + _, err := db.Exec(query) + + CheckError(err, "error with verifiy cooldown remove query ") +} + +func SendVerification(verify Verify) { + + fmt.Println("sending email") + + from := SiteEmail + pass := SiteEmailPassword + to := verify.Identifier + body := fmt.Sprintf("You can use either\r\nEmail: %s \r\n Verfication Code: %s\r\n for the board %s", verify.Identifier, verify.Code, verify.Board) + + msg := "From: " + from + "\n" + + "To: " + to + "\n" + + "Subject: Image Board Verification\n\n" + + body + + err := smtp.SendMail(SiteEmailServer + ":" + SiteEmailPort, + smtp.PlainAuth("", from, pass, SiteEmailServer), + from, []string{to}, []byte(msg)) + + + CheckError(err, "error with smtp") +} + +func IsEmailSetup() bool { + if SiteEmail == "" { + return false + } + + if SiteEmailPassword == "" { + return false + } + + if SiteEmailServer == "" { + return false + } + + if SiteEmailPort == "" { + return false + } + + return true +} + +func HasAuth(db *sql.DB, code string, board string) bool { + + verify := GetVerificationByCode(db, code) + + if HasBoardAccess(db, verify) { + return true + } + + fmt.Println("has auth is false") + + return false; +} + +func HasAuthCooldown(db *sql.DB, auth string) bool { + current := VerifyCooldownCurrent(db, auth) + if current.Time > 0 { + return true + } + + fmt.Println("has auth is false") + return false +} + +func GetVerify(db *sql.DB, access string) Verify { + verify := GetVerificationByCode(db, access) + + if verify.Identifier == "" { + verify = GetVerificationByEmail(db, access) + } + + return verify +} + +func CreateNewCaptcha(db *sql.DB){ + id := RandomID(8) + file := "public/" + id + ".png" + + for true { + if _, err := os.Stat("./" + file); err == nil { + id = RandomID(8) + file = "public/" + id + ".png" + }else{ + break + } + } + + captcha := Captcha() + + var pattern string + rnd := fmt.Sprintf("%d", rand.Intn(3)) + + srnd := string(rnd) + + switch srnd { + case "0" : + pattern = "pattern:verticalbricks" + break + + case "1" : + pattern = "pattern:verticalsaw" + break + + case "2" : + pattern = "pattern:hs_cross" + break + + } + + cmd := exec.Command("convert", "-size", "200x98", pattern, "-transparent", "white", file) + + err := cmd.Run() + + CheckError(err, "error with captcha first pass") + + cmd = exec.Command("convert", file, "-fill", "blue", "-pointsize", "62", "-annotate", "+0+70", captcha, "-tile", "pattern:left30", "-gravity", "center", "-transparent", "white", file) + + err = cmd.Run() + + CheckError(err, "error with captcha second pass") + + rnd = fmt.Sprintf("%d", rand.Intn(24) - 12) + + cmd = exec.Command("convert", file, "-rotate", rnd, "-wave", "5x35", "-distort", "Arc", "20", "-wave", "2x35", "-transparent", "white", file) + + err = cmd.Run() + + CheckError(err, "error with captcha third pass") + + var verification Verify + verification.Type = "captcha" + verification.Code = captcha + verification.Identifier = file + + CreateVerification(db, verification) +} + +func CreateBoardAccess(db *sql.DB, verify Verify) { + if(!HasBoardAccess(db, verify)){ + query := fmt.Sprintf("insert into boardaccess (identifier, board) values('%s', '%s')", + verify.Identifier, verify.Board) + + _, err := db.Exec(query) + + CheckError(err, "could not instert verification and board into board access") + } +} + +func HasBoardAccess(db *sql.DB, verify Verify) bool { + query := fmt.Sprintf("select count(*) from boardaccess where identifier='%s' and board='%s'", + verify.Identifier, verify.Board) + + rows, err := db.Query(query) + + defer rows.Close() + + CheckError(err, "could not select boardaccess based on verify") + + var count int + + rows.Next() + rows.Scan(&count) + + if(count > 0) { + return true + } else { + return false + } +} + +func BoardHasAuthType(db *sql.DB, board string, auth string) bool { + authTypes := GetActorAuth(db, board) + + for _, e := range authTypes { + if(e == auth){ + return true + } + } + + return false +} + +func Captcha() string { + rand.Seed(time.Now().UnixNano()) + domain := "ABEFHKMNPQRSUVWXYZ#$&" + rng := 4 + newID := "" + for i := 0; i < rng; i++ { + newID += string(domain[rand.Intn(len(domain))]) + } + + return newID +} + + -- cgit v1.2.3