diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Database.go | 868 | ||||
-rw-r--r-- | Follow.go | 222 | ||||
-rw-r--r-- | OutboxPost.go | 545 | ||||
-rw-r--r-- | README.md | 0 | ||||
-rw-r--r-- | activityPubStruct.go | 191 | ||||
-rw-r--r-- | databaseschema.psql | 135 | ||||
-rw-r--r-- | main.go | 937 | ||||
-rw-r--r-- | outboxGet.go | 120 | ||||
-rw-r--r-- | verification.go | 456 |
10 files changed, 3476 insertions, 0 deletions
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 --- /dev/null +++ b/README.md 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 @@ -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 +} + + |