├── board ├── votes │ ├── model_methods.go │ ├── errors.go │ ├── deps.go │ ├── finders.go │ ├── model.go │ └── mutators.go ├── legacy │ ├── model │ │ ├── comment.go │ │ ├── stat.go │ │ ├── gaming.go │ │ ├── sitemap.go │ │ ├── activity.go │ │ ├── election.go │ │ ├── vote.go │ │ ├── category.go │ │ ├── notification.go │ │ └── part.go │ ├── acl.go │ ├── gaming.go │ ├── error.go │ ├── helpers_test.go │ └── helpers.go ├── categories │ ├── deps.go │ ├── finders.go │ └── model.go ├── activity │ ├── deps.go │ ├── mutators.go │ ├── model.go │ └── finders.go ├── assets │ ├── deps.go │ ├── mutators.go │ └── finders.go ├── comments │ ├── deps.go │ └── mutators.go ├── flags │ ├── deps.go │ ├── mutators.go │ ├── model.go │ └── finders.go ├── notifications │ ├── deps.go │ ├── transmit.go │ ├── init.go │ ├── database.go │ ├── finders.go │ ├── email.go │ └── model.go ├── realtime │ ├── starred.go │ └── counters.go ├── posts │ ├── deps.go │ ├── finders.go │ └── model.go ├── events │ ├── init.go │ ├── global.go │ ├── activities.go │ ├── mentions.go │ ├── audit.go │ ├── posts.go │ └── flags.go └── search │ └── init.go ├── .env.example ├── .gitmodules ├── modules ├── mail │ ├── mailer.go │ ├── test_mailer.go │ └── model.go ├── acl │ ├── model.go │ └── init.go ├── api │ └── controller │ │ ├── init.go │ │ ├── users │ │ ├── init.go │ │ ├── validation.go │ │ ├── patch.go │ │ └── user_recover_password.go │ │ ├── categories.go │ │ ├── posts │ │ ├── init.go │ │ ├── post_get.go │ │ └── download_assets.go │ │ ├── config.go │ │ ├── helpers.go │ │ ├── notifications.go │ │ ├── posts.go │ │ ├── pages.go │ │ ├── reactions.go │ │ ├── flags.go │ │ └── user.go ├── feed │ ├── list.go │ ├── light_post.go │ ├── votes.go │ ├── model.go │ └── rates.go ├── security │ ├── deps.go │ ├── user_trust.go │ ├── model.go │ └── init.go ├── content │ ├── parseable.go │ ├── parse.go │ └── init.go ├── gaming │ ├── deps.go │ ├── post.go │ ├── shop.go │ ├── init.go │ └── model.go ├── notifications │ ├── fake.go │ ├── init.go │ └── mention.go ├── assets │ ├── model.go │ └── assets.go ├── exceptions │ └── init.go ├── helpers │ ├── crypto.go │ └── init.go └── user │ └── one.go ├── core ├── content │ ├── parseable.go │ ├── deps.go │ ├── tags.go │ ├── processor.go │ └── assets.go ├── user │ ├── deps.go │ ├── trust.go │ └── mutators.go ├── common │ ├── findby.go │ ├── maps.go │ └── query.go ├── shell │ ├── events.go │ ├── command.go │ └── migration.go ├── config │ ├── update.go │ ├── reactions.go │ ├── banning.go │ └── model.go ├── events │ ├── constants.go │ ├── actions.go │ └── init.go ├── templates │ └── init.go ├── mail │ └── init.go └── http │ └── middlewares.go ├── .gitignore ├── deps ├── cache.go ├── ledis.go ├── config.go ├── container.go ├── logger.go ├── deps.go ├── mongo.go └── s3.go ├── .golangci.yml ├── static ├── resources │ ├── config.toml │ └── config.hjson └── templates │ ├── partials │ └── gtm.tmpl │ └── pages │ └── index.tmpl ├── .air.toml ├── config.hcl ├── config.toml.example ├── roles.json ├── .github └── workflows │ └── go.yml ├── cmd └── anzu │ └── config.go ├── internal └── dal │ └── seed.go ├── Dockerfile ├── docker-compose.yml ├── .do ├── app.yaml └── deploy.template.yaml └── CLAUDE.md /board/votes/model_methods.go: -------------------------------------------------------------------------------- 1 | package votes 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGO_URL=mongodb://root:example@localhost:27017/admin 2 | MONGO_NAME=anzu 3 | ENV=dev -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "static/frontend"] 2 | path = static/frontend 3 | url = ../frontend.git 4 | -------------------------------------------------------------------------------- /modules/mail/mailer.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | type Mailer interface { 4 | Send(Mail) string 5 | SendRaw(Raw) string 6 | } 7 | -------------------------------------------------------------------------------- /board/legacy/model/comment.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type CommentForm struct { 4 | Content string `json:"content" binding:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /modules/acl/model.go: -------------------------------------------------------------------------------- 1 | package acl 2 | 3 | type AclRole struct { 4 | Permissions []string `json:"permissions"` 5 | Inherits []string `json:"parents"` 6 | } 7 | -------------------------------------------------------------------------------- /modules/api/controller/init.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import "github.com/op/go-logging" 4 | 5 | var ( 6 | log = logging.MustGetLogger("controller") 7 | ) 8 | -------------------------------------------------------------------------------- /board/categories/deps.go: -------------------------------------------------------------------------------- 1 | package categories 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/mongo" 5 | ) 6 | 7 | type deps interface { 8 | Mgo() *mongo.Database 9 | } 10 | -------------------------------------------------------------------------------- /core/content/parseable.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | type Parseable interface { 4 | GetContent() string 5 | UpdateContent(string) Parseable 6 | GetParseableMeta() map[string]interface{} 7 | } 8 | -------------------------------------------------------------------------------- /modules/feed/list.go: -------------------------------------------------------------------------------- 1 | package feed 2 | 3 | type List struct { 4 | _ *FeedModule // Reserved for future use 5 | _ int // Reserved for future use 6 | _ int // Reserved for future use 7 | } 8 | -------------------------------------------------------------------------------- /modules/security/deps.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "github.com/xuyu/goredis" 5 | "gopkg.in/mgo.v2" 6 | ) 7 | 8 | type Deps interface { 9 | Mgo() *mgo.Database 10 | Cache() *goredis.Redis 11 | } 12 | -------------------------------------------------------------------------------- /modules/feed/light_post.go: -------------------------------------------------------------------------------- 1 | package feed 2 | 3 | type LightPost struct { 4 | di *FeedModule 5 | data LightPostModel 6 | } 7 | 8 | func (post *LightPost) Data() LightPostModel { 9 | 10 | return post.data 11 | } 12 | -------------------------------------------------------------------------------- /board/votes/errors.go: -------------------------------------------------------------------------------- 1 | package votes 2 | 3 | // NotAllowed check. 4 | type NotAllowed struct { 5 | Reason string 6 | } 7 | 8 | func (e *NotAllowed) Error() string { 9 | return "can't allow to perform operation" 10 | } 11 | -------------------------------------------------------------------------------- /core/user/deps.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/siddontang/ledisdb/ledis" 5 | "go.mongodb.org/mongo-driver/mongo" 6 | ) 7 | 8 | type deps interface { 9 | Mgo() *mongo.Database 10 | LedisDB() *ledis.DB 11 | } 12 | -------------------------------------------------------------------------------- /core/common/findby.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson" 5 | "go.mongodb.org/mongo-driver/bson/primitive" 6 | ) 7 | 8 | func ById(id primitive.ObjectID) bson.M { 9 | return bson.M{"_id": id} 10 | } 11 | -------------------------------------------------------------------------------- /modules/content/parseable.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | type Parseable interface { 4 | GetContent() string 5 | UpdateContent(string) bool 6 | OnParseFilterFinished(string) bool 7 | OnParseFinished() bool 8 | GetParseableMeta() map[string]interface{} 9 | } 10 | -------------------------------------------------------------------------------- /modules/gaming/deps.go: -------------------------------------------------------------------------------- 1 | package gaming 2 | 3 | import ( 4 | "github.com/tryanzu/core/board/legacy/model" 5 | "go.mongodb.org/mongo-driver/mongo" 6 | ) 7 | 8 | type Deps interface { 9 | Mgo() *mongo.Database 10 | GamingConfig() *model.GamingRules 11 | } 12 | -------------------------------------------------------------------------------- /board/activity/deps.go: -------------------------------------------------------------------------------- 1 | package activity 2 | 3 | import ( 4 | "github.com/op/go-logging" 5 | "go.mongodb.org/mongo-driver/mongo" 6 | ) 7 | 8 | var ( 9 | log = logging.MustGetLogger("activity") 10 | ) 11 | 12 | type deps interface { 13 | Mgo() *mongo.Database 14 | } 15 | -------------------------------------------------------------------------------- /modules/api/controller/users/init.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/tryanzu/core/modules/feed" 5 | "github.com/tryanzu/core/modules/user" 6 | ) 7 | 8 | type API struct { 9 | Feed *feed.FeedModule `inject:""` 10 | User *user.Module `inject:""` 11 | } 12 | -------------------------------------------------------------------------------- /board/assets/deps.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "github.com/siddontang/ledisdb/ledis" 5 | "github.com/tryanzu/core/deps" 6 | "go.mongodb.org/mongo-driver/mongo" 7 | ) 8 | 9 | type Deps interface { 10 | Mgo() *mongo.Database 11 | S3() *deps.S3Service 12 | LedisDB() *ledis.DB 13 | } 14 | -------------------------------------------------------------------------------- /board/comments/deps.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "github.com/siddontang/ledisdb/ledis" 5 | "github.com/tryanzu/core/deps" 6 | "go.mongodb.org/mongo-driver/mongo" 7 | ) 8 | 9 | type Deps interface { 10 | Mgo() *mongo.Database 11 | S3() *deps.S3Service 12 | LedisDB() *ledis.DB 13 | } 14 | -------------------------------------------------------------------------------- /board/flags/deps.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "github.com/siddontang/ledisdb/ledis" 5 | "github.com/tryanzu/core/deps" 6 | "go.mongodb.org/mongo-driver/mongo" 7 | ) 8 | 9 | type DepsInterface interface { 10 | Mgo() *mongo.Database 11 | S3() *deps.S3Service 12 | LedisDB() *ledis.DB 13 | } 14 | -------------------------------------------------------------------------------- /board/notifications/deps.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "github.com/siddontang/ledisdb/ledis" 5 | "github.com/tryanzu/core/deps" 6 | "go.mongodb.org/mongo-driver/mongo" 7 | ) 8 | 9 | type Deps interface { 10 | Mgo() *mongo.Database 11 | S3() *deps.S3Service 12 | LedisDB() *ledis.DB 13 | } 14 | -------------------------------------------------------------------------------- /board/activity/mutators.go: -------------------------------------------------------------------------------- 1 | package activity 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Track activity. 9 | func Track(d deps, activity M) (err error) { 10 | activity.Created = time.Now() 11 | ctx := context.TODO() 12 | _, err = d.Mgo().Collection("activity").InsertOne(ctx, activity) 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | development.json 3 | handle/ctrl 4 | blacker-osx 5 | dump.rdb 6 | env.json 7 | Procfile 8 | manifest.yml 9 | Godeps 10 | test/ 11 | .c9/ 12 | spartangeek-blacker 13 | .ottoid 14 | temp-blacker 15 | .otto/ 16 | experiments/xxx.go 17 | experiments/ 18 | .DS_Store 19 | vendor 20 | /.idea 21 | /anzu 22 | cache.db 23 | var 24 | .vscode 25 | .env 26 | -------------------------------------------------------------------------------- /modules/gaming/post.go: -------------------------------------------------------------------------------- 1 | package gaming 2 | 3 | import ( 4 | "github.com/tryanzu/core/modules/feed" 5 | ) 6 | 7 | type Post struct { 8 | post *feed.Post 9 | di *Module 10 | } 11 | 12 | // Review gaming facts of a post 13 | func (self *Post) Review() { 14 | 15 | //data := self.post.Data() 16 | //reached, viewed := self.post.GetReachViews() 17 | 18 | } 19 | -------------------------------------------------------------------------------- /board/legacy/acl.go: -------------------------------------------------------------------------------- 1 | package handle 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/tryanzu/core/modules/acl" 6 | ) 7 | 8 | type AclAPI struct { 9 | Acl *acl.Module `inject:""` 10 | } 11 | 12 | func (di *AclAPI) GetRules(c *gin.Context) { 13 | 14 | rules := di.Acl.Rules 15 | 16 | c.JSON(200, gin.H{"status": "okay", "rules": rules}) 17 | } 18 | -------------------------------------------------------------------------------- /core/common/maps.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | ) 6 | 7 | // UsersStringMap is just map of id -> username (cache purposes). 8 | type UsersStringMap map[primitive.ObjectID]string 9 | 10 | type AssetRef struct { 11 | URL string 12 | UseOriginal bool 13 | } 14 | 15 | type AssetRefsMap map[primitive.ObjectID]AssetRef 16 | -------------------------------------------------------------------------------- /modules/content/parse.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | func (self Module) Parse(o Parseable) error { 4 | 5 | chain := []func(Parseable) bool{ 6 | self.AsyncAssetDownload, 7 | self.ParseContentMentions, 8 | } 9 | 10 | for _, fn := range chain { 11 | next := fn(o) 12 | 13 | if !next { 14 | break 15 | } 16 | } 17 | 18 | o.OnParseFinished() 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /modules/notifications/fake.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "fmt" 5 | "github.com/tryanzu/core/board/legacy/model" 6 | ) 7 | 8 | type FakeBroadcaster struct { 9 | } 10 | 11 | func (broadcaster FakeBroadcaster) Send(message *model.UserFirebaseNotification) { 12 | 13 | fmt.Printf("\n\n%v\n\n", message) 14 | 15 | // Used on tests (dont do anything just yet) 16 | } 17 | -------------------------------------------------------------------------------- /board/votes/deps.go: -------------------------------------------------------------------------------- 1 | package votes 2 | 3 | import ( 4 | "github.com/siddontang/ledisdb/ledis" 5 | "go.mongodb.org/mongo-driver/bson/primitive" 6 | "go.mongodb.org/mongo-driver/mongo" 7 | ) 8 | 9 | type Deps interface { 10 | LedisDB() *ledis.DB 11 | Mgo() *mongo.Database 12 | } 13 | 14 | type Votable interface { 15 | VotableType() string 16 | VotableID() primitive.ObjectID 17 | } 18 | -------------------------------------------------------------------------------- /board/notifications/transmit.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/tryanzu/core/board/realtime" 7 | ) 8 | 9 | func transmitWorker(n int) { 10 | for n := range Transmit { 11 | m, err := json.Marshal(n) 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | realtime.ToChan <- realtime.M{Channel: n.Chan, Content: string(m)} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/security/user_trust.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | /** 4 | import ( 5 | "gopkg.in/mgo.v2/bson" 6 | "log" 7 | "time" 8 | ) 9 | 10 | func CanTrustUser(deps Deps, id bson.ObjectId) (score int, trust bool) { 11 | var user struct { 12 | Email string `bson:"email"` 13 | Validated string `bson:"validated"` 14 | Created time.Time `bson:"created_at"` 15 | } 16 | 17 | return 18 | } 19 | */ 20 | -------------------------------------------------------------------------------- /deps/cache.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | import ( 4 | "github.com/go-redis/redis/v8" 5 | ) 6 | 7 | var ( 8 | RedisURL string = "redis://127.0.0.1:6379" 9 | ) 10 | 11 | func IgniteCache(container Deps) (Deps, error) { 12 | url, err := redis.ParseURL(RedisURL) 13 | if err != nil { 14 | return container, err 15 | } 16 | client := redis.NewClient(url) 17 | container.CacheProvider = client 18 | return container, nil 19 | } 20 | -------------------------------------------------------------------------------- /modules/mail/test_mailer.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "github.com/op/go-logging" 5 | ) 6 | 7 | type TestMailer struct { 8 | Logger *logging.Logger 9 | } 10 | 11 | func (t TestMailer) Send(mail Mail) string { 12 | t.Logger.Debugf("Mail sent: %+v", mail) 13 | return "test-id" 14 | } 15 | 16 | func (t TestMailer) SendRaw(raw Raw) string { 17 | t.Logger.Debugf("Mail send: %+v", raw) 18 | return "test-id" 19 | } 20 | -------------------------------------------------------------------------------- /modules/security/model.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | ) 6 | 7 | type IpAddress struct { 8 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 9 | Address string `bson:"address" json:"address"` 10 | Users []primitive.ObjectID `bson:"users" json:"users"` 11 | Banned bool `bson:"banned" json:"banned"` 12 | } 13 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | 4 | issues: 5 | exclude-rules: 6 | - path: ^board/events/(comments|activities|mentions|posts)\.go$ 7 | linters: 8 | - typecheck 9 | text: typecheck false positives from generated event handlers 10 | - path: ^modules/api/controller/categories\.go$ 11 | linters: 12 | - typecheck 13 | text: typecheck false positives from generated controller 14 | -------------------------------------------------------------------------------- /board/legacy/model/stat.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | ) 6 | 7 | type StatsComments struct { 8 | Id primitive.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` 9 | Count int `bson:"count" json:"count"` 10 | } 11 | 12 | type Stats struct { 13 | Comments int `json:"comments"` 14 | Users int `json:"users"` 15 | Posts int `json:"posts"` 16 | } 17 | -------------------------------------------------------------------------------- /board/legacy/gaming.go: -------------------------------------------------------------------------------- 1 | package handle 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/tryanzu/core/modules/gaming" 6 | ) 7 | 8 | type GamingAPI struct { 9 | Gaming *gaming.Module `inject:""` 10 | } 11 | 12 | // Get gamification rules (roles, badges) 13 | func (di *GamingAPI) GetRules(c *gin.Context) { 14 | 15 | rules := di.Gaming.GetRules() 16 | 17 | // Just return the previously loaded rules 18 | c.JSON(200, rules) 19 | } 20 | -------------------------------------------------------------------------------- /board/realtime/starred.go: -------------------------------------------------------------------------------- 1 | package realtime 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | var featuredM chan M 9 | 10 | func starredMessagesWorker() { 11 | for m := range featuredM { 12 | var ev SocketEvent 13 | err := json.Unmarshal([]byte(m.Content), &ev) 14 | if err != nil { 15 | continue 16 | } 17 | ev.Params["at"] = time.Now() 18 | m.Content = ev.encode() 19 | ToChan <- m 20 | time.Sleep(15 * time.Second) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /deps/ledis.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | import ( 4 | lediscfg "github.com/siddontang/ledisdb/config" 5 | "github.com/siddontang/ledisdb/ledis" 6 | ) 7 | 8 | func IgniteLedisDB(container Deps) (Deps, error) { 9 | conf := lediscfg.NewConfigDefault() 10 | conn, err := ledis.Open(conf) 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | 15 | db, err := conn.Select(0) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | container.LedisProvider = db 21 | return container, nil 22 | } 23 | -------------------------------------------------------------------------------- /board/legacy/model/gaming.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type GamingRules struct { 8 | Updated time.Time `json:"updated_at"` 9 | Rules []RuleModel `json:"rules"` 10 | } 11 | 12 | type RuleModel struct { 13 | Level int `json:"level"` 14 | Name string `json:"name"` 15 | Start int `json:"swords_start"` 16 | End int `json:"swords_end"` 17 | Tribute int `json:"tribute"` 18 | Shit int `json:"shit"` 19 | Coins int `json:"coins"` 20 | } 21 | -------------------------------------------------------------------------------- /board/legacy/model/sitemap.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/xml" 5 | ) 6 | 7 | type SitemapSet struct { 8 | XMLName xml.Name `xml:"urlset"` 9 | XMLNs string `xml:"xmlns,attr"` 10 | XSI string `xml:"xmlns:xsi,attr"` 11 | XSILocation string `xml:"xsi:schemaLocation,attr"` 12 | Urls []SitemapUrl `xml:"url"` 13 | } 14 | 15 | type SitemapUrl struct { 16 | Location string `xml:"loc"` 17 | Updated string `xml:"lastmod"` 18 | Priority string `xml:"priority"` 19 | } 20 | -------------------------------------------------------------------------------- /static/resources/config.toml: -------------------------------------------------------------------------------- 1 | # Site configuration. 2 | # Mostly front-end needed config that will be for public purposes. 3 | homedir = "./" 4 | [site] 5 | name = "Anzu" 6 | description = "Next-generation community engine, bring the conversation online." 7 | url = "http://localhost:3200/" 8 | logoUrl = "/images/anzu.svg" 9 | theme = "autumn" 10 | reactions = [ 11 | ["default", "upvote", "downvote"], 12 | ] 13 | 14 | [[site.nav]] 15 | name = "Inicio" 16 | href = "/" 17 | 18 | [[site.nav]] 19 | name = "Preguntas frecuentes" 20 | href = "/" -------------------------------------------------------------------------------- /board/notifications/init.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | // How much capacity each of the incoming notifications channels will have. 4 | const BuffersLength = 10 5 | const PoolSize = 4 6 | 7 | // Define pool of channels. 8 | var ( 9 | Transmit chan Socket 10 | Database chan Notification 11 | ) 12 | 13 | func init() { 14 | Transmit = make(chan Socket, BuffersLength) 15 | Database = make(chan Notification, BuffersLength) 16 | 17 | for n := 0; n < PoolSize; n++ { 18 | go databaseWorker(n) 19 | go transmitWorker(n) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/content/deps.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "github.com/siddontang/ledisdb/ledis" 5 | "github.com/tryanzu/core/core/config" 6 | "github.com/tryanzu/core/deps" 7 | "go.mongodb.org/mongo-driver/mongo" 8 | ) 9 | 10 | type DepsInterface interface { 11 | Mgo() *mongo.Database 12 | S3() *deps.S3Service 13 | LedisDB() *ledis.DB 14 | } 15 | 16 | func Boot() { 17 | log.SetBackend(config.LoggingBackend) 18 | go func() { 19 | for { 20 | <-config.C.Reload 21 | log.SetBackend(config.LoggingBackend) 22 | } 23 | }() 24 | } 25 | -------------------------------------------------------------------------------- /board/posts/deps.go: -------------------------------------------------------------------------------- 1 | package post 2 | 3 | import ( 4 | "github.com/op/go-logging" 5 | "github.com/siddontang/ledisdb/ledis" 6 | "github.com/tryanzu/core/core/config" 7 | "go.mongodb.org/mongo-driver/mongo" 8 | ) 9 | 10 | type deps interface { 11 | Mgo() *mongo.Database 12 | LedisDB() *ledis.DB 13 | } 14 | 15 | var log = logging.MustGetLogger("search") 16 | 17 | func Boot() { 18 | log.SetBackend(config.LoggingBackend) 19 | go func() { 20 | for { 21 | <-config.C.Reload 22 | log.SetBackend(config.LoggingBackend) 23 | } 24 | }() 25 | } 26 | -------------------------------------------------------------------------------- /core/shell/events.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "github.com/abiosoft/ishell" 5 | _ "github.com/tryanzu/core/board/comments" 6 | _ "github.com/tryanzu/core/board/posts" 7 | "github.com/tryanzu/core/core/events" 8 | "go.mongodb.org/mongo-driver/bson/primitive" 9 | ) 10 | 11 | func TestEventHandler(c *ishell.Context) { 12 | c.ShowPrompt(false) 13 | defer c.ShowPrompt(true) 14 | 15 | c.Println("Testing events mechanism") 16 | 17 | objID, _ := primitive.ObjectIDFromHex("59a9a33bcdab0b5dcb31d4b0") 18 | events.In <- events.PostComment(objID) 19 | } 20 | -------------------------------------------------------------------------------- /modules/assets/model.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | type Asset struct { 10 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id"` 11 | Related string `bson:"related" json:"related"` 12 | RelatedId primitive.ObjectID `bson:"related_id" json:"related_id"` 13 | Path string `bson:"path" json:"path"` 14 | Meta interface{} `bson:"meta" json:"meta"` 15 | Created time.Time `bson:"created_at" json:"created_at"` 16 | } 17 | -------------------------------------------------------------------------------- /deps/config.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | var ( 9 | ENV string 10 | AppSecret string 11 | 12 | // SentryURL config 13 | SentryURL string 14 | ) 15 | 16 | func IgniteConfig(d Deps) (container Deps, err error) { 17 | gamingRules, err := os.ReadFile("gaming.json") 18 | if err != nil { 19 | log.Error(err) 20 | return 21 | } 22 | 23 | err = json.Unmarshal(gamingRules, &d.GamingConfigProvider) 24 | if err != nil { 25 | log.Error(err) 26 | return 27 | } 28 | 29 | container = d 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /static/templates/partials/gtm.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /board/assets/mutators.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | ) 9 | 10 | // FromURL asset. 11 | func FromURL(deps Deps, url string) (ref Asset, err error) { 12 | ref = Asset{ 13 | ID: primitive.NewObjectID(), 14 | Original: url, 15 | Status: "awaiting", 16 | Created: time.Now(), 17 | Updated: time.Now(), 18 | } 19 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 20 | defer cancel() 21 | _, err = deps.Mgo().Collection("remote_assets").InsertOne(ctx, &ref) 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /modules/api/controller/categories.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/tryanzu/core/board/categories" 6 | "github.com/tryanzu/core/deps" 7 | "github.com/tryanzu/core/modules/acl" 8 | "go.mongodb.org/mongo-driver/bson/primitive" 9 | ) 10 | 11 | // Categories paginated fetch. 12 | func Categories(c *gin.Context) { 13 | tree := categories.MakeTree(deps.Container) 14 | if sid, exists := c.Get("userID"); exists { 15 | uid := sid.(primitive.ObjectID) 16 | auth := acl.LoadedACL.User(uid) 17 | tree = tree.CheckWrite(auth.CanWrite) 18 | } 19 | c.JSON(200, tree) 20 | } 21 | -------------------------------------------------------------------------------- /modules/notifications/init.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "github.com/tryanzu/core/board/legacy/model" 5 | "github.com/tryanzu/core/modules/exceptions" 6 | "github.com/tryanzu/core/modules/user" 7 | "gopkg.in/mgo.v2/bson" 8 | ) 9 | 10 | type NotificationsModule struct { 11 | Errors *exceptions.ExceptionsModule `inject:""` 12 | User *user.Module `inject:""` 13 | } 14 | 15 | type MentionParseObject struct { 16 | Type string 17 | RelatedNested string 18 | Content string 19 | Title string 20 | Author bson.ObjectId 21 | Post model.Post 22 | } 23 | -------------------------------------------------------------------------------- /board/events/init.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/op/go-logging" 5 | pool "github.com/tryanzu/core/core/events" 6 | ) 7 | 8 | var ( 9 | log = logging.MustGetLogger("main") 10 | ) 11 | 12 | func init() { 13 | globalEvents() 14 | activityEvents() 15 | commentsEvents() 16 | postsEvents() 17 | mentionEvents() 18 | flagHandlers() 19 | } 20 | 21 | func register(list []pool.EventHandler) { 22 | for _, h := range list { 23 | pool.On <- h 24 | } 25 | } 26 | 27 | func pipeErr(err ...error) error { 28 | for _, e := range err { 29 | if e != nil { 30 | return e 31 | } 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /modules/content/init.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "github.com/olebedev/config" 5 | "github.com/tryanzu/core/deps" 6 | "github.com/tryanzu/core/modules/exceptions" 7 | "github.com/tryanzu/core/modules/notifications" 8 | "github.com/xuyu/goredis" 9 | ) 10 | 11 | type Module struct { 12 | Errors *exceptions.ExceptionsModule `inject:""` 13 | S3 *deps.S3Service `inject:""` 14 | Config *config.Config `inject:""` 15 | Notifications *notifications.NotificationsModule `inject:""` 16 | Redis *goredis.Redis `inject:""` 17 | } 18 | -------------------------------------------------------------------------------- /board/legacy/model/activity.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | "time" 6 | ) 7 | 8 | type Activity struct { 9 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id"` 10 | UserId primitive.ObjectID `bson:"user_id,omitempty" json:"user_id"` 11 | Event string `bson:"event,omitempty" event:"related"` 12 | RelatedId primitive.ObjectID `bson:"related_id,omitempty" json:"related_id,omitempty"` 13 | List []primitive.ObjectID `bson:"list,omitempty" json:"list,omitempty"` 14 | Created time.Time `bson:"created_at" json:"created_at"` 15 | } 16 | -------------------------------------------------------------------------------- /core/config/update.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | 7 | "github.com/BurntSushi/toml" 8 | "github.com/divideandconquer/go-merge/merge" 9 | ) 10 | 11 | func MergeUpdate(config map[string]interface{}) error { 12 | 13 | // Copy of current runtime config. 14 | current := C.UserCopy() 15 | merged := merge.Merge(current, config) 16 | buf := new(bytes.Buffer) 17 | encoder := toml.NewEncoder(buf) 18 | 19 | if err := encoder.Encode(merged); err != nil { 20 | return err 21 | } 22 | 23 | if err := os.WriteFile("./config.toml", buf.Bytes(), 0644); err != nil { 24 | return err 25 | } 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /board/activity/model.go: -------------------------------------------------------------------------------- 1 | package activity 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // M stands for activity model. 10 | type M struct { 11 | ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` 12 | UserID primitive.ObjectID `bson:"user_id,omitempty" json:"user_id"` 13 | Event string `bson:"event,omitempty" event:"related"` 14 | RelatedID primitive.ObjectID `bson:"related_id,omitempty" json:"related_id,omitempty"` 15 | List []primitive.ObjectID `bson:"list,omitempty" json:"list,omitempty"` 16 | Created time.Time `bson:"created_at" json:"created_at"` 17 | } 18 | -------------------------------------------------------------------------------- /board/events/global.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | notify "github.com/tryanzu/core/board/notifications" 5 | pool "github.com/tryanzu/core/core/events" 6 | ) 7 | 8 | // Bind event handlers for global unrelated actions... 9 | func globalEvents() { 10 | pool.On <- pool.EventHandler{ 11 | On: pool.RAW_EMIT, 12 | Handler: func(e pool.Event) (err error) { 13 | 14 | // Just broadcast it to transmit channel 15 | m := notify.Socket{ 16 | Chan: e.Params["channel"].(string), 17 | Action: e.Params["event"].(string), 18 | Params: e.Params["params"].(map[string]interface{}), 19 | } 20 | notify.Transmit <- m 21 | return 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /deps/container.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | import ( 4 | slog "log" 5 | ) 6 | 7 | // Contains bootstraped dependencies. 8 | var ( 9 | Container Deps 10 | ) 11 | 12 | // An ignitor takes a Container and injects bootstraped dependencies. 13 | type Ignitor func(Deps) (Deps, error) 14 | 15 | // Runs ignitors to fulfill deps container. 16 | func Bootstrap() { 17 | ignitors := []Ignitor{ 18 | IgniteLogger, 19 | IgniteConfig, 20 | IgniteMongoDB, 21 | IgniteCache, 22 | IgniteS3, 23 | IgniteLedisDB, 24 | } 25 | 26 | var err error 27 | Container = Deps{} 28 | for _, fn := range ignitors { 29 | Container, err = fn(Container) 30 | if err != nil { 31 | slog.Panic(err) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /board/events/activities.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/tryanzu/core/board/legacy/model" 8 | pool "github.com/tryanzu/core/core/events" 9 | "github.com/tryanzu/core/deps" 10 | ) 11 | 12 | // Bind event handlers for activity related actions... 13 | func activityEvents() { 14 | pool.On <- pool.EventHandler{ 15 | On: pool.RECENT_ACTIVITY, 16 | Handler: func(e pool.Event) (err error) { 17 | activity := e.Params["activity"].(model.Activity) 18 | activity.Created = time.Now() 19 | 20 | // Attempt to record recent activity. 21 | _, err = deps.Container.Mgo().Collection("activity").InsertOne(context.Background(), activity) 22 | return 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | tmp_dir = "tmp" 3 | 4 | [build] 5 | bin = "./tmp/main api" 6 | cmd = "go build -o ./tmp/main ./cmd/anzu" 7 | delay = 1000 8 | exclude_dir = ["assets", "tmp", "vendor", "static", "var"] 9 | exclude_file = [] 10 | exclude_regex = [] 11 | exclude_unchanged = false 12 | follow_symlink = false 13 | full_bin = "" 14 | include_dir = [] 15 | include_ext = ["go", "tpl", "tmpl", "html"] 16 | kill_delay = "0s" 17 | log = "build-errors.log" 18 | send_interrupt = false 19 | stop_on_error = true 20 | 21 | [color] 22 | app = "" 23 | build = "yellow" 24 | main = "magenta" 25 | runner = "green" 26 | watcher = "cyan" 27 | 28 | [log] 29 | time = false 30 | 31 | [misc] 32 | clean_on_exit = false 33 | -------------------------------------------------------------------------------- /board/legacy/error.go: -------------------------------------------------------------------------------- 1 | package handle 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/getsentry/raven-go" 7 | ) 8 | 9 | type ErrorAPI struct { 10 | ErrorService *raven.Client `inject:""` 11 | } 12 | 13 | func (di *ErrorAPI) Recover() { 14 | 15 | var packet *raven.Packet 16 | 17 | switch rval := recover().(type) { 18 | case nil: 19 | return 20 | case error: 21 | packet = raven.NewPacket(rval.Error(), raven.NewException(rval, raven.NewStacktrace(2, 3, nil))) 22 | default: 23 | rvalStr := fmt.Sprint(rval) 24 | packet = raven.NewPacket(rvalStr, raven.NewException(errors.New(rvalStr), raven.NewStacktrace(2, 3, nil))) 25 | } 26 | 27 | // Grab the error and send it to sentry 28 | di.ErrorService.Capture(packet, map[string]string{}) 29 | } 30 | -------------------------------------------------------------------------------- /board/legacy/helpers_test.go: -------------------------------------------------------------------------------- 1 | package handle 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func TestSlugAscii(t *testing.T) { 9 | 10 | var tests = []struct{ in, out string }{ 11 | {"PC '¿Que opinan spartanos? 2.0'", "pc-que-opinan-spartanos-2-0"}, 12 | {"Clan de destiny ayudamos a todos :)", "clan-de-destiny-ayudamos-a-todos"}, 13 | {"AMD FX-6300 ó AMD Athlon x4 750k?", "amd-fx-6300-o-amd-athlon-x4-750k"}, 14 | } 15 | 16 | Convey("Make sure the slug generator works properly with weird characters", t, func() { 17 | 18 | for _, test := range tests { 19 | 20 | Convey(test.in+" should be "+test.out, func() { 21 | 22 | So(str_slug(test.in), ShouldEqual, test.out) 23 | }) 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /modules/api/controller/posts/init.go: -------------------------------------------------------------------------------- 1 | package posts 2 | 3 | import ( 4 | "github.com/olebedev/config" 5 | "github.com/tryanzu/core/deps" 6 | "github.com/tryanzu/core/modules/acl" 7 | "github.com/tryanzu/core/modules/exceptions" 8 | "github.com/tryanzu/core/modules/feed" 9 | "github.com/tryanzu/core/modules/gaming" 10 | 11 | "regexp" 12 | ) 13 | 14 | var legalSlug = regexp.MustCompile(`^([a-zA-Z0-9\-\.|/]+)$`) 15 | 16 | type API struct { 17 | Feed *feed.FeedModule `inject:""` 18 | Acl *acl.Module `inject:""` 19 | Gaming *gaming.Module `inject:""` 20 | Errors *exceptions.ExceptionsModule `inject:""` 21 | Config *config.Config `inject:""` 22 | S3 *deps.S3Service `inject:""` 23 | } 24 | -------------------------------------------------------------------------------- /config.hcl: -------------------------------------------------------------------------------- 1 | banReason spam { 2 | effects = <= 0 && buf[i] == '-' { 34 | buf = buf[:i] 35 | } 36 | return string(buf) 37 | } 38 | -------------------------------------------------------------------------------- /core/config/banning.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/dop251/goja" 5 | ) 6 | 7 | // BanReason config def. 8 | type BanReason struct { 9 | Code string `hcl:"effects"` 10 | } 11 | 12 | // BanEffects def. 13 | type BanEffects struct { 14 | Duration int64 15 | IPAddress bool 16 | } 17 | 18 | // Effects from config (duration, ipaddress). 19 | func (re BanReason) Effects(related string, times int) (BanEffects, error) { 20 | if len(re.Code) == 0 { 21 | return BanEffects{60, false}, nil 22 | } 23 | vm := goja.New() 24 | _, _ = vm.RunString(` 25 | var exports = {}; 26 | `) 27 | vm.Set("banN", times) 28 | vm.Set("related", related) 29 | if _, err := vm.RunString(re.Code); err != nil { 30 | return BanEffects{}, err 31 | } 32 | obj := vm.Get("exports").ToObject(vm) 33 | duration := obj.Get("duration").ToInteger() 34 | ip := obj.Get("ip").ToBoolean() 35 | 36 | return BanEffects{duration, ip}, nil 37 | } 38 | -------------------------------------------------------------------------------- /deps/logger.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/op/go-logging" 7 | ) 8 | 9 | var log = logging.MustGetLogger("blacker") 10 | 11 | // Example format string. Everything except the message has a custom color 12 | // which is dependent on the log level. Many fields have a custom output 13 | // formatting too, eg. the time returns the hour down to the milli second. 14 | var format = logging.MustStringFormatter( 15 | `%{color}%{time:15:04:05.000} %{pid} %{module} %{shortfile} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`, 16 | ) 17 | 18 | func IgniteLogger(container Deps) (Deps, error) { 19 | backend := logging.NewLogBackend(os.Stdout, "", 0) 20 | formatter := logging.NewBackendFormatter(backend, format) 21 | leveled := logging.AddModuleLevel(formatter) 22 | leveled.SetLevel(logging.DEBUG, "") 23 | logging.SetBackend(leveled) 24 | container.LoggerProvider = log 25 | return container, nil 26 | } 27 | -------------------------------------------------------------------------------- /modules/api/controller/config.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/tryanzu/core/core/config" 9 | ) 10 | 11 | // UpdateConfig from running anzu. 12 | func UpdateConfig(c *gin.Context) { 13 | var update ConfigUpdate 14 | if err := c.Bind(&update); err != nil { 15 | _ = c.AbortWithError(http.StatusBadRequest, errors.New("invalid payload.")) 16 | return 17 | } 18 | 19 | //usr := c.MustGet("user").(user.User) 20 | err := config.MergeUpdate(map[string]interface{}{ 21 | update.Section: update.Changes, 22 | }) 23 | if err != nil { 24 | _ = c.AbortWithError(http.StatusInternalServerError, err) 25 | return 26 | } 27 | 28 | c.JSON(http.StatusOK, gin.H{"status": "okay"}) 29 | } 30 | 31 | type ConfigUpdate struct { 32 | Section string `json:"section" binding:"required"` 33 | Changes map[string]interface{} `json:"changes" binding:"required"` 34 | } 35 | -------------------------------------------------------------------------------- /core/shell/command.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import "github.com/abiosoft/ishell" 4 | 5 | func RunShell() { 6 | shell := ishell.New() 7 | 8 | shell.AddCmd(&ishell.Cmd{ 9 | Name: "cleanup-emails", 10 | Help: "Find email duplicates & allow to clean them up.", 11 | Func: CleanupDuplicatedEmails, 12 | }) 13 | 14 | shell.AddCmd(&ishell.Cmd{ 15 | Name: "test-events", 16 | Help: "Test events abstraction.", 17 | Func: TestEventHandler, 18 | }) 19 | 20 | shell.AddCmd(&ishell.Cmd{ 21 | Name: "migrate-comments", 22 | Help: "Migrate legacy comments (before anzu).", 23 | Func: MigrateComments, 24 | }) 25 | 26 | shell.AddCmd(&ishell.Cmd{ 27 | Name: "gc", 28 | Help: "Run garbage collector and timed events.", 29 | Func: RunAnzuGarbageCollector, 30 | }) 31 | 32 | shell.AddCmd(&ishell.Cmd{ 33 | Name: "rebuild-trustnet", 34 | Help: "Rebuild trustnet from scratch", 35 | Func: RebuildTrustNet, 36 | }) 37 | 38 | // start shell 39 | shell.Start() 40 | 41 | select {} 42 | } 43 | -------------------------------------------------------------------------------- /modules/api/controller/helpers.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/tryanzu/core/modules/acl" 6 | "gopkg.in/go-playground/validator.v8" 7 | ) 8 | 9 | func jsonErr(c *gin.Context, status int, message string) { 10 | // This specific json error structure is handled 11 | // by the frontend in a generic way so errors 12 | // can be shown to the user and also translated. 13 | c.AbortWithStatusJSON(status, gin.H{ 14 | "status": "error", 15 | "message": message, 16 | }) 17 | } 18 | 19 | func jsonBindErr(c *gin.Context, status int, message string, bindErr error) { 20 | // This specific json error structure is handled 21 | // by the frontend in a generic way so errors 22 | // can be shown to the user and also translated. 23 | c.AbortWithStatusJSON(status, gin.H{ 24 | "status": "error", 25 | "message": message, 26 | "details": bindErr.(validator.ValidationErrors), 27 | }) 28 | } 29 | 30 | func perms(c *gin.Context) *acl.User { 31 | return c.MustGet("acl").(*acl.User) 32 | } 33 | -------------------------------------------------------------------------------- /core/content/tags.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "regexp" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | var tagRegex, _ = regexp.Compile(`(?i)\[([a-z0-9]+(:?))+\]`) 10 | var tagParamsRegex, _ = regexp.Compile(`(?i)(([a-z0-9]+)(:?))+?`) 11 | 12 | type tag struct { 13 | Original string 14 | Name string 15 | Params []string 16 | } 17 | 18 | type tags []tag 19 | 20 | func (list tags) withTag(name string) tags { 21 | filtered := tags{} 22 | for _, tag := range list { 23 | if tag.Name != name { 24 | continue 25 | } 26 | filtered = append(filtered, tag) 27 | } 28 | return filtered 29 | } 30 | 31 | func (list tags) getIdParams(index int) (id []primitive.ObjectID) { 32 | for _, tag := range list { 33 | if len(tag.Params) < index+1 { 34 | continue 35 | } 36 | 37 | if cid := tag.Params[index]; func() bool { _, err := primitive.ObjectIDFromHex(cid); return err == nil }() { 38 | objID, _ := primitive.ObjectIDFromHex(cid) 39 | id = append(id, objID) 40 | } 41 | } 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /board/flags/model.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | type status string 10 | 11 | const ( 12 | PENDING status = "pending" 13 | REJECTED status = "rejected" 14 | ) 15 | 16 | // Flag represents a report sent by a user flagging a post/comment. 17 | type Flag struct { 18 | ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 19 | UserID primitive.ObjectID `bson:"user_id" json:"user_id"` 20 | RelatedTo string `bson:"related_to" json:"related_to"` 21 | RelatedID *primitive.ObjectID `bson:"related_id" json:"related_id,omitempty"` 22 | Content string `bson:"content" json:"content"` 23 | Status status `bson:"status" json:"status"` 24 | Reason string `bson:"reason" json:"reason"` 25 | Created time.Time `bson:"created_at" json:"created_at"` 26 | Updated time.Time `bson:"updated_at" json:"updated_at"` 27 | Deleted *time.Time `bson:"deleted_at,omitempty" json:"-"` 28 | } 29 | -------------------------------------------------------------------------------- /board/legacy/model/election.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | "time" 6 | ) 7 | 8 | type ElectionOption struct { 9 | UserId primitive.ObjectID `bson:"user_id" json:"user_id"` 10 | Content string `bson:"content" json:"content"` 11 | User interface{} `bson:"author,omitempty" json:"author,omitempty"` 12 | Votes Votes `bson:"votes" json:"votes"` 13 | Created time.Time `bson:"created_at" json:"created_at"` 14 | } 15 | 16 | type ElectionForm struct { 17 | Component string `json:"component" binding:"required"` 18 | Content string `json:"content" binding:"required"` 19 | } 20 | 21 | // ByElectionsCreatedAt implements sort.Interface for []ElectionOption based on Created field 22 | type ByElectionsCreatedAt []ElectionOption 23 | 24 | func (a ByElectionsCreatedAt) Len() int { return len(a) } 25 | func (a ByElectionsCreatedAt) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 26 | func (a ByElectionsCreatedAt) Less(i, j int) bool { return !a[i].Created.Before(a[j].Created) } 27 | -------------------------------------------------------------------------------- /board/activity/finders.go: -------------------------------------------------------------------------------- 1 | package activity 2 | 3 | import ( 4 | "context" 5 | 6 | "go.mongodb.org/mongo-driver/bson" 7 | ) 8 | 9 | func Count(d deps, q bson.M) int { 10 | ctx := context.TODO() 11 | n, err := d.Mgo().Collection("activity").CountDocuments(ctx, q) 12 | if err != nil { 13 | panic(err) 14 | } 15 | return int(n) 16 | } 17 | 18 | func CountList(d deps, q bson.M) int { 19 | var result struct { 20 | Count int `bson:"count"` 21 | } 22 | q["list"] = bson.M{"$exists": true} 23 | ctx := context.TODO() 24 | cursor, err := d.Mgo().Collection("activity").Aggregate(ctx, []bson.M{ 25 | {"$match": q}, 26 | {"$project": bson.M{"size": bson.M{"$size": "$list"}}}, 27 | {"$group": bson.M{"_id": "null", "count": bson.M{"$sum": "$size"}}}, 28 | }) 29 | if err != nil { 30 | log.Errorf("activity count err=%v", err) 31 | return 0 32 | } 33 | defer cursor.Close(ctx) 34 | if cursor.Next(ctx) { 35 | err = cursor.Decode(&result) 36 | if err != nil { 37 | log.Errorf("activity count decode err=%v", err) 38 | return 0 39 | } 40 | } 41 | return result.Count 42 | } 43 | -------------------------------------------------------------------------------- /board/legacy/model/vote.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | "time" 6 | ) 7 | 8 | type Vote struct { 9 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 10 | UserId primitive.ObjectID `bson:"user_id" json:"user_id"` 11 | Type string `bson:"type" json:"type"` 12 | NestedType string `bson:"nested_type,omitempty" json:"nested_type,omitempty"` 13 | RelatedId primitive.ObjectID `bson:"related_id" json:"related_id"` 14 | Value int `bson:"value" json:"value"` 15 | Created time.Time `bson:"created_at" json:"created_at"` 16 | } 17 | 18 | type VoteForm struct { 19 | Component string `json:"component" binding:"required"` 20 | Direction string `json:"direction" binding:"required"` 21 | } 22 | 23 | type VoteCommentForm struct { 24 | Comment string `json:"comment" binding:"required"` 25 | Direction string `json:"direction" binding:"required"` 26 | } 27 | 28 | type VotePostForm struct { 29 | Direction string `json:"direction" binding:"required"` 30 | } 31 | -------------------------------------------------------------------------------- /board/votes/finders.go: -------------------------------------------------------------------------------- 1 | package votes 2 | 3 | import ( 4 | "context" 5 | "github.com/tryanzu/core/core/common" 6 | "go.mongodb.org/mongo-driver/bson" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | "go.mongodb.org/mongo-driver/mongo" 9 | ) 10 | 11 | // FindVotableByUser gets the votes for ref. 12 | func FindVotableByUser(deps Deps, votable Votable, userID primitive.ObjectID) (vote Vote, err error) { 13 | ctx := context.TODO() 14 | err = deps.Mgo().Collection("votes").FindOne(ctx, bson.M{ 15 | "type": votable.VotableType(), 16 | "related_id": votable.VotableID(), 17 | "user_id": userID, 18 | }).Decode(&vote) 19 | if err == mongo.ErrNoDocuments { 20 | err = nil // No vote found is not an error 21 | } 22 | return 23 | } 24 | 25 | // FindList of votes for given scopes. 26 | func FindList(deps Deps, scopes ...common.Scope) (list List, err error) { 27 | ctx := context.TODO() 28 | cursor, err := deps.Mgo().Collection("votes").Find(ctx, common.ByScope(scopes...)) 29 | if err != nil { 30 | return 31 | } 32 | defer cursor.Close(ctx) 33 | err = cursor.All(ctx, &list) 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /deps/deps.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | import ( 4 | "github.com/go-redis/redis/v8" 5 | "github.com/op/go-logging" 6 | "github.com/siddontang/ledisdb/ledis" 7 | "github.com/tryanzu/core/board/legacy/model" 8 | "go.mongodb.org/mongo-driver/mongo" 9 | ) 10 | 11 | type Deps struct { 12 | GamingConfigProvider *model.GamingRules 13 | DatabaseSessionProvider *mongo.Client 14 | DatabaseProvider *mongo.Database 15 | LoggerProvider *logging.Logger 16 | CacheProvider *redis.Client 17 | S3Provider *S3Service 18 | LedisProvider *ledis.DB 19 | } 20 | 21 | func (d Deps) GamingConfig() *model.GamingRules { 22 | return d.GamingConfigProvider 23 | } 24 | 25 | func (d Deps) Log() *logging.Logger { 26 | return d.LoggerProvider 27 | 28 | } 29 | 30 | func (d Deps) Mgo() *mongo.Database { 31 | return d.DatabaseProvider 32 | } 33 | 34 | func (d Deps) LedisDB() *ledis.DB { 35 | return d.LedisProvider 36 | } 37 | 38 | func (d Deps) MgoSession() *mongo.Client { 39 | return d.DatabaseSessionProvider 40 | } 41 | 42 | func (d Deps) S3() *S3Service { 43 | return d.S3Provider 44 | } 45 | -------------------------------------------------------------------------------- /board/events/mentions.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | notify "github.com/tryanzu/core/board/notifications" 5 | ev "github.com/tryanzu/core/core/events" 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // Bind event handlers for posts related actions... 10 | func mentionEvents() { 11 | ev.On <- ev.EventHandler{ 12 | On: ev.NEW_MENTION, 13 | Handler: func(e ev.Event) error { 14 | userID := e.Params["user_id"].(primitive.ObjectID) 15 | relatedID := e.Params["related_id"].(primitive.ObjectID) 16 | users := e.Params["users"].([]primitive.ObjectID) 17 | related := e.Params["related"].(string) 18 | 19 | // Create notification 20 | if related == "comment" { 21 | notify.Database <- notify.Notification{ 22 | UserId: userID, 23 | Type: "mention", 24 | RelatedId: relatedID, 25 | Users: users, 26 | } 27 | } 28 | if related == "chat" { 29 | notify.Database <- notify.Notification{ 30 | UserId: userID, 31 | Type: "chat", 32 | RelatedId: relatedID, 33 | Users: users, 34 | } 35 | } 36 | return nil 37 | }, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /modules/api/controller/users/validation.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/tryanzu/core/core/user" 8 | "github.com/tryanzu/core/deps" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | ) 11 | 12 | func (this API) ResendConfirmation(c *gin.Context) { 13 | id := c.MustGet("userID").(primitive.ObjectID) 14 | 15 | // Get the user using its id 16 | usr, err := user.FindId(deps.Container, id) 17 | if err != nil { 18 | if err == user.UserNotFound { 19 | c.JSON(http.StatusNotFound, gin.H{"status": "error", "message": "User not found"}) 20 | return 21 | } 22 | c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Failed to retrieve user"}) 23 | return 24 | } 25 | 26 | if usr.Validated { 27 | c.JSON(http.StatusConflict, gin.H{"status": "error", "message": "User has been validated already."}) 28 | return 29 | } 30 | 31 | err = usr.ConfirmationEmail(deps.Container) 32 | if err != nil { 33 | c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": err.Error()}) 34 | return 35 | } 36 | 37 | c.JSON(http.StatusOK, gin.H{"status": "okay"}) 38 | } 39 | -------------------------------------------------------------------------------- /board/events/audit.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | ev "github.com/tryanzu/core/core/events" 8 | "github.com/tryanzu/core/deps" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | ) 11 | 12 | type auditM struct { 13 | ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 14 | UserID primitive.ObjectID `bson:"user_id" json:"user_id"` 15 | Related string `bson:"related" json:"related"` 16 | RelatedID primitive.ObjectID `bson:"related_id" json:"related_id"` 17 | Reason string `bson:"reason" json:"reason"` 18 | Action string `bson:"action" json:"action"` 19 | Created time.Time `bson:"created_at" json:"created_at"` 20 | } 21 | 22 | // Audit action log. 23 | func audit(related string, id primitive.ObjectID, action string, u ev.UserSign) { 24 | m := auditM{ 25 | UserID: u.UserID, 26 | Related: related, 27 | RelatedID: id, 28 | Reason: u.Reason, 29 | Action: action, 30 | Created: time.Now(), 31 | } 32 | _, err := deps.Container.Mgo().Collection("audits").InsertOne(context.Background(), &m) 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/exceptions/init.go: -------------------------------------------------------------------------------- 1 | package exceptions 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/getsentry/raven-go" 7 | ) 8 | 9 | type ExceptionsModule struct { 10 | ErrorService *raven.Client `inject:""` 11 | } 12 | 13 | func (di *ExceptionsModule) Recover() { 14 | 15 | var packet *raven.Packet 16 | 17 | switch rval := recover().(type) { 18 | case nil: 19 | return 20 | case error: 21 | packet = raven.NewPacket(rval.Error(), raven.NewException(rval, raven.NewStacktrace(2, 3, nil))) 22 | default: 23 | rvalStr := fmt.Sprint(rval) 24 | packet = raven.NewPacket(rvalStr, raven.NewException(errors.New(rvalStr), raven.NewStacktrace(2, 3, nil))) 25 | } 26 | 27 | // Grab the error and send it to sentry 28 | di.ErrorService.Capture(packet, map[string]string{}) 29 | } 30 | 31 | type NotFound struct { 32 | Msg string 33 | } 34 | 35 | func (e NotFound) Error() string { 36 | return e.Msg 37 | } 38 | 39 | type OutOfBounds struct { 40 | Msg string 41 | } 42 | 43 | func (e OutOfBounds) Error() string { 44 | return e.Msg 45 | } 46 | 47 | type UnexpectedValue struct { 48 | Msg string 49 | } 50 | 51 | func (e UnexpectedValue) Error() string { 52 | return e.Msg 53 | } 54 | -------------------------------------------------------------------------------- /core/common/query.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | 6 | "go.mongodb.org/mongo-driver/bson" 7 | "go.mongodb.org/mongo-driver/bson/primitive" 8 | "go.mongodb.org/mongo-driver/mongo" 9 | ) 10 | 11 | type Query func(col *mongo.Collection, ctx context.Context) (*mongo.Cursor, error) 12 | type Scope func(bson.M) bson.M 13 | 14 | func SoftDelete(query bson.M) bson.M { 15 | query["deleted_at"] = bson.M{"$exists": false} 16 | return query 17 | } 18 | 19 | func FulltextSearch(search string) Scope { 20 | return func(query bson.M) bson.M { 21 | query["$text"] = bson.M{"$search": search} 22 | return query 23 | } 24 | } 25 | 26 | func FieldExists(field string, exists bool) Scope { 27 | return func(query bson.M) bson.M { 28 | query[field] = bson.M{"$exists": exists} 29 | return query 30 | } 31 | } 32 | 33 | func WithinID(list []primitive.ObjectID) Scope { 34 | return func(query bson.M) bson.M { 35 | if len(list) > 0 { 36 | query["_id"] = bson.M{"$in": list} 37 | } 38 | return query 39 | } 40 | } 41 | 42 | func ByScope(scopes ...Scope) bson.M { 43 | query := bson.M{} 44 | 45 | // Apply all scopes to construct query. 46 | for _, s := range scopes { 47 | query = s(query) 48 | } 49 | return query 50 | } 51 | -------------------------------------------------------------------------------- /modules/api/controller/notifications.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gin-gonic/gin" 7 | notify "github.com/tryanzu/core/board/notifications" 8 | "github.com/tryanzu/core/core/user" 9 | "github.com/tryanzu/core/deps" 10 | ) 11 | 12 | // Notifications from current authenticated user. 13 | func Notifications(c *gin.Context) { 14 | var ( 15 | take = 10 16 | skip = 0 17 | ) 18 | 19 | if n, err := strconv.Atoi(c.Query("take")); err == nil && n <= 50 { 20 | take = n 21 | } 22 | 23 | if n, err := strconv.Atoi(c.Query("skip")); err == nil && n > 0 { 24 | skip = n 25 | } 26 | 27 | usr := c.MustGet("user").(user.User) 28 | list, err := notify.FetchBy(deps.Container, notify.UserID(usr.Id, take, skip)) 29 | if err != nil { 30 | _ = c.AbortWithError(500, err) 31 | return 32 | } 33 | 34 | batch, err := list.Humanize(deps.Container) 35 | if err != nil { 36 | _ = c.AbortWithError(500, err) 37 | return 38 | } 39 | 40 | err = user.ResetNotifications(deps.Container, usr.Id) 41 | if err != nil { 42 | _ = c.AbortWithError(500, err) 43 | return 44 | } 45 | 46 | if len(batch) == 0 { 47 | c.JSON(200, make([]string, 0)) 48 | return 49 | } 50 | 51 | c.JSON(200, batch) 52 | } 53 | -------------------------------------------------------------------------------- /roles.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "permissions": ["publish", "comment", "edit-own-posts", "solve-own-posts", "delete-own-posts", "edit-own-comments", "delete-own-comments"], 4 | "parents": [] 5 | }, 6 | 7 | "spartan-girl": { 8 | "permissions": ["block-own-post-comments"], 9 | "parents": ["user"] 10 | }, 11 | 12 | "editor": { 13 | "permissions": [], 14 | "parents": ["user"] 15 | }, 16 | 17 | "child-moderator": { 18 | "permissions": ["block-category-post-comments", "edit-category-comments", "edit-category-posts", "solve-category-posts", "delete-category-posts", "delete-category-comments"], 19 | "parents": ["spartan-girl"] 20 | }, 21 | 22 | "category-moderator": { 23 | "permissions": [], 24 | "parents": ["child-moderator"] 25 | }, 26 | 27 | "super-moderator": { 28 | "permissions": ["block-board-post-comments", "edit-board-comments", "edit-board-posts", "solve-board-posts", "delete-board-comments", "delete-board-posts", "pin-board-posts"], 29 | "parents": ["category-moderator"] 30 | }, 31 | 32 | "administrator": { 33 | "permissions": ["board-config", "sensitive-data", "users:admin"], 34 | "parents": ["super-moderator"] 35 | }, 36 | 37 | "developer": { 38 | "permissions": ["debug", "dev-tools"], 39 | "parents": ["administrator"] 40 | } 41 | } -------------------------------------------------------------------------------- /modules/api/controller/users/patch.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/tryanzu/core/modules/helpers" 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | var PATCHABLE_FIELDS = []string{"onesignal_id"} 10 | 11 | type PatchForm struct { 12 | Value string `form:"value" json:"value" binding:"required"` 13 | } 14 | 15 | func (this API) Patch(c *gin.Context) { 16 | 17 | var form PatchForm 18 | 19 | field := c.Param("field") 20 | id := c.MustGet("user_id") 21 | userId, err := primitive.ObjectIDFromHex(id.(string)) 22 | if err != nil { 23 | c.JSON(400, gin.H{"status": "error", "message": "Invalid user ID."}) 24 | return 25 | } 26 | 27 | if exists, _ := helpers.InArray(field, PATCHABLE_FIELDS); exists && c.Bind(&form) == nil { 28 | usr, err := this.User.Get(userId) 29 | if err != nil { 30 | c.JSON(404, gin.H{"status": "error", "message": "User not found."}) 31 | return 32 | } 33 | 34 | err = usr.Update(map[string]interface{}{field: form.Value}) 35 | if err != nil { 36 | c.JSON(500, gin.H{"status": "error", "message": "Failed to update user."}) 37 | return 38 | } 39 | 40 | c.JSON(200, gin.H{"status": "okay"}) 41 | return 42 | } 43 | 44 | c.JSON(400, gin.H{"status": "error", "message": "Invalid request."}) 45 | } 46 | -------------------------------------------------------------------------------- /board/notifications/database.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/tryanzu/core/deps" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | "go.mongodb.org/mongo-driver/mongo/options" 11 | ) 12 | 13 | func databaseWorker(n int) { 14 | for n := range Database { 15 | n.Id = primitive.NewObjectID() 16 | n.Seen = false 17 | n.Created = time.Now() 18 | n.Updated = time.Now() 19 | 20 | _, err := deps.Container.Mgo().Collection("notifications").InsertOne(context.Background(), n) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | _, err = deps.Container.Mgo().Collection("users").UpdateOne( 26 | context.Background(), 27 | bson.M{"_id": n.UserId}, 28 | bson.M{"$inc": bson.M{"notifications": 1}}, 29 | ) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | var u struct { 35 | Count int `bson:"notifications"` 36 | } 37 | 38 | opts := options.FindOne().SetProjection(bson.M{"notifications": 1}) 39 | err = deps.Container.Mgo().Collection("users").FindOne(context.Background(), bson.M{"_id": n.UserId}, opts).Decode(&u) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | Transmit <- Socket{"user " + n.UserId.Hex(), "notification", map[string]interface{}{ 45 | "fire": "notification", 46 | "count": u.Count, 47 | }} 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /board/notifications/finders.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/tryanzu/core/core/common" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | ) 13 | 14 | var NotificationNotFound = errors.New("Notification has not been found by given criteria.") 15 | 16 | func FindId(deps Deps, id primitive.ObjectID) (notification Notification, err error) { 17 | err = deps.Mgo().Collection("notifications").FindOne(context.Background(), bson.M{"_id": id}).Decode(¬ification) 18 | return 19 | } 20 | 21 | // Fetch multiple leads by conditions 22 | func FetchBy(deps Deps, query common.Query) (list Notifications, err error) { 23 | cursor, err := query(deps.Mgo().Collection("notifications"), context.Background()) 24 | if err != nil { 25 | return 26 | } 27 | defer cursor.Close(context.Background()) 28 | err = cursor.All(context.Background(), &list) 29 | return 30 | } 31 | 32 | func UserID(id primitive.ObjectID, take, skip int) common.Query { 33 | return func(col *mongo.Collection, ctx context.Context) (*mongo.Cursor, error) { 34 | opts := options.Find().SetLimit(int64(take)).SetSkip(int64(skip)).SetSort(bson.M{"updated_at": -1}) 35 | return col.Find(ctx, bson.M{"user_id": id}, opts) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /modules/assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "net/http" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/tryanzu/core/deps" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | ) 13 | 14 | func Boot() *Module { 15 | 16 | module := &Module{} 17 | 18 | return module 19 | } 20 | 21 | type Module struct { 22 | S3 *deps.S3Service `inject:""` 23 | } 24 | 25 | func (module *Module) UploadBase64(content, filename, related string, related_id primitive.ObjectID, meta interface{}) error { 26 | ctx := context.Background() 27 | data, err := base64.StdEncoding.DecodeString(content) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | extension := filepath.Ext(filename) 33 | random := primitive.NewObjectID().Hex() 34 | 35 | // Detect the downloaded file type 36 | dataType := http.DetectContentType(data) 37 | 38 | // S3 path 39 | path := related + "/" + random + extension 40 | 41 | // Upload binary to s3 42 | err = module.S3.PutObject(path, data, dataType) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | database := deps.Container.Mgo() 48 | asset := &Asset{ 49 | Related: related, 50 | RelatedId: related_id, 51 | Path: path, 52 | Meta: meta, 53 | Created: time.Now(), 54 | } 55 | 56 | collection := database.Collection("assets") 57 | _, err = collection.InsertOne(ctx, asset) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /modules/api/controller/posts.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/tryanzu/core/board/comments" 8 | "github.com/tryanzu/core/core/events" 9 | "github.com/tryanzu/core/core/user" 10 | "github.com/tryanzu/core/deps" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | ) 13 | 14 | // UpdatePost pushes a new reply. 15 | func UpdatePost(c *gin.Context) { 16 | var ( 17 | kind = c.DefaultQuery("type", "post") 18 | form struct { 19 | Content string `json:"content" binding:"required"` 20 | } 21 | ) 22 | 23 | cid, err := primitive.ObjectIDFromHex(c.Param("id")) 24 | if err != nil { 25 | _ = c.AbortWithError(500, errors.New("Invalid id for reply")) 26 | return 27 | } 28 | 29 | if err := c.BindJSON(&form); err != nil { 30 | _ = c.AbortWithError(500, errors.New("Invalid kind of reply")) 31 | return 32 | } 33 | 34 | if kind != "post" && kind != "comment" { 35 | _ = c.AbortWithError(500, errors.New("Invalid kind of reply")) 36 | return 37 | } 38 | 39 | usr := c.MustGet("user").(user.User) 40 | comment, err := comments.UpsertComment(deps.Container, comments.Comment{ 41 | UserId: usr.Id, 42 | Content: form.Content, 43 | ReplyType: kind, 44 | ReplyTo: cid, 45 | }) 46 | 47 | if err != nil { 48 | _ = c.AbortWithError(500, errors.New("Invalid kind of reply")) 49 | return 50 | } 51 | 52 | events.In <- events.PostComment(comment.Id) 53 | c.JSON(200, comment) 54 | } 55 | -------------------------------------------------------------------------------- /core/shell/migration.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/abiosoft/ishell" 8 | "github.com/tryanzu/core/board/comments" 9 | "github.com/tryanzu/core/deps" 10 | "go.mongodb.org/mongo-driver/bson" 11 | ) 12 | 13 | func MigrateComments(c *ishell.Context) { 14 | c.ShowPrompt(false) 15 | defer c.ShowPrompt(true) 16 | 17 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 18 | defer cancel() 19 | 20 | db := deps.Container.Mgo() 21 | collection := db.Collection("comments") 22 | 23 | filter := bson.M{"reply_type": bson.M{"$exists": false}} 24 | cursor, err := collection.Find(ctx, filter) 25 | if err != nil { 26 | c.Printf("Error finding comments: %v\n", err) 27 | return 28 | } 29 | defer cursor.Close(ctx) 30 | 31 | var comment comments.Comment 32 | c.ProgressBar().Indeterminate(true) 33 | c.ProgressBar().Start() 34 | 35 | for cursor.Next(ctx) { 36 | err := cursor.Decode(&comment) 37 | if err != nil { 38 | c.Printf("Error decoding comment: %v\n", err) 39 | continue 40 | } 41 | 42 | updateCtx, updateCancel := context.WithTimeout(context.Background(), 5*time.Second) 43 | _, err = collection.UpdateOne(updateCtx, bson.M{"_id": comment.Id}, bson.M{"$set": bson.M{ 44 | "reply_type": "post", 45 | "reply_to": comment.PostId, 46 | }}) 47 | updateCancel() 48 | 49 | if err != nil { 50 | c.Println("Could not migrate comment", err) 51 | } 52 | } 53 | c.ProgressBar().Stop() 54 | } 55 | -------------------------------------------------------------------------------- /board/categories/finders.go: -------------------------------------------------------------------------------- 1 | package categories 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/tryanzu/core/core/config" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/mongo/options" 10 | ) 11 | 12 | var ( 13 | cachedTree Categories 14 | cachedAt *time.Time 15 | ) 16 | 17 | // MakeTree returns categories tree. 18 | func MakeTree(d deps) Categories { 19 | if cachedAt == nil || time.Until(*cachedAt) > time.Minute { 20 | t := time.Now() 21 | cachedTree = makeTree(d) 22 | cachedAt = &t 23 | } 24 | return cachedTree 25 | } 26 | 27 | func makeTree(d deps) (list Categories) { 28 | cnf := config.C.Copy() 29 | ctx := context.TODO() 30 | opt := options.Find().SetSort(bson.M{"order": 1}) 31 | cursor, err := d.Mgo().Collection("categories").Find(ctx, bson.M{}, opt) 32 | if err != nil { 33 | panic(err) 34 | } 35 | defer cursor.Close(ctx) 36 | err = cursor.All(ctx, &list) 37 | if err != nil { 38 | panic(err) 39 | } 40 | parent := list[:0] 41 | child := []Category{} 42 | for _, c := range list { 43 | if !c.Parent.IsZero() { 44 | c.Reactions = cnf.Site.MakeReactions(c.ReactSet) 45 | child = append(child, c) 46 | } else { 47 | parent = append(parent, c) 48 | } 49 | } 50 | for n, p := range parent { 51 | matches := []Category{} 52 | for _, c := range child { 53 | if c.Parent == p.ID { 54 | matches = append(matches, c) 55 | } 56 | } 57 | parent[n].Child = matches 58 | } 59 | list = parent 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /core/user/trust.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/golang-jwt/jwt/v4" 7 | "github.com/tryanzu/core/core/config" 8 | "go.mongodb.org/mongo-driver/bson/primitive" 9 | ) 10 | 11 | type userToken struct { 12 | Address string `json:"address"` 13 | UserID string `json:"user_id"` 14 | Scopes []string `json:"scope"` 15 | jwt.RegisteredClaims 16 | } 17 | 18 | func CanBeTrusted(user User) bool { 19 | return user.Warnings < 6 20 | } 21 | 22 | func IsBanned(d deps, id primitive.ObjectID) bool { 23 | ledis := d.LedisDB() 24 | k := []byte("ban:") 25 | k = append(k, []byte(id.Hex())...) 26 | n, err := ledis.Exists(k) 27 | if err != nil { 28 | panic(err) 29 | } 30 | return n == 1 31 | } 32 | 33 | func genToken(address string, id primitive.ObjectID, roles []UserRole, expiration int) string { 34 | scope := make([]string, len(roles)) 35 | for k, role := range roles { 36 | scope[k] = role.Name 37 | } 38 | if expiration <= 0 { 39 | expiration = 24 40 | } 41 | claims := userToken{ 42 | address, 43 | id.Hex(), 44 | scope, 45 | jwt.RegisteredClaims{ 46 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expiration))), 47 | Issuer: "anzu", 48 | }, 49 | } 50 | 51 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 52 | c := config.C.Copy() 53 | 54 | // Use the secret inside the configuration to encrypt it 55 | tkn, err := token.SignedString([]byte(c.Security.Secret)) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | return tkn 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | pull_request: 7 | branches: [master, develop] 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: ^1.24 21 | 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@v6 24 | with: 25 | version: latest 26 | 27 | build: 28 | name: Build 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Set up Go 1.x 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: ^1.24 35 | id: go 36 | 37 | - name: Check out code into the Go module directory 38 | uses: actions/checkout@v4 39 | 40 | - name: Get dependencies 41 | run: | 42 | go get -v -t -d ./... 43 | if [ -f Gopkg.toml ]; then 44 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 45 | dep ensure 46 | fi 47 | 48 | - name: Build 49 | run: go build -o anzu -v ./cmd/anzu 50 | 51 | - name: Test 52 | run: go test -v ./... 53 | -------------------------------------------------------------------------------- /cmd/anzu/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/subosito/gotenv" 7 | post "github.com/tryanzu/core/board/posts" 8 | "github.com/tryanzu/core/board/search" 9 | "github.com/tryanzu/core/core/config" 10 | "github.com/tryanzu/core/core/content" 11 | "github.com/tryanzu/core/core/events" 12 | "github.com/tryanzu/core/core/mail" 13 | "github.com/tryanzu/core/core/templates" 14 | "github.com/tryanzu/core/deps" 15 | "github.com/tryanzu/core/internal/dal" 16 | "github.com/tryanzu/core/modules/api" 17 | ) 18 | 19 | func init() { 20 | _ = gotenv.Load() 21 | if v, exists := os.LookupEnv("ENV"); exists { 22 | api.ENV = v 23 | deps.ENV = v 24 | } 25 | if v, exists := os.LookupEnv("MONGO_URL"); exists { 26 | deps.MongoURL = v 27 | } 28 | if v, exists := os.LookupEnv("MONGO_NAME"); exists { 29 | deps.MongoName = v 30 | } 31 | if v, exists := os.LookupEnv("REDIS_URL"); exists { 32 | deps.RedisURL = v 33 | } 34 | if v, exists := os.LookupEnv("SENTRY_URL"); exists { 35 | deps.SentryURL = v 36 | } 37 | // Run config service bootstraping sequences. 38 | config.Bootstrap() 39 | events.Boot() 40 | 41 | // Run dependencies bootstraping sequences. 42 | deps.Bootstrap() 43 | if deps.ShouldSeed != nil && *deps.ShouldSeed { 44 | err := dal.Seed(deps.Container.Mgo()) 45 | if err != nil { 46 | deps.Container.Log().Error("db seed failed", err) 47 | } 48 | } 49 | 50 | // Boot internal services. 51 | content.Boot() 52 | post.Boot() 53 | mail.Boot() 54 | templates.Boot() 55 | search.Boot() 56 | } 57 | -------------------------------------------------------------------------------- /internal/dal/seed.go: -------------------------------------------------------------------------------- 1 | package dal 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/tryanzu/core/board/categories" 8 | "github.com/tryanzu/core/modules/user" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | ) 12 | 13 | func Seed(db *mongo.Database) error { 14 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 15 | defer cancel() 16 | 17 | parent := categories.Category{ 18 | ID: primitive.NewObjectID(), 19 | Name: "General", 20 | Description: "All general matters can go here", 21 | Slug: "general", 22 | Order: 0, 23 | Permissions: categories.ACL{ 24 | Read: []string{"*"}, 25 | Write: []string{"*"}, 26 | }, 27 | } 28 | _, err := db.Collection("categories").InsertOne(ctx, parent) 29 | if err != nil { 30 | return err 31 | } 32 | category := categories.Category{ 33 | ID: primitive.NewObjectID(), 34 | Parent: parent.ID, 35 | Name: "General", 36 | Description: "All general matters can go here", 37 | Slug: "general", 38 | Order: 0, 39 | Permissions: categories.ACL{ 40 | Read: []string{"*"}, 41 | Write: []string{"*"}, 42 | }, 43 | } 44 | _, err = db.Collection("categories").InsertOne(ctx, category) 45 | if err != nil { 46 | return err 47 | } 48 | _, err = user.InsertUser(db.Collection("users"), "admin", "admin", "admin@local.domain", user.Validated(true), user.WithRole("administrator")) 49 | if err != nil { 50 | return err 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /modules/mail/model.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "gopkg.in/mgo.v2/bson" 5 | 6 | "io" 7 | core "net/mail" 8 | "time" 9 | ) 10 | 11 | type Mailable interface { 12 | From() *core.Address 13 | To() []*core.Address 14 | SubjectText() string 15 | } 16 | 17 | type MailBase struct { 18 | Subject string 19 | Recipient []MailRecipient 20 | Variables map[string]interface{} 21 | FromName string 22 | FromEmail string 23 | } 24 | 25 | func (mail MailBase) SubjectText() string { 26 | return mail.Subject 27 | } 28 | 29 | func (mail MailBase) From() *core.Address { 30 | return &core.Address{Name: mail.FromName, Address: mail.FromEmail} 31 | } 32 | 33 | func (mail MailBase) To() []*core.Address { 34 | var recipients []*core.Address 35 | for _, r := range mail.Recipient { 36 | recipients = append(recipients, &core.Address{ 37 | Name: r.Name, 38 | Address: r.Email, 39 | }) 40 | } 41 | 42 | return recipients 43 | } 44 | 45 | type Mail struct { 46 | MailBase 47 | Template int 48 | } 49 | 50 | type Raw struct { 51 | MailBase 52 | Content io.Reader 53 | } 54 | 55 | type MailRecipient struct { 56 | Name string 57 | Email string 58 | } 59 | 60 | type ModuleConfig struct { 61 | From string 62 | FromName string 63 | Recipients []string 64 | IgnoredDomains []string 65 | } 66 | 67 | type InboundMail struct { 68 | Id bson.ObjectId `bson:"_id,omitempty" json:"id"` 69 | MessageId string `bson:"messageid" json:"message_id"` 70 | Created time.Time `bson:"created_at" json:"created_at"` 71 | } 72 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Frontend build stage 2 | FROM node:20-alpine AS frontend_build 3 | WORKDIR /tmp/anzu/static/frontend 4 | COPY static/frontend/package.json . 5 | COPY static/frontend/package-lock.json* . 6 | RUN npm ci --legacy-peer-deps 7 | COPY static/frontend/ . 8 | RUN npm run build 9 | 10 | # Backend build stage 11 | FROM golang:1.24-alpine AS build_base 12 | RUN apk add --no-cache git 13 | WORKDIR /tmp/anzu 14 | # We want to populate the module cache based on the go.{mod,sum} files. 15 | COPY go.mod . 16 | COPY go.sum . 17 | 18 | RUN go mod download 19 | 20 | COPY . . 21 | # Copy the built frontend from the previous stage 22 | COPY --from=frontend_build /tmp/anzu/static/frontend/public /tmp/anzu/static/frontend/public 23 | 24 | RUN go build -o ./out/anzu ./cmd/anzu 25 | 26 | # Start fresh from a smaller image 27 | FROM alpine:latest 28 | RUN apk add ca-certificates 29 | 30 | COPY --from=build_base /tmp/anzu/out/anzu /anzu 31 | COPY --from=build_base /tmp/anzu/config.toml.example /config.toml 32 | COPY --from=build_base /tmp/anzu/config.hcl /config.hcl 33 | COPY --from=build_base /tmp/anzu/gaming.json /gaming.json 34 | COPY --from=build_base /tmp/anzu/roles.json /roles.json 35 | COPY --from=build_base /tmp/anzu/static/resources /static/resources 36 | COPY --from=build_base /tmp/anzu/static/templates /static/templates 37 | COPY --from=build_base /tmp/anzu/static/frontend/public /static/frontend/public 38 | 39 | # This container exposes port 8080 to the outside world 40 | EXPOSE 3200 41 | 42 | # Run the binary program produced by `go install` 43 | CMD ["/anzu", "api"] 44 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Use root/example as user/password credentials 2 | version: "3.1" 3 | 4 | services: 5 | anzu: 6 | build: . 7 | ports: 8 | - 3200:3200 9 | environment: 10 | MONGO_URL: mongodb://root:example@mongo:27017/ 11 | MONGO_NAME: anzu 12 | AWS_ACCESS_KEY_ID: minio 13 | AWS_SECRET_ACCESS_KEY: minio123 14 | AWS_S3_ENDPOINT: http://minio:9000 15 | AWS_S3_BUCKET: spartan-board 16 | AWS_S3_REGION: us-west-1 17 | depends_on: 18 | - mongo 19 | - minio 20 | mongo: 21 | image: mongo:8 22 | restart: always 23 | ports: 24 | - 27017:27017 25 | environment: 26 | MONGO_INITDB_ROOT_USERNAME: root 27 | MONGO_INITDB_ROOT_PASSWORD: example 28 | 29 | mongo-express: 30 | image: mongo-express 31 | restart: always 32 | ports: 33 | - 8081:8081 34 | environment: 35 | ME_CONFIG_MONGODB_ADMINUSERNAME: root 36 | ME_CONFIG_MONGODB_ADMINPASSWORD: example 37 | ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/ 38 | ME_CONFIG_BASICAUTH: false 39 | minio: 40 | image: minio/minio 41 | restart: always 42 | ports: 43 | - 9000:9000 44 | environment: 45 | MINIO_ROOT_USER: minio 46 | MINIO_ROOT_PASSWORD: minio123 47 | command: server /data --console-address ":9001" 48 | volumes: 49 | - minio-data:/data 50 | volumes: 51 | minio-data: 52 | -------------------------------------------------------------------------------- /core/templates/init.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "strings" 7 | 8 | "github.com/op/go-logging" 9 | "github.com/tryanzu/core/core/config" 10 | ) 11 | 12 | var log = logging.MustGetLogger("templates") 13 | 14 | var GlobalFuncs = template.FuncMap{ 15 | "trust": func(html string) template.HTML { 16 | return template.HTML(html) 17 | }, 18 | "nl2br": func(html template.HTML) template.HTML { 19 | return template.HTML(strings.Replace(string(html), "\n", "
", -1)) 20 | }, 21 | } 22 | 23 | var Templates *template.Template 24 | 25 | func Boot() { 26 | go func() { 27 | for { 28 | c := config.C.Copy() 29 | Templates = template.Must(template.New("").Funcs(GlobalFuncs).ParseGlob(c.Homedir + "static/templates/**/*.html")) 30 | log.SetBackend(config.LoggingBackend) 31 | log.Info("core/templates is now configured") 32 | 33 | // Wait for config changes... 34 | <-config.C.Reload 35 | } 36 | }() 37 | } 38 | 39 | func Execute(name string, data interface{}) (*bytes.Buffer, error) { 40 | buf := new(bytes.Buffer) 41 | tpl, err := template.New(name).Funcs(GlobalFuncs).ParseFiles(name) 42 | if err != nil { 43 | return nil, err 44 | } 45 | err = tpl.Execute(buf, data) 46 | return buf, err 47 | } 48 | 49 | func ExecuteTemplate(name string, data interface{}) (buf *bytes.Buffer, err error) { 50 | buf = new(bytes.Buffer) 51 | switch v := data.(type) { 52 | case map[string]interface{}: 53 | v["config"] = config.C.Copy() 54 | err = Templates.ExecuteTemplate(buf, name, v) 55 | default: 56 | err = Templates.ExecuteTemplate(buf, name, v) 57 | } 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /board/flags/finders.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | ) 12 | 13 | var FlagNotFound = errors.New("Flag has not been found by given criteria.") 14 | 15 | func FindId(d DepsInterface, id primitive.ObjectID) (f Flag, err error) { 16 | ctx := context.TODO() 17 | err = d.Mgo().Collection("flags").FindOne(ctx, bson.M{"_id": id}).Decode(&f) 18 | if err == mongo.ErrNoDocuments { 19 | err = FlagNotFound 20 | } 21 | return 22 | } 23 | 24 | func FindOne(d DepsInterface, related string, relatedID, userID primitive.ObjectID) (f Flag, err error) { 25 | ctx := context.TODO() 26 | err = d.Mgo().Collection("flags").FindOne(ctx, bson.M{ 27 | "related_to": related, 28 | "related_id": relatedID, 29 | "user_id": userID, 30 | }).Decode(&f) 31 | if err == mongo.ErrNoDocuments { 32 | return f, FlagNotFound 33 | } 34 | 35 | return 36 | } 37 | 38 | func Count(d DepsInterface, q bson.M) int { 39 | ctx := context.TODO() 40 | n, err := d.Mgo().Collection("flags").CountDocuments(ctx, q) 41 | if err != nil { 42 | panic(err) 43 | } 44 | return int(n) 45 | } 46 | 47 | // TodaysCountByUser flags. 48 | func TodaysCountByUser(d DepsInterface, id primitive.ObjectID) int { 49 | today := time.Now() 50 | startOfDay := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, today.Location()) 51 | endOfDay := time.Date(today.Year(), today.Month(), today.Day(), 23, 59, 59, 0, today.Location()) 52 | return Count(d, bson.M{ 53 | "user_id": id, 54 | "created_at": bson.M{"$gte": startOfDay, "$lte": endOfDay}, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /modules/feed/model.go: -------------------------------------------------------------------------------- 1 | package feed 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | type LightPostModel struct { 10 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 11 | Title string `bson:"title" json:"title"` 12 | Slug string `bson:"slug" json:"slug"` 13 | Content string `bson:"content" json:"content"` 14 | Type string `bson:"type" json:"type"` 15 | Category primitive.ObjectID `bson:"category" json:"category"` 16 | UserId primitive.ObjectID `bson:"user_id,omitempty" json:"user_id,omitempty"` 17 | Pinned bool `bson:"pinned,omitempty" json:"pinned,omitempty"` 18 | IsQuestion bool `bson:"is_question,omitempty" json:"is_question"` 19 | Solved bool `bson:"solved,omitempty" json:"solved,omitempty"` 20 | Lock bool `bson:"lock" json:"lock"` 21 | BestAnswer *Comment `bson:"-" json:"best_answer,omitempty"` 22 | Created time.Time `bson:"created_at" json:"created_at"` 23 | Updated time.Time `bson:"updated_at" json:"updated_at"` 24 | } 25 | 26 | type PostCommentModel struct { 27 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 28 | Comment Comment `bson:"comment" json:"comment,omitempty"` 29 | } 30 | 31 | type PostCommentCountModel struct { 32 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 33 | Count int `bson:"count" json:"count"` 34 | } 35 | 36 | type VotesModel struct { 37 | Up int `bson:"up" json:"up"` 38 | Down int `bson:"down" json:"down"` 39 | Rating int `bson:"rating,omitempty" json:"rating,omitempty"` 40 | } 41 | -------------------------------------------------------------------------------- /modules/notifications/mention.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/tryanzu/core/deps" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | ) 11 | 12 | type Mention struct { 13 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 14 | UserId primitive.ObjectID `bson:"user_id" json:"user_id"` // Mentioned 15 | FromId primitive.ObjectID `bson:"from_id" json:"from_id"` 16 | Related string `bson:"related" json:"related"` 17 | RelatedId primitive.ObjectID `bson:"related_id" json:"related_id"` 18 | Created time.Time `bson:"created_at" json:"created_at"` 19 | Updated time.Time `bson:"updated_at" json:"updated_at"` 20 | } 21 | 22 | func (self *NotificationsModule) Mention(parseableMeta map[string]interface{}, user_id, target_user primitive.ObjectID) { 23 | ctx := context.Background() 24 | defer self.Errors.Recover() 25 | 26 | database := deps.Container.Mgo() 27 | usr, err := self.User.Get(user_id) 28 | 29 | if err != nil { 30 | panic(fmt.Sprintf("Could not get user while notifying mention (user_id: %v, target_user: %v). It said: %v", user_id, target_user, err)) 31 | } 32 | 33 | id, exists := parseableMeta["id"].(primitive.ObjectID) 34 | 35 | if !exists { 36 | panic(fmt.Sprintf("ID does not exists in parseable meta (%v)", parseableMeta)) 37 | } 38 | 39 | mention := Mention{ 40 | UserId: target_user, 41 | Related: "comment", 42 | RelatedId: id, 43 | FromId: usr.Data().Id, 44 | Created: time.Now(), 45 | Updated: time.Now(), 46 | } 47 | 48 | collection := database.Collection("mentions") 49 | _, err = collection.InsertOne(ctx, mention) 50 | if err != nil { 51 | panic(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /board/votes/model.go: -------------------------------------------------------------------------------- 1 | package votes 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | "go.mongodb.org/mongo-driver/mongo" 6 | 7 | "time" 8 | ) 9 | 10 | type voteDir int 11 | 12 | const ( 13 | UP voteDir = iota 14 | DOWN 15 | ) 16 | 17 | // Vote represents a reaction to a post || comment 18 | type Vote struct { 19 | ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 20 | UserID primitive.ObjectID `bson:"user_id" json:"user_id"` 21 | Type string `bson:"type" json:"type"` 22 | NestedType string `bson:"nested_type,omitempty" json:"nested_type,omitempty"` 23 | RelatedID primitive.ObjectID `bson:"related_id" json:"related_id"` 24 | Value string `bson:"value" json:"value"` 25 | Created time.Time `bson:"created_at" json:"created_at"` 26 | Deleted *time.Time `bson:"deleted_at,omitempty" json:"-"` 27 | } 28 | 29 | func (v Vote) Remove(deps Deps) error { 30 | return nil 31 | } 32 | 33 | func (v Vote) DbField() string { 34 | return "votes." + v.Value 35 | } 36 | 37 | // Votes represents the aggregated count of votes 38 | type Votes map[string]int 39 | 40 | /*struct { 41 | Up int `bson:"up" json:"up"` 42 | Down int `bson:"down" json:"down"` 43 | Rating int `bson:"rating,omitempty" json:"rating,omitempty"` 44 | }*/ 45 | 46 | func coll(deps Deps) *mongo.Collection { 47 | return deps.Mgo().Collection("votes") 48 | } 49 | 50 | // List aggregates a list of votes for certain event. 51 | type List []Vote 52 | 53 | // ValuesMap is a map of votes with values. 54 | func (ls List) ValuesMap() map[string][]string { 55 | m := make(map[string][]string, len(ls)) 56 | for _, v := range ls { 57 | m[v.RelatedID.Hex()] = append(m[v.RelatedID.Hex()], v.Value) 58 | } 59 | return m 60 | } 61 | -------------------------------------------------------------------------------- /modules/api/controller/posts/post_get.go: -------------------------------------------------------------------------------- 1 | package posts 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/tryanzu/core/core/content" 6 | "github.com/tryanzu/core/core/events" 7 | "github.com/tryanzu/core/deps" 8 | "github.com/tryanzu/core/modules/feed" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | ) 12 | 13 | func (this API) Get(c *gin.Context) { 14 | var ( 15 | kind string 16 | post *feed.Post 17 | err error 18 | ) 19 | 20 | id := c.Params.ByName("id") 21 | if _, err := primitive.ObjectIDFromHex(id); err == nil { 22 | kind = "id" 23 | } 24 | 25 | if legalSlug.MatchString(id) && kind == "" { 26 | kind = "slug" 27 | } 28 | 29 | if kind == "" { 30 | c.JSON(400, gin.H{"message": "Invalid request, id not valid.", "status": "error"}) 31 | return 32 | } 33 | 34 | if kind == "id" { 35 | oid, _ := primitive.ObjectIDFromHex(id) 36 | post, err = this.Feed.Post(oid) 37 | } else { 38 | post, err = this.Feed.Post(bson.M{"slug": id}) 39 | } 40 | if err != nil { 41 | c.JSON(404, gin.H{"message": "Couldnt found post.", "status": "error"}) 42 | return 43 | } 44 | 45 | // Needed data loading to show post 46 | post.LoadUsers() 47 | if sid, exists := c.Get("userID"); exists { 48 | uid := sid.(primitive.ObjectID) 49 | post.LoadVotes(uid) 50 | 51 | // Notify about view. 52 | events.In <- events.PostView(signs(c), post.Id) 53 | } 54 | 55 | _, _ = content.Postprocess(deps.Container, post) 56 | post.LoadUsersHashtables() 57 | data := post.Data() 58 | data.Comments.Total = this.Feed.TrueCommentCount(data.Id) 59 | 60 | c.JSON(200, data) 61 | } 62 | 63 | func signs(c *gin.Context) events.UserSign { 64 | usr := c.MustGet("userID").(primitive.ObjectID) 65 | sign := events.UserSign{ 66 | UserID: usr, 67 | } 68 | if r := c.Query("reason"); len(r) > 0 { 69 | sign.Reason = r 70 | } 71 | return sign 72 | } 73 | -------------------------------------------------------------------------------- /board/categories/model.go: -------------------------------------------------------------------------------- 1 | package categories 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | ) 6 | 7 | // Category model. 8 | type Category struct { 9 | ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 10 | Name string `bson:"name" json:"name"` 11 | Description string `bson:"description" json:"description"` 12 | Slug string `bson:"slug" json:"slug"` 13 | Color string `bson:"color" json:"color"` 14 | Permissions ACL `bson:"permissions" json:"-"` 15 | Parent primitive.ObjectID `bson:"parent,omitempty" json:"parent,omitempty"` 16 | ReactSet []string `bson:"reactSet" json:"-"` 17 | Order int `bson:"order,omitempty" json:"order,omitempty"` 18 | 19 | // Runtime computed properties. 20 | Child Categories `bson:"-" json:"subcategories,omitempty"` 21 | Writable bool `bson:"-" json:"writable"` 22 | Reactions []string `bson:"-" json:"reactions,omitempty"` 23 | } 24 | 25 | // Categories list. 26 | type Categories []Category 27 | 28 | // ACL for categories. 29 | type ACL struct { 30 | Read []string `bson:"read" json:"read"` 31 | Write []string `bson:"write" json:"write"` 32 | } 33 | 34 | // CheckWrite permissions for categories tree. 35 | func (slice Categories) CheckWrite(fn func([]string) bool) Categories { 36 | list := make(Categories, len(slice)) 37 | for n, c := range slice { 38 | list[n] = c 39 | list[n].Writable = fn(c.Permissions.Write) 40 | if len(c.Child) > 0 { 41 | list[n].Child = list[n].Child.CheckWrite(fn) 42 | } 43 | } 44 | return list 45 | } 46 | 47 | func (slice Categories) Len() int { 48 | return len(slice) 49 | } 50 | 51 | func (slice Categories) Less(i, j int) bool { 52 | return slice[i].Order < slice[j].Order 53 | } 54 | 55 | func (slice Categories) Swap(i, j int) { 56 | slice[i], slice[j] = slice[j], slice[i] 57 | } 58 | -------------------------------------------------------------------------------- /.do/app.yaml: -------------------------------------------------------------------------------- 1 | name: anzu 2 | services: 3 | - name: anzu-web 4 | dockerfile_path: Dockerfile 5 | http_port: 3200 6 | instance_count: 1 7 | instance_size_slug: basic-xxs # Smallest size for testing, user can scale up. 8 | routes: 9 | - path: / 10 | envs: 11 | - key: ENV 12 | value: production 13 | - key: MONGO_URL 14 | value: ${db.DATABASE_URL} 15 | - key: MONGO_NAME 16 | value: ${db.NAME} 17 | - key: REDIS_URL 18 | value: ${cache.INTERNAL_URL} 19 | - key: JWT_SECRET 20 | type: SECRET 21 | value: "CHANGEME" # IMPORTANT: Replace with a long, random string 22 | - key: S3_ENDPOINT 23 | value: "CHANGEME" # e.g., nyc3.digitaloceanspaces.com 24 | - key: S3_ACCESS_KEY_ID 25 | type: SECRET 26 | value: "CHANGEME" 27 | - key: S3_SECRET_ACCESS_KEY 28 | type: SECRET 29 | value: "CHANGEME" 30 | - key: S3_BUCKET_NAME 31 | value: "anzu-assets" 32 | - key: S3_REGION 33 | value: "us-east-1" # Or your bucket's region 34 | - key: OAUTH_GOOGLE_CLIENT_ID 35 | type: SECRET 36 | value: "CHANGEME" 37 | - key: OAUTH_GOOGLE_CLIENT_SECRET 38 | type: SECRET 39 | value: "CHANGEME" 40 | - key: OAUTH_FACEBOOK_CLIENT_ID 41 | type: SECRET 42 | value: "CHANGEME" 43 | - key: OAUTH_FACEBOOK_CLIENT_SECRET 44 | type: SECRET 45 | value: "CHANGEME" 46 | - key: SMTP_HOST 47 | value: "CHANGEME" # e.g., smtp.sendgrid.net 48 | - key: SMTP_PORT 49 | value: "587" 50 | - key: SMTP_USERNAME 51 | type: SECRET 52 | value: "CHANGEME" 53 | - key: SMTP_PASSWORD 54 | type: SECRET 55 | value: "CHANGEME" 56 | - key: SMTP_FROM_EMAIL 57 | value: "CHANGEME" # e.g., no-reply@yourdomain.com 58 | databases: 59 | - name: db 60 | engine: MONGO 61 | - name: cache 62 | engine: REDIS 63 | -------------------------------------------------------------------------------- /board/legacy/model/category.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | ) 6 | 7 | type Category struct { 8 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 9 | Name string `bson:"name" json:"name"` 10 | Description string `bson:"description" json:"description"` 11 | Slug string `bson:"slug" json:"slug"` 12 | Color string `bson:"color" json:"color"` 13 | Permissions CategoryAcl `bson:"permissions" json:"permissions"` 14 | Parent primitive.ObjectID `bson:"parent,omitempty" json:"parent,omitempty"` 15 | Order int `bson:"order,omitempty" json:"order,omitempty"` 16 | Count int `bson:"count,omitempty" json:"count,omitempty"` 17 | Recent int `bson:"recent,omitempty" json:"recent,omitempty"` 18 | Child []Category `bson:"subcategories,omitempty" json:"subcategories,omitempty"` 19 | } 20 | 21 | type CategoryAcl struct { 22 | Read []string `bson:"read" json:"read"` 23 | Write []string `bson:"write" json:"write"` 24 | } 25 | 26 | type CategoryCounters struct { 27 | List []CategoryCounter `json:"list"` 28 | } 29 | 30 | type CategoryCounter struct { 31 | Slug string `json:"slug"` 32 | Count int `json:"count"` 33 | } 34 | 35 | type Categories []Category 36 | 37 | func (slice Categories) Len() int { 38 | return len(slice) 39 | } 40 | 41 | func (slice Categories) Less(i, j int) bool { 42 | return slice[i].Count > slice[j].Count 43 | } 44 | 45 | func (slice Categories) Swap(i, j int) { 46 | slice[i], slice[j] = slice[j], slice[i] 47 | } 48 | 49 | type CategoriesOrder []Category 50 | 51 | func (slice CategoriesOrder) Len() int { 52 | return len(slice) 53 | } 54 | 55 | func (slice CategoriesOrder) Less(i, j int) bool { 56 | return slice[i].Order < slice[j].Order 57 | } 58 | 59 | func (slice CategoriesOrder) Swap(i, j int) { 60 | slice[i], slice[j] = slice[j], slice[i] 61 | } 62 | -------------------------------------------------------------------------------- /board/realtime/counters.go: -------------------------------------------------------------------------------- 1 | package realtime 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | func countClientsWorker() { 10 | channels := map[string]map[*Client]struct{}{} 11 | changes := 0 12 | for { 13 | select { 14 | case client := <-counters: 15 | if client.Channels == nil { 16 | for name := range channels { 17 | delete(channels[name], client) 18 | } 19 | continue 20 | } 21 | client.Channels.Range(func(k, v interface{}) bool { 22 | name := k.(string) 23 | if _, exists := channels[name]; !exists { 24 | channels[name] = map[*Client]struct{}{} 25 | } 26 | channels[name][client] = struct{}{} 27 | return true 28 | }) 29 | for name := range channels { 30 | if _, exists := client.Channels.Load(name); !exists { 31 | delete(channels[name], client) 32 | } 33 | } 34 | changes++ 35 | case <-time.After(time.Millisecond * 500): 36 | if changes == 0 { 37 | continue 38 | } 39 | counters := make(map[string]interface{}, len(channels)) 40 | unique := map[primitive.ObjectID]struct{}{} 41 | peers := [][2]string{} 42 | for name, listeners := range channels { 43 | counters[name] = len(listeners) 44 | 45 | // Calculate the list of connected peers on the counters channel 46 | if name != "chat:counters" { 47 | continue 48 | } 49 | for client := range listeners { 50 | if client.User == nil { 51 | continue 52 | } 53 | if _, exists := unique[client.User.Id]; exists { 54 | continue 55 | } 56 | id := client.User.Id.Hex() 57 | name := client.User.UserName 58 | peers = append(peers, [2]string{id, name}) 59 | unique[client.User.Id] = struct{}{} 60 | } 61 | } 62 | 63 | m := M{ 64 | Channel: "chat:counters", 65 | Content: SocketEvent{ 66 | Event: "update", 67 | Params: map[string]interface{}{ 68 | "channels": counters, 69 | "peers": peers, 70 | }, 71 | }.encode(), 72 | } 73 | changes = 0 74 | ToChan <- m 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /modules/api/controller/users/user_recover_password.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/gin-gonic/gin/binding" 6 | "github.com/tryanzu/core/board/legacy/model" 7 | "github.com/tryanzu/core/core/user" 8 | "github.com/tryanzu/core/deps" 9 | "github.com/tryanzu/core/modules/helpers" 10 | ) 11 | 12 | func (this API) RequestPasswordRecovery(c *gin.Context) { 13 | if !helpers.IsEmail(c.Query("email")) { 14 | c.JSON(400, gin.H{"status": "error", "message": "Invalid request, need valid email."}) 15 | return 16 | } 17 | 18 | usr, err := user.FindEmail(deps.Container, c.Query("email")) 19 | if err != nil { 20 | c.JSON(400, gin.H{"status": "error", "message": err.Error()}) 21 | return 22 | } 23 | 24 | err = usr.RecoveryPasswordEmail(deps.Container) 25 | if err != nil { 26 | c.JSON(400, gin.H{"status": "error", "message": err.Error()}) 27 | return 28 | } 29 | 30 | c.JSON(200, gin.H{"status": "okay"}) 31 | } 32 | 33 | func (this API) UpdatePasswordFromToken(c *gin.Context) { 34 | token := c.Param("token") 35 | 36 | if len(token) < 4 { 37 | c.JSON(400, gin.H{"status": "error", "message": "Invalid request, need valid token."}) 38 | return 39 | } 40 | 41 | valid, err := this.User.IsValidRecoveryToken(token) 42 | 43 | if err != nil { 44 | c.JSON(400, gin.H{"status": "error", "message": err.Error()}) 45 | return 46 | } 47 | 48 | if !valid { 49 | c.JSON(400, gin.H{"status": "error", "message": "Invalid request, need valid token."}) 50 | return 51 | } 52 | 53 | var form model.UserProfileForm 54 | 55 | if c.BindWith(&form, binding.JSON) == nil { 56 | 57 | if form.Password != "" { 58 | 59 | usr, err := this.User.GetUserFromRecoveryToken(token) 60 | 61 | if err != nil { 62 | c.JSON(400, gin.H{"status": "error", "message": err.Error()}) 63 | return 64 | } 65 | 66 | _ = usr.Update(map[string]interface{}{"password": form.Password}) 67 | 68 | c.JSON(200, gin.H{"status": "okay"}) 69 | return 70 | } 71 | } 72 | 73 | c.JSON(400, gin.H{"status": "error", "message": "Invalid request."}) 74 | } 75 | -------------------------------------------------------------------------------- /core/content/processor.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/op/go-logging" 7 | ) 8 | 9 | var log = logging.MustGetLogger("content") 10 | 11 | // Content processor definition. 12 | type Processor func(DepsInterface, Parseable, tags) (Parseable, error) 13 | type Preprocessor func(DepsInterface, Parseable) (Parseable, error) 14 | 15 | // Postprocess a parseable type. 16 | func Postprocess(d DepsInterface, c Parseable) (processed Parseable, err error) { 17 | starts := time.Now() 18 | list := parseTags(c) 19 | pipeline := []Processor{ 20 | postReplaceMentionTags, 21 | postReplaceAssetTags, 22 | } 23 | 24 | // Run pipeline over parseable. 25 | processed = c 26 | for _, fn := range pipeline { 27 | processed, err = fn(d, processed, list) 28 | 29 | if err != nil { 30 | return 31 | } 32 | } 33 | 34 | elapsed := time.Since(starts) 35 | log.Debugf("postprocess content took=%v", elapsed) 36 | return 37 | } 38 | 39 | // Preprocess a parseable type. 40 | func Preprocess(d DepsInterface, c Parseable) (processed Parseable, err error) { 41 | starts := time.Now() 42 | pipeline := []Preprocessor{ 43 | preReplaceMentionTags, 44 | preReplaceAssetTags, 45 | } 46 | 47 | // Run pipeline over parseable. 48 | processed = c 49 | for _, fn := range pipeline { 50 | processed, err = fn(d, processed) 51 | if err != nil { 52 | return 53 | } 54 | } 55 | 56 | elapsed := time.Since(starts) 57 | log.Debugf("preprocess took = %v", elapsed) 58 | return 59 | } 60 | 61 | func parseTags(c Parseable) (list []tag) { 62 | 63 | // Use regex to find all tags inside the parseable content. 64 | found := tagRegex.FindAllString(c.GetContent(), -1) 65 | for _, match := range found { 66 | // Having parsed all tags now destructure the tag params. 67 | params := tagParamsRegex.FindAllString(match, -1) 68 | count := len(params) - 1 69 | 70 | for n, param := range params { 71 | if n != count { 72 | params[n] = param[:len(param)-1] 73 | } 74 | } 75 | 76 | if len(params) > 0 { 77 | list = append(list, tag{match, params[0], params[1:]}) 78 | } 79 | } 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /modules/helpers/crypto.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | // Adapted from https://elithrar.github.io/article/generating-secure-random-numbers-crypto-rand/ 11 | 12 | func init() { 13 | assertAvailablePRNG() 14 | } 15 | 16 | func assertAvailablePRNG() { 17 | // Assert that a cryptographically secure PRNG is available. 18 | // Panic otherwise. 19 | buf := make([]byte, 1) 20 | 21 | _, err := io.ReadFull(rand.Reader, buf) 22 | if err != nil { 23 | panic(fmt.Sprintf("crypto/rand is unavailable: Read() failed with %#v", err)) 24 | } 25 | } 26 | 27 | // GenerateRandomBytes returns securely generated random bytes. 28 | // It will return an error if the system's secure random 29 | // number generator fails to function correctly, in which 30 | // case the caller should not continue. 31 | func GenerateRandomBytes(n int) ([]byte, error) { 32 | b := make([]byte, n) 33 | _, err := rand.Read(b) 34 | // Note that err == nil only if we read len(b) bytes. 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return b, nil 40 | } 41 | 42 | // GenerateRandomString returns a securely generated random string. 43 | // It will return an error if the system's secure random 44 | // number generator fails to function correctly, in which 45 | // case the caller should not continue. 46 | func GenerateRandomString(n int) (string, error) { 47 | const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" 48 | bytes, err := GenerateRandomBytes(n) 49 | if err != nil { 50 | return "", err 51 | } 52 | for i, b := range bytes { 53 | bytes[i] = letters[b%byte(len(letters))] 54 | } 55 | return string(bytes), nil 56 | } 57 | 58 | // GenerateRandomStringURLSafe returns a URL-safe, base64 encoded 59 | // securely generated random string. 60 | // It will return an error if the system's secure random 61 | // number generator fails to function correctly, in which 62 | // case the caller should not continue. 63 | func GenerateRandomStringURLSafe(n int) (string, error) { 64 | b, err := GenerateRandomBytes(n) 65 | return base64.URLEncoding.EncodeToString(b), err 66 | } 67 | -------------------------------------------------------------------------------- /modules/gaming/shop.go: -------------------------------------------------------------------------------- 1 | package gaming 2 | 3 | import ( 4 | "context" 5 | "github.com/tryanzu/core/deps" 6 | "github.com/tryanzu/core/modules/exceptions" 7 | "github.com/tryanzu/core/modules/user" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | "time" 11 | ) 12 | 13 | func (self *User) AcquireBadge(id primitive.ObjectID, validation bool) error { 14 | 15 | var badge BadgeModel 16 | 17 | ctx := context.Background() 18 | database := deps.Container.Mgo() 19 | usr := self.user.Data() 20 | 21 | // Find the badge using it's id 22 | err := database.Collection("badges").FindOne(ctx, bson.M{"_id": id}).Decode(&badge) 23 | 24 | if err != nil { 25 | return exceptions.NotFound{Msg: "Invalid badge id, not found."} 26 | } 27 | 28 | if validation { 29 | 30 | if badge.Type != "clothes" && badge.Type != "weapon" && badge.Type != "power" && badge.Type != "armour" { 31 | 32 | return exceptions.UnexpectedValue{Msg: "Not a valid type of badge to get acquired."} 33 | } 34 | 35 | if badge.Coins > 0 && usr.Gaming.Coins < badge.Coins { 36 | 37 | return exceptions.OutOfBounds{Msg: "Not enough coins to buy item."} 38 | } 39 | 40 | if badge.RequiredLevel > 0 && usr.Gaming.Level < badge.RequiredLevel { 41 | 42 | return exceptions.OutOfBounds{Msg: "Not enough level."} 43 | } 44 | 45 | if !badge.RequiredBadge.IsZero() { 46 | 47 | var user_valid bool = false 48 | 49 | user_badges := usr.Gaming.Badges 50 | 51 | for _, user_badge := range user_badges { 52 | 53 | if user_badge.Id == badge.RequiredBadge { 54 | 55 | user_valid = true 56 | } 57 | } 58 | 59 | if !user_valid { 60 | 61 | return exceptions.OutOfBounds{Msg: "Don't have required badge."} 62 | } 63 | } 64 | } 65 | 66 | badge_push := user.UserBadge{ 67 | Id: id, 68 | Date: time.Now(), 69 | } 70 | 71 | _, err = database.Collection("users").UpdateOne(ctx, bson.M{"_id": usr.Id}, bson.M{"$push": bson.M{"gaming.badges": badge_push}}) 72 | 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | if badge.Coins > 0 { 78 | 79 | // Pay the badge price using user coins 80 | go self.Coins(-badge.Coins) 81 | } 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /modules/api/controller/pages.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | posts "github.com/tryanzu/core/board/posts" 6 | "github.com/tryanzu/core/core/config" 7 | "github.com/tryanzu/core/core/user" 8 | "github.com/tryanzu/core/deps" 9 | "github.com/tryanzu/core/modules/helpers" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | ) 12 | 13 | // HomePage is the site's entry point. 14 | func HomePage(c *gin.Context) { 15 | conf := c.MustGet("config").(config.Anzu) 16 | title := conf.Site.Name 17 | if len(conf.Site.TitleMotto) > 0 { 18 | title += " - " + conf.Site.TitleMotto 19 | } 20 | c.HTML(200, "pages/index.tmpl", gin.H{ 21 | "config": conf, 22 | "title": title, 23 | "description": c.MustGet("siteDescription").(string), 24 | "image": c.MustGet("siteUrl").(string) + "/images/default-post.jpg", 25 | "jwt": c.GetString("jwt"), 26 | }) 27 | } 28 | 29 | func UserPage(c *gin.Context) { 30 | id, _ := primitive.ObjectIDFromHex(c.Param("id")) 31 | usr, err := user.FindId(deps.Container, id) 32 | 33 | if err != nil { 34 | c.AbortWithStatus(404) 35 | return 36 | } 37 | 38 | c.HTML(200, "pages/index.tmpl", gin.H{ 39 | "config": c.MustGet("config").(config.Anzu), 40 | "title": usr.UserName + " - Perfil de usuario", 41 | "description": "Explora las aportaciones y el perfil de " + usr.UserName + " en Buldar", 42 | "image": c.MustGet("siteUrl").(string) + "/images/default-post.jpg", 43 | "jwt": c.GetString("jwt"), 44 | }) 45 | } 46 | 47 | func PostPage(c *gin.Context) { 48 | id, _ := primitive.ObjectIDFromHex(c.Param("id")) 49 | post, err := posts.FindId(deps.Container, id) 50 | 51 | if err != nil { 52 | c.AbortWithStatus(404) 53 | return 54 | } 55 | 56 | if post.Slug != c.Param("slug") { 57 | c.Redirect(301, c.MustGet("siteUrl").(string)+"/p/"+post.Slug+"/"+post.Id.Hex()) 58 | return 59 | } 60 | 61 | c.HTML(200, "pages/index.tmpl", gin.H{ 62 | "config": c.MustGet("config").(config.Anzu), 63 | "title": post.Title, 64 | "description": helpers.Truncate(post.Content, 160), 65 | "image": c.MustGet("siteUrl").(string) + "/images/default-post.jpg", 66 | "jwt": c.GetString("jwt"), 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /board/assets/finders.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/tryanzu/core/core/common" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | ) 12 | 13 | func FindList(d Deps, scopes ...common.Scope) (list Assets, err error) { 14 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 15 | defer cancel() 16 | 17 | cursor, err := d.Mgo().Collection("remote_assets").Find(ctx, common.ByScope(scopes...)) 18 | if err != nil { 19 | return 20 | } 21 | defer cursor.Close(ctx) 22 | 23 | err = cursor.All(ctx, &list) 24 | return 25 | } 26 | 27 | func FindHash(d Deps, hash string) (asset Asset, err error) { 28 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 29 | defer cancel() 30 | 31 | err = d.Mgo().Collection("remote_assets").FindOne(ctx, bson.M{ 32 | "hash": hash, 33 | }).Decode(&asset) 34 | if err == mongo.ErrNoDocuments { 35 | err = nil // Convert to match original behavior 36 | } 37 | return 38 | } 39 | 40 | func FindURLs(d Deps, list ...primitive.ObjectID) (common.AssetRefsMap, error) { 41 | hash := common.AssetRefsMap{} 42 | missing := []primitive.ObjectID{} 43 | 44 | // Attempt to fill hashmap using cache layer first. 45 | for _, id := range list { 46 | var ref common.AssetRef 47 | url, err := d.LedisDB().Get([]byte("asset:" + id.Hex() + ":url")) 48 | if err != nil || len(url) == 0 { 49 | // Append to list of missing keys 50 | missing = append(missing, id) 51 | continue 52 | } 53 | ref.URL = string(url) 54 | // uo = use original 55 | uo, err := d.LedisDB().Exists([]byte("asset:" + id.Hex() + ":uo")) 56 | ref.UseOriginal = err == nil && uo > 0 57 | hash[id] = ref 58 | } 59 | 60 | if len(missing) == 0 { 61 | return hash, nil 62 | } 63 | 64 | assets, err := FindList(d, common.WithinID(missing)) 65 | if err != nil { 66 | return hash, err 67 | } 68 | 69 | err = assets.UpdateCache(d) 70 | if err != nil { 71 | return hash, err 72 | } 73 | 74 | for _, u := range assets { 75 | hash[u.ID] = common.AssetRef{ 76 | URL: u.URL(), 77 | UseOriginal: u.Status == "remote", 78 | } 79 | } 80 | 81 | return hash, nil 82 | } 83 | -------------------------------------------------------------------------------- /modules/api/controller/reactions.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/tryanzu/core/board/comments" 6 | post "github.com/tryanzu/core/board/posts" 7 | "github.com/tryanzu/core/board/votes" 8 | "github.com/tryanzu/core/core/events" 9 | "github.com/tryanzu/core/core/user" 10 | "github.com/tryanzu/core/deps" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | 13 | "net/http" 14 | ) 15 | 16 | type upsertReactionBody struct { 17 | Type string `json:"type" binding:"required"` 18 | } 19 | 20 | // UpsertReaction realted to a reactable. 21 | func UpsertReaction(c *gin.Context) { 22 | var ( 23 | id primitive.ObjectID 24 | body upsertReactionBody 25 | votable votes.Votable 26 | err error 27 | ) 28 | 29 | usr := c.MustGet("user").(user.User) 30 | if usr.Gaming.Swords < 15 { 31 | jsonErr(c, http.StatusPreconditionFailed, "Not enough user reputation.") 32 | return 33 | } 34 | 35 | // ID validation. 36 | if id, err = primitive.ObjectIDFromHex(c.Params.ByName("id")); err != nil { 37 | jsonErr(c, http.StatusBadRequest, "malformed request, invalid id") 38 | return 39 | } 40 | 41 | // Bind body data. 42 | if err = c.Bind(&body); err != nil { 43 | c.JSON(http.StatusBadRequest, gin.H{"status": "error", "reason": "Invalid request."}) 44 | return 45 | } 46 | 47 | switch c.Params.ByName("type") { 48 | case "post": 49 | if postData, err := post.FindId(deps.Container, id); err == nil { 50 | votable = postData 51 | } 52 | case "comment": 53 | if comment, err := comments.FindId(deps.Container, id); err == nil { 54 | votable = comment 55 | } 56 | default: 57 | jsonErr(c, http.StatusBadRequest, "invalid type") 58 | return 59 | } 60 | 61 | if votable == nil { 62 | jsonErr(c, http.StatusNotFound, "invalid id") 63 | return 64 | } 65 | 66 | vote, status, err := votes.UpsertVote(deps.Container, votable, usr.Id, body.Type) 67 | if err != nil { 68 | c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": err.Error()}) 69 | return 70 | } 71 | 72 | // Events pool signal 73 | events.In <- events.Vote(vote) 74 | 75 | if vote.Deleted != nil { 76 | c.JSON(http.StatusOK, status) 77 | return 78 | } 79 | 80 | c.JSON(http.StatusOK, status) 81 | } 82 | -------------------------------------------------------------------------------- /board/events/posts.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/tryanzu/core/board/comments" 5 | notify "github.com/tryanzu/core/board/notifications" 6 | posts "github.com/tryanzu/core/board/posts" 7 | ev "github.com/tryanzu/core/core/events" 8 | "github.com/tryanzu/core/deps" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | ) 11 | 12 | // Bind event handlers for posts related actions... 13 | func postsEvents() { 14 | ev.On <- ev.EventHandler{ 15 | On: ev.POSTS_NEW, 16 | Handler: func(e ev.Event) error { 17 | post, err := posts.FindId(deps.Container, e.Params["id"].(primitive.ObjectID)) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | notify.Transmit <- notify.Socket{ 23 | Chan: "feed", 24 | Action: "action", 25 | Params: map[string]interface{}{ 26 | "fire": "new-post", 27 | "category": post.Category.Hex(), 28 | "user_id": post.UserId.Hex(), 29 | "id": post.Id.Hex(), 30 | "slug": post.Slug, 31 | }, 32 | } 33 | 34 | return nil 35 | }, 36 | } 37 | 38 | ev.On <- ev.EventHandler{ 39 | On: ev.POST_VIEW, 40 | Handler: func(e ev.Event) error { 41 | post, err := posts.FindId(deps.Container, e.Params["id"].(primitive.ObjectID)) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | err = posts.TrackView(deps.Container, post.Id, e.Sign.UserID) 47 | return err 48 | }, 49 | } 50 | 51 | ev.On <- ev.EventHandler{ 52 | On: ev.POST_DELETED, 53 | Handler: func(e ev.Event) error { 54 | pid := e.Params["id"].(primitive.ObjectID) 55 | 56 | // Notify transmitter 57 | notify.Transmit <- notify.Socket{ 58 | Chan: "feed", 59 | Action: "action", 60 | Params: map[string]interface{}{ 61 | "fire": "delete-post", 62 | "id": pid.Hex(), 63 | }, 64 | } 65 | 66 | if e.Sign != nil { 67 | audit("post", pid, "delete", *e.Sign) 68 | } 69 | 70 | err := comments.DeletePostComments(deps.Container, pid) 71 | return err 72 | }, 73 | } 74 | 75 | ev.On <- ev.EventHandler{ 76 | On: ev.POSTS_REACHED, 77 | Handler: func(e ev.Event) error { 78 | list := e.Params["list"].([]primitive.ObjectID) 79 | err := posts.TrackReachedList(deps.Container, list, e.Sign.UserID) 80 | return err 81 | }, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /core/mail/init.go: -------------------------------------------------------------------------------- 1 | package mail 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/op/go-logging" 7 | "github.com/tryanzu/core/core/config" 8 | gomail "gopkg.in/gomail.v2" 9 | ) 10 | 11 | var ( 12 | log = logging.MustGetLogger("mailer") 13 | 14 | // In channel will receive the messages to be sent. 15 | In chan *gomail.Message 16 | ) 17 | 18 | // Boot send worker with automatic runtime config. 19 | func Boot() { 20 | go func() { 21 | for { 22 | log.SetBackend(config.LoggingBackend) 23 | 24 | // New incoming messages chan && sendingWorkers 25 | In = make(chan *gomail.Message, 4) 26 | 27 | // Spawn a daemon that will consume incoming messages to be sent. 28 | // If not properly configured it will start ignoring signals. 29 | go sendWorker(config.C) 30 | 31 | <-config.C.Reload 32 | 33 | // When receiving the config signal 34 | // the In chan must be closed so active sendWorker 35 | // can finish. 36 | close(In) 37 | } 38 | }() 39 | } 40 | 41 | func sendWorker(c *config.Config) { 42 | var ( 43 | sender gomail.SendCloser 44 | err error 45 | open = false 46 | ) 47 | 48 | mail := c.Copy().Mail 49 | if len(mail.Server) > 0 { 50 | log.Info("send worker has started...", mail) 51 | dialer := gomail.NewDialer(mail.Server, 587, mail.User, mail.Password) 52 | for { 53 | select { 54 | case m, alive := <-In: 55 | if !alive { 56 | log.Info("Mail send worker has stopped...") 57 | return 58 | } 59 | if !open { 60 | if sender, err = dialer.Dial(); err != nil { 61 | log.Error(err) 62 | continue 63 | } 64 | open = true 65 | } 66 | if err := gomail.Send(sender, m); err != nil { 67 | log.Error(err) 68 | open = false 69 | } 70 | case <-time.After(30 * time.Second): 71 | // Close the connection to the SMTP server if no email was sent in 72 | // the last 30 seconds. 73 | if open { 74 | if err := sender.Close(); err != nil { 75 | log.Error(err) 76 | break 77 | } 78 | open = false 79 | } 80 | } 81 | } 82 | } 83 | 84 | log.Warning("mail settings are not configured, discarding emails...") 85 | for { 86 | m, alive := <-In 87 | if !alive { 88 | log.Info("worker has stopped...") 89 | return 90 | } 91 | 92 | log.Debug(m) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /core/content/assets.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/tryanzu/core/board/assets" 8 | "github.com/tryanzu/core/core/common" 9 | "go.mongodb.org/mongo-driver/bson/primitive" 10 | ) 11 | 12 | var ( 13 | assetURL, _ = regexp.Compile(`(?m)^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+`) 14 | ) 15 | 16 | func preReplaceAssetTags(d DepsInterface, c Parseable) (processed Parseable, err error) { 17 | processed = c 18 | content := processed.GetContent() 19 | list := assetURL.FindAllString(content, -1) 20 | if len(list) == 0 { 21 | return 22 | } 23 | tags := assets.Assets{} 24 | for _, url := range list { 25 | var asset assets.Asset 26 | asset, err = assets.FromURL(d, url) 27 | if err != nil { 28 | return 29 | } 30 | content = asset.Replace(content) 31 | tags = append(tags, asset) 32 | } 33 | 34 | // Attempt to host remote assets using S3 in another process 35 | go tags.HostRemotes(d, "post") 36 | processed = processed.UpdateContent(content) 37 | return 38 | } 39 | 40 | func postReplaceAssetTags(d DepsInterface, c Parseable, list tags) (processed Parseable, err error) { 41 | processed = c 42 | if len(list) == 0 { 43 | return 44 | } 45 | 46 | assetList := list.withTag("asset") 47 | ids := assetList.getIdParams(0) 48 | if len(ids) == 0 { 49 | return 50 | } 51 | var urls common.AssetRefsMap 52 | urls, err = assets.FindURLs(d, ids...) 53 | if err != nil { 54 | return 55 | } 56 | 57 | content := processed.GetContent() 58 | for _, tag := range assetList { 59 | if id := tag.Params[0]; func() bool { _, err := primitive.ObjectIDFromHex(id); return err == nil }() { 60 | objID, _ := primitive.ObjectIDFromHex(id) 61 | ref, exists := urls[objID] 62 | if !exists { 63 | continue 64 | } 65 | content = strings.Replace(content, tag.Original, ref.URL, -1) 66 | } 67 | } 68 | 69 | processed = processed.UpdateContent(content) 70 | return 71 | } 72 | 73 | /* 74 | 75 | // Mention ref. 76 | type Mention struct { 77 | UserID primitive.ObjectID 78 | Username string 79 | Comment string 80 | Original string 81 | } 82 | 83 | func (m Mention) Replace(content string) string { 84 | tag := "[mention:" + m.UserID.Hex() 85 | if m.Comment != "" { 86 | tag = tag + ":" + m.Comment 87 | } 88 | tag = tag + "]" 89 | 90 | return strings.Replace(content, m.Original, tag, -1) 91 | } 92 | */ 93 | -------------------------------------------------------------------------------- /modules/acl/init.go: -------------------------------------------------------------------------------- 1 | package acl 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | "time" 8 | 9 | "github.com/mikespook/gorbac" 10 | "github.com/tryanzu/core/board/legacy/model" 11 | "github.com/tryanzu/core/deps" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/bson/primitive" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | ) 16 | 17 | var LoadedACL *Module 18 | 19 | type Module struct { 20 | Map *gorbac.RBAC 21 | Rules map[string]AclRole 22 | Permissions map[string]gorbac.Permission 23 | } 24 | 25 | func (module *Module) User(id primitive.ObjectID) *User { 26 | 27 | var usr model.User 28 | database := deps.Container.Mgo() 29 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 30 | defer cancel() 31 | 32 | // Get the user using it's id 33 | err := database.Collection("users").FindOne(ctx, bson.M{"_id": id}).Decode(&usr) 34 | 35 | if err != nil { 36 | if err == mongo.ErrNoDocuments { 37 | return nil 38 | } 39 | panic(err) 40 | } 41 | 42 | user := &User{data: usr, acl: module} 43 | 44 | return user 45 | } 46 | 47 | func (refs *Module) CheckPermissions(roles []string, permission string) bool { 48 | for _, role := range roles { 49 | p, exists := refs.Permissions[permission] 50 | if exists { 51 | if refs.Map.IsGranted(role, p, nil) { 52 | // User's role is granted to do "permission" 53 | return true 54 | } 55 | } 56 | } 57 | return false 58 | } 59 | 60 | func Boot(file string) *Module { 61 | module := &Module{} 62 | rules, err := os.ReadFile(file) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | // Unmarshal file with gaming rules 68 | if err := json.Unmarshal(rules, &module.Rules); err != nil { 69 | panic(err) 70 | } 71 | 72 | module.Map = gorbac.New() 73 | module.Permissions = make(map[string]gorbac.Permission) 74 | 75 | for name, rules := range module.Rules { 76 | 77 | role := gorbac.NewStdRole(name) 78 | 79 | for _, p := range rules.Permissions { 80 | module.Permissions[p] = gorbac.NewStdPermission(p) 81 | _ = role.Assign(module.Permissions[p]) 82 | } 83 | 84 | // Populate map with permissions 85 | _ = module.Map.Add(role) 86 | } 87 | 88 | for name, rules := range module.Rules { 89 | if len(rules.Inherits) > 0 { 90 | _ = module.Map.SetParents(name, rules.Inherits) 91 | } 92 | } 93 | 94 | LoadedACL = module 95 | return module 96 | } 97 | -------------------------------------------------------------------------------- /board/notifications/email.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/matcornic/hermes/v2" 7 | "github.com/tryanzu/core/board/comments" 8 | post "github.com/tryanzu/core/board/posts" 9 | "github.com/tryanzu/core/core/config" 10 | "github.com/tryanzu/core/core/user" 11 | "gopkg.in/gomail.v2" 12 | ) 13 | 14 | // SomeoneCommentedYourPostEmail notification constructor. 15 | func SomeoneCommentedYourPostEmail(p post.Post, usr user.User) (*gomail.Message, error) { 16 | c := config.C.Copy() 17 | m := gomail.NewMessage() 18 | from := c.Mail.From 19 | if len(from) == 0 { 20 | from = "no-reply@tryanzu.com" 21 | } 22 | h := config.C.Hermes() 23 | body, err := h.GenerateHTML(post.SomeoneCommentedYourPost(usr.UserName, p)) 24 | if err != nil { 25 | return nil, err 26 | } 27 | m.SetHeader("From", from) 28 | m.SetHeader("Reply-To", from) 29 | m.SetHeader("To", usr.Email) 30 | m.SetHeader("Subject", "Alguien respondió tu publicación: "+p.Title) 31 | m.SetBody("text/html", body) 32 | return m, nil 33 | } 34 | 35 | // SomeoneCommentedYourCommentEmail notification constructor. 36 | func SomeoneCommentedYourCommentEmail(p post.Post, comment comments.Comment, usr user.User) (*gomail.Message, error) { 37 | c := config.C.Copy() 38 | link := c.Site.MakeURL("p/" + p.Slug + "/" + p.Id.Hex()) 39 | email := hermes.Email{ 40 | Body: hermes.Body{ 41 | Name: usr.UserName, 42 | Intros: []string{ 43 | fmt.Sprintf("Tu comentario en %s (Publicación %s) recibió una respuesta mientras no estabas.", c.Site.Name, p.Title), 44 | }, 45 | Actions: []hermes.Action{ 46 | { 47 | Button: hermes.Button{ 48 | Color: "#3D5AFE", 49 | Text: "Ver comentario", 50 | Link: link, 51 | }, 52 | }, 53 | }, 54 | Outros: []string{ 55 | "Si deseas dejar de recibir notificaciones puedes entrar en tu cuenta y cambiar la configuración de avisos.", 56 | }, 57 | Signature: "Un saludo", 58 | }, 59 | } 60 | m := gomail.NewMessage() 61 | from := c.Mail.From 62 | if len(from) == 0 { 63 | from = "no-reply@tryanzu.com" 64 | } 65 | h := config.C.Hermes() 66 | body, err := h.GenerateHTML(email) 67 | if err != nil { 68 | return nil, err 69 | } 70 | m.SetHeader("From", from) 71 | m.SetHeader("Reply-To", from) 72 | m.SetHeader("To", usr.Email) 73 | m.SetHeader("Subject", "Alguien respondió tu comentario en: "+p.Title) 74 | m.SetBody("text/html", body) 75 | return m, nil 76 | } 77 | -------------------------------------------------------------------------------- /deps/mongo.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "slices" 7 | "time" 8 | 9 | "go.mongodb.org/mongo-driver/mongo" 10 | "go.mongodb.org/mongo-driver/mongo/options" 11 | ) 12 | 13 | var ( 14 | // MongoURL config uri 15 | MongoURL string 16 | // MongoName db name 17 | MongoName string 18 | 19 | ShouldSeed = flag.Bool("should-seed", false, "determines whether we seed the initial categories and admin user to bootstrap the site") 20 | ) 21 | 22 | func IgniteMongoDB(container Deps) (Deps, error) { 23 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 24 | defer cancel() 25 | 26 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(MongoURL)) 27 | if err != nil { 28 | log.Error(err) 29 | log.Info(MongoURL) 30 | return container, err 31 | } 32 | 33 | // Test connection 34 | if err := client.Ping(ctx, nil); err != nil { 35 | log.Error(err) 36 | return container, err 37 | } 38 | 39 | db := client.Database(MongoName) 40 | collections, err := db.ListCollectionNames(ctx, map[string]any{}) 41 | if err != nil { 42 | return container, err 43 | } 44 | seed := !slices.Contains(collections, "users") 45 | if seed { 46 | ShouldSeed = &seed 47 | } 48 | 49 | // Ensure indexes 50 | usersCol := db.Collection("users") 51 | 52 | // Email index 53 | emailIndexModel := mongo.IndexModel{ 54 | Keys: map[string]any{"email": 1}, 55 | Options: options.Index().SetUnique(true), 56 | } 57 | _, err = usersCol.Indexes().CreateOne(ctx, emailIndexModel) 58 | if err != nil { 59 | log.Error("Failed to create email index:", err) 60 | } 61 | 62 | // Username index 63 | usernameIndexModel := mongo.IndexModel{ 64 | Keys: map[string]any{"username": 1}, 65 | Options: options.Index().SetUnique(true), 66 | } 67 | _, err = usersCol.Indexes().CreateOne(ctx, usernameIndexModel) 68 | if err != nil { 69 | log.Error("Failed to create username index:", err) 70 | } 71 | 72 | // Text search index for posts 73 | postsCol := db.Collection("posts") 74 | searchIndexModel := mongo.IndexModel{ 75 | Keys: map[string]any{ 76 | "title": "text", 77 | "content": "text", 78 | }, 79 | } 80 | _, err = postsCol.Indexes().CreateOne(ctx, searchIndexModel) 81 | if err != nil { 82 | log.Error("Failed to create text search index:", err) 83 | } 84 | 85 | container.DatabaseSessionProvider = client 86 | container.DatabaseProvider = db 87 | 88 | return container, nil 89 | } 90 | -------------------------------------------------------------------------------- /board/posts/finders.go: -------------------------------------------------------------------------------- 1 | package post 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/matcornic/hermes/v2" 8 | "github.com/tryanzu/core/core/config" 9 | "math" 10 | 11 | "github.com/tryanzu/core/core/common" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/bson/primitive" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | ) 16 | 17 | // PostNotFound err. 18 | var PostNotFound = errors.New("post has not been found by given criteria") 19 | 20 | func FindId(deps deps, id primitive.ObjectID) (post Post, err error) { 21 | ctx := context.TODO() 22 | err = deps.Mgo().Collection("posts").FindOne(ctx, bson.M{"_id": id}).Decode(&post) 23 | if err == mongo.ErrNoDocuments { 24 | err = PostNotFound 25 | } 26 | return 27 | } 28 | 29 | func FindList(deps deps, scopes ...common.Scope) (list Posts, err error) { 30 | ctx := context.TODO() 31 | cursor, err := deps.Mgo().Collection("posts").Find(ctx, common.ByScope(scopes...)) 32 | if err != nil { 33 | return 34 | } 35 | defer cursor.Close(ctx) 36 | err = cursor.All(ctx, &list) 37 | return 38 | } 39 | 40 | func FindRateList(d deps, date string, offset, limit int) ([]primitive.ObjectID, error) { 41 | list := []primitive.ObjectID{} 42 | scores, err := d.LedisDB().ZRangeByScoreGeneric([]byte("posts:"+date), 0, math.MaxInt64, offset, limit, true) 43 | if err != nil { 44 | return list, err 45 | } 46 | for _, n := range scores { 47 | id, err := primitive.ObjectIDFromHex(string(n.Member)) 48 | if err != nil { 49 | continue 50 | } 51 | list = append(list, id) 52 | } 53 | log.Info("getting rate list at %s", date) 54 | return list, err 55 | } 56 | 57 | func SomeoneCommentedYourPost(name string, post Post) hermes.Email { 58 | c := config.C.Copy() 59 | link := c.Site.MakeURL("p/" + post.Slug + "/" + post.Id.Hex()) 60 | return hermes.Email{ 61 | Body: hermes.Body{ 62 | Name: name, 63 | Intros: []string{ 64 | fmt.Sprintf("Tu publicación en %s (%s) recibió un comentario mientras no estabas.", c.Site.Name, post.Title), 65 | }, 66 | Actions: []hermes.Action{ 67 | { 68 | Button: hermes.Button{ 69 | Color: "#3D5AFE", 70 | Text: "Ver publicación", 71 | Link: link, 72 | }, 73 | }, 74 | }, 75 | Outros: []string{ 76 | "Si deseas dejar de recibir notificaciones puedes entrar en tu cuenta y cambiar la configuración de avisos.", 77 | }, 78 | Signature: "Un saludo", 79 | }, 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /board/posts/model.go: -------------------------------------------------------------------------------- 1 | package post 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | type Post struct { 10 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 11 | Title string `bson:"title" json:"title"` 12 | Slug string `bson:"slug" json:"slug"` 13 | Type string `bson:"type" json:"type"` 14 | Content string `bson:"content" json:"content"` 15 | Categories []string `bson:"categories" json:"categories"` 16 | Comments comments `bson:"comments"` 17 | Category primitive.ObjectID `bson:"category" json:"category"` 18 | UserId primitive.ObjectID `bson:"user_id,omitempty" json:"user_id,omitempty"` 19 | Users []primitive.ObjectID `bson:"users,omitempty" json:"users,omitempty"` 20 | RelatedComponents []primitive.ObjectID `bson:"related_components,omitempty" json:"related_components,omitempty"` 21 | Following bool `bson:"following,omitempty" json:"following,omitempty"` 22 | Pinned bool `bson:"pinned,omitempty" json:"pinned,omitempty"` 23 | Lock bool `bson:"lock" json:"lock"` 24 | IsQuestion bool `bson:"is_question" json:"is_question"` 25 | Solved bool `bson:"solved,omitempty" json:"solved,omitempty"` 26 | Liked int `bson:"liked,omitempty" json:"liked,omitempty"` 27 | Created time.Time `bson:"created_at" json:"created_at"` 28 | Updated time.Time `bson:"updated_at" json:"updated_at"` 29 | Deleted time.Time `bson:"deleted_at,omitempty" json:"deleted_at,omitempty"` 30 | } 31 | 32 | type comments struct { 33 | Count int `bson:"count"` 34 | } 35 | 36 | func (Post) VotableType() string { 37 | return "post" 38 | } 39 | 40 | func (p Post) VotableID() primitive.ObjectID { 41 | return p.Id 42 | } 43 | 44 | // Posts list. 45 | type Posts []Post 46 | 47 | func (list Posts) IDs() []primitive.ObjectID { 48 | m := make([]primitive.ObjectID, len(list)) 49 | for k, item := range list { 50 | m[k] = item.Id 51 | } 52 | return m 53 | } 54 | 55 | func (list Posts) Map() map[primitive.ObjectID]Post { 56 | m := make(map[primitive.ObjectID]Post, len(list)) 57 | for _, item := range list { 58 | m[item.Id] = item 59 | } 60 | 61 | return m 62 | } 63 | -------------------------------------------------------------------------------- /board/search/init.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | "time" 7 | 8 | "github.com/lestrrat-go/ngram" 9 | "github.com/op/go-logging" 10 | "github.com/tryanzu/core/core/config" 11 | "github.com/tryanzu/core/deps" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/bson/primitive" 14 | "go.mongodb.org/mongo-driver/mongo/options" 15 | ) 16 | 17 | var log = logging.MustGetLogger("search") 18 | var usersIndex *ngram.Index 19 | 20 | const bufferSize = 256 21 | 22 | type User struct { 23 | ID primitive.ObjectID `bson:"_id,omitempty"` 24 | Username string `bson:"username"` 25 | Seen time.Time `bson:"last_seen_at"` 26 | Score float64 `bson:"-"` 27 | } 28 | 29 | func (u User) Id() string { 30 | return u.ID.Hex() 31 | } 32 | 33 | func (u User) Content() string { 34 | return u.Username 35 | } 36 | 37 | type users []User 38 | 39 | func (a users) Len() int { return len(a) } 40 | func (a users) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 41 | func (a users) Less(i, j int) bool { return a[i].Score > a[j].Score } 42 | 43 | func prepare() { 44 | log.SetBackend(config.LoggingBackend) 45 | log.Info("service starting...") 46 | go func() { 47 | for { 48 | opts := options.Find().SetSort(bson.M{"last_seen_at": -1}).SetLimit(bufferSize) 49 | cursor, err := deps.Container.Mgo().Collection("users").Find(context.Background(), bson.M{}, opts) 50 | if err != nil { 51 | log.Error(err) 52 | time.Sleep(10 * time.Minute) 53 | continue 54 | } 55 | usersIndex = ngram.NewIndex(1) 56 | for cursor.Next(context.Background()) { 57 | var user User 58 | err := cursor.Decode(&user) 59 | if err != nil { 60 | log.Error(err) 61 | continue 62 | } 63 | err = usersIndex.AddItem(user) 64 | if err != nil { 65 | log.Error(err) 66 | } 67 | } 68 | cursor.Close(context.Background()) 69 | log.Info("in-memory user search index has been rehydrated") 70 | time.Sleep(10 * time.Minute) 71 | } 72 | }() 73 | go func() { 74 | for { 75 | <-config.C.Reload 76 | log.SetBackend(config.LoggingBackend) 77 | } 78 | }() 79 | } 80 | 81 | func Users(match string) users { 82 | results := usersIndex.IterateSimilar(match, 0.5, bufferSize) 83 | list := users{} 84 | for res := range results { 85 | u := res.Item.(User) 86 | u.Score = res.Score 87 | list = append(list, u) 88 | } 89 | sort.Sort(list) 90 | return list 91 | } 92 | 93 | func Boot() { 94 | prepare() 95 | } 96 | -------------------------------------------------------------------------------- /board/legacy/model/notification.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | "time" 6 | ) 7 | 8 | type Counter struct { 9 | UserId primitive.ObjectID `bson:"user_id" json:"user_id"` 10 | Counters map[string]PostCounter `bson:"counters" json:"counters"` 11 | } 12 | 13 | type PostCounter struct { 14 | Counter int `bson:"counter" json:"counter"` 15 | Updated time.Time `bson:"updated_at" json:"updated_at"` 16 | } 17 | 18 | type Notification struct { 19 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 20 | UserId primitive.ObjectID `bson:"user_id" json:"user_id"` 21 | RelatedId primitive.ObjectID `bson:"related_id" json:"related_id"` 22 | Title string `bson:"title" json:"title"` 23 | Text string `bson:"text" json:"text"` 24 | Link string `bson:"link" json:"link"` 25 | Related string `bson:"related" json:"related"` 26 | Seen bool `bson:"seen" json:"seen"` 27 | Image string `bson:"image" json:"image"` 28 | Created time.Time `bson:"created_at" json:"created_at"` 29 | Updated time.Time `bson:"updated_at" json:"updated_at"` 30 | } 31 | 32 | type UserFirebaseNotification struct { 33 | UserId primitive.ObjectID `json:"user_id"` 34 | RelatedId primitive.ObjectID `json:"related_id"` 35 | RelatedExtra string `bson:"related_extra" json:"related_extra"` 36 | Position string `bson:"position,omitempty" json:"position,omitempty"` 37 | Title string `json:"title,omitempty"` 38 | Username string `json:"username,omitempty"` 39 | Text string `json:"text"` 40 | Related string `json:"related"` 41 | Seen bool `json:"seen"` 42 | Image string `json:"image"` 43 | Created time.Time `json:"created_at"` 44 | Updated time.Time `json:"updated_at"` 45 | } 46 | 47 | type UserFirebaseNotifications struct { 48 | Count int `json:"count"` 49 | List map[string]UserFirebaseNotification `json:"list,omitempty"` 50 | } 51 | 52 | type MentionModel struct { 53 | PostId primitive.ObjectID `bson:"post_id" json:"post_id"` 54 | UserId primitive.ObjectID `bson:"user_id" json:"user_id"` 55 | Nested int `bson:"nested" json:"nested"` 56 | } 57 | 58 | type UserFirebase struct { 59 | Online int `json:"online"` 60 | Viewing string `json:"viewing"` 61 | Pending int `json:"pending"` 62 | } 63 | -------------------------------------------------------------------------------- /.do/deploy.template.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | name: anzu 3 | services: 4 | - name: anzu-web 5 | git: 6 | repo_clone_url: https://github.com/tryanzu/anzu.git 7 | branch: main 8 | dockerfile_path: Dockerfile 9 | http_port: 3200 10 | instance_count: 1 11 | instance_size_slug: basic-xxs # Smallest size for testing, user can scale up. 12 | routes: 13 | - path: / 14 | envs: 15 | - key: ENV 16 | value: production 17 | - key: MONGO_URL 18 | value: ${db.DATABASE_URL} 19 | - key: MONGO_NAME 20 | value: ${db.NAME} 21 | - key: REDIS_URL 22 | value: ${cache.INTERNAL_URL} 23 | - key: JWT_SECRET 24 | type: SECRET 25 | value: "CHANGEME" # IMPORTANT: Replace with a long, random string 26 | - key: S3_ENDPOINT 27 | value: "CHANGEME" # e.g., nyc3.digitaloceanspaces.com 28 | - key: S3_ACCESS_KEY_ID 29 | type: SECRET 30 | value: "CHANGEME" 31 | - key: S3_SECRET_ACCESS_KEY 32 | type: SECRET 33 | value: "CHANGEME" 34 | - key: S3_BUCKET_NAME 35 | value: "anzu-assets" 36 | - key: S3_REGION 37 | value: "us-east-1" # Or your bucket's region 38 | - key: OAUTH_GOOGLE_CLIENT_ID 39 | type: SECRET 40 | value: "CHANGEME" 41 | - key: OAUTH_GOOGLE_CLIENT_SECRET 42 | type: SECRET 43 | value: "CHANGEME" 44 | - key: OAUTH_FACEBOOK_CLIENT_ID 45 | type: SECRET 46 | value: "CHANGEME" 47 | - key: OAUTH_FACEBOOK_CLIENT_SECRET 48 | type: SECRET 49 | value: "CHANGEME" 50 | - key: SMTP_HOST 51 | value: "CHANGEME" # e.g., smtp.sendgrid.net 52 | - key: SMTP_PORT 53 | value: "587" 54 | - key: SMTP_USERNAME 55 | type: SECRET 56 | value: "CHANGEME" 57 | - key: SMTP_PASSWORD 58 | type: SECRET 59 | value: "CHANGEME" 60 | - key: SMTP_FROM_EMAIL 61 | value: "CHANGEME" # e.g., no-reply@yourdomain.com 62 | databases: 63 | - name: db 64 | engine: MONGODB 65 | production: true 66 | cluster_name: "anzu-mongodb" 67 | - name: cache 68 | engine: VALKEY 69 | cluster_name: "anzu-valkey" 70 | production: true 71 | -------------------------------------------------------------------------------- /modules/api/controller/flags.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/tryanzu/core/board/flags" 8 | "github.com/tryanzu/core/core/config" 9 | "github.com/tryanzu/core/core/events" 10 | "github.com/tryanzu/core/core/user" 11 | "github.com/tryanzu/core/deps" 12 | "go.mongodb.org/mongo-driver/bson/primitive" 13 | ) 14 | 15 | type upsertFlagForm struct { 16 | RelatedTo string `json:"related_to" binding:"required,eq=post|eq=comment"` 17 | RelatedID primitive.ObjectID `json:"related_id" binding:"required"` 18 | Reason string `json:"category" binding:"required"` 19 | Content string `json:"content" binding:"max=255"` 20 | } 21 | 22 | // NewFlag endpoint. 23 | func NewFlag(c *gin.Context) { 24 | var form upsertFlagForm 25 | if err := c.BindJSON(&form); err != nil { 26 | jsonBindErr(c, http.StatusBadRequest, "Invalid flag request, check parameters", err) 27 | return 28 | } 29 | 30 | rules := config.C.Rules() 31 | if _, exists := rules.Flags[form.Reason]; !exists { 32 | jsonErr(c, http.StatusBadRequest, "Invalid flag reason") 33 | return 34 | } 35 | 36 | usr := c.MustGet("user").(user.User) 37 | if count := flags.TodaysCountByUser(deps.Container, usr.Id); count > 10 { 38 | jsonErr(c, http.StatusPreconditionFailed, "Can't flag anymore for today") 39 | return 40 | } 41 | 42 | flag, err := flags.UpsertFlag(deps.Container, flags.Flag{ 43 | UserID: usr.Id, 44 | RelatedID: &form.RelatedID, 45 | RelatedTo: form.RelatedTo, 46 | Content: form.Content, 47 | Reason: form.Reason, 48 | }) 49 | if err != nil { 50 | jsonErr(c, http.StatusInternalServerError, err.Error()) 51 | return 52 | } 53 | 54 | events.In <- events.NewFlag(flag.ID) 55 | c.JSON(200, gin.H{"flag": flag, "status": "okay"}) 56 | } 57 | 58 | // Flag status request. 59 | func Flag(c *gin.Context) { 60 | var ( 61 | id primitive.ObjectID 62 | related = c.Params.ByName("related") 63 | err error 64 | ) 65 | if id, err = primitive.ObjectIDFromHex(c.Params.ByName("id")); err != nil { 66 | jsonErr(c, http.StatusBadRequest, "malformed request, invalid id") 67 | return 68 | } 69 | usr := c.MustGet("user").(user.User) 70 | f, err := flags.FindOne(deps.Container, related, id, usr.Id) 71 | if err != nil { 72 | jsonErr(c, http.StatusNotFound, "flag not found") 73 | return 74 | } 75 | c.JSON(http.StatusOK, gin.H{"flag": f}) 76 | } 77 | 78 | // FlagReasons endpoint. 79 | func FlagReasons(c *gin.Context) { 80 | rules := config.C.Rules() 81 | reasons := []string{} 82 | for k := range rules.Flags { 83 | reasons = append(reasons, k) 84 | } 85 | c.JSON(200, gin.H{"status": "okay", "reasons": reasons}) 86 | } 87 | -------------------------------------------------------------------------------- /board/events/flags.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/tryanzu/core/board/flags" 8 | "github.com/tryanzu/core/board/realtime" 9 | ev "github.com/tryanzu/core/core/events" 10 | "github.com/tryanzu/core/core/user" 11 | "github.com/tryanzu/core/deps" 12 | "go.mongodb.org/mongo-driver/bson/primitive" 13 | ) 14 | 15 | // ErrInvalidIDRef for events with an id. 16 | var ErrInvalidIDRef = errors.New("invalid id reference. could not find related object") 17 | 18 | // Bind event handlers for flag related actions... 19 | func flagHandlers() { 20 | ev.On <- ev.EventHandler{ 21 | On: ev.NEW_FLAG, 22 | Handler: func(e ev.Event) error { 23 | fid := e.Params["id"].(primitive.ObjectID) 24 | f, err := flags.FindId(deps.Container, fid) 25 | if err != nil { 26 | return ErrInvalidIDRef 27 | } 28 | if f.Reason == "spam" && f.RelatedTo == "chat" { 29 | usr, err := user.FindId(deps.Container, f.UserID) 30 | if err != nil { 31 | return err 32 | } 33 | ban, err := user.UpsertBan(deps.Container, user.Ban{ 34 | UserID: f.UserID, 35 | RelatedID: &fid, 36 | RelatedTo: "chat", 37 | Content: "Flag received from chat", 38 | Reason: "spam", 39 | }) 40 | if err != nil { 41 | return err 42 | } 43 | log.Debug("ban created with id", ban.ID) 44 | realtime.ToChan <- banLog(ban, usr) 45 | } 46 | return nil 47 | }, 48 | } 49 | ev.On <- ev.EventHandler{ 50 | On: ev.NEW_BAN, 51 | Handler: func(e ev.Event) error { 52 | uid := e.Params["userId"].(primitive.ObjectID) 53 | usr, err := user.FindId(deps.Container, uid) 54 | if err != nil { 55 | return err 56 | } 57 | ban, err := user.UpsertBan(deps.Container, user.Ban{ 58 | UserID: uid, 59 | RelatedID: &uid, 60 | RelatedTo: "chat", 61 | Content: "Flag ban received from chat", 62 | Reason: "spam", 63 | }) 64 | if err != nil { 65 | return err 66 | } 67 | log.Debug("ban created with id", ban.ID) 68 | realtime.ToChan <- banLog(ban, usr) 69 | return nil 70 | }, 71 | } 72 | } 73 | 74 | func banLog(ban user.Ban, user user.User) realtime.M { 75 | diff := ban.Until.Sub(ban.Created).Truncate(time.Second) 76 | return realtime.M{ 77 | Channel: "chat:general", 78 | Content: realtime.SocketEvent{ 79 | Event: "log", 80 | Params: map[string]interface{}{ 81 | "msg": "%1$s has been banned for %3$s. reason: %2$s", 82 | "i18n": []string{user.UserName, ban.Reason, diff.String()}, 83 | "meta": map[string]interface{}{ 84 | "userId": user.Id, 85 | "user": user.UserName, 86 | "reason": ban.Reason, 87 | }, 88 | "at": ban.Created, 89 | "id": primitive.NewObjectID(), 90 | }, 91 | }.Encode(), 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /modules/security/init.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/tryanzu/core/deps" 8 | "github.com/tryanzu/core/modules/user" 9 | "github.com/xuyu/goredis" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | ) 14 | 15 | type Module struct { 16 | Redis *goredis.Redis `inject:""` 17 | } 18 | 19 | func (module Module) TrustUserIP(address string, usr *user.One) bool { 20 | ctx := context.Background() 21 | var ( 22 | ip IpAddress 23 | err error 24 | ) 25 | database := deps.Container.Mgo() 26 | user := usr.Data() 27 | 28 | // The address haven't been trusted before so we need to lookup 29 | trustedAddressesCollection := database.Collection("trusted_addresses") 30 | err = trustedAddressesCollection.FindOne(ctx, bson.M{"address": address}).Decode(&ip) 31 | if err != nil { 32 | if err == mongo.ErrNoDocuments { 33 | trusted := &IpAddress{ 34 | Address: address, 35 | Users: []primitive.ObjectID{user.Id}, 36 | Banned: user.Banned, 37 | } 38 | _, err = trustedAddressesCollection.InsertOne(ctx, trusted) 39 | return err != nil && !user.Banned 40 | } 41 | return false 42 | } 43 | 44 | if ip.Banned && user.Banned { 45 | return false 46 | } else if !ip.Banned && user.Banned { 47 | // In case the ip is not banned but the user is then update it 48 | filter := bson.M{"_id": ip.Id} 49 | update := bson.M{"$set": bson.M{ 50 | "banned": true, 51 | "banned_at": time.Now(), 52 | }, "$push": bson.M{"banned_reason": user.UserName + " has propagated the ban to the IP address."}} 53 | _, err = trustedAddressesCollection.UpdateOne(ctx, filter, update) 54 | if err != nil { 55 | panic(err) 56 | } 57 | return false 58 | } else if ip.Banned && !user.Banned { 59 | // In case the ip is banned but the user is not then update it 60 | usersCollection := database.Collection("users") 61 | filter := bson.M{"_id": user.Id} 62 | update := bson.M{"$set": bson.M{"banned": true, "banned_at": time.Now()}, "$push": bson.M{"banned_reason": user.UserName + " has accessed from a flagged IP. " + ip.Address}} 63 | _, err = usersCollection.UpdateOne(ctx, filter, update) 64 | if err != nil { 65 | panic(err) 66 | } 67 | return false 68 | } 69 | 70 | return true 71 | } 72 | 73 | func (module Module) TrustIP(address string) bool { 74 | ctx := context.Background() 75 | var ip IpAddress 76 | database := deps.Container.Mgo() 77 | collection := database.Collection("trusted_addresses") 78 | err := collection.FindOne(ctx, bson.M{"address": address}).Decode(&ip) 79 | 80 | if err != nil { 81 | return err == mongo.ErrNoDocuments 82 | } 83 | 84 | if ip.Banned { 85 | return false 86 | } 87 | 88 | return true 89 | } 90 | -------------------------------------------------------------------------------- /board/votes/mutators.go: -------------------------------------------------------------------------------- 1 | package votes 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/tryanzu/core/core/config" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | ) 13 | 14 | // VoteType should be an integer in the form of up or down. 15 | type VoteType string 16 | 17 | func isValidVoteType(str string) bool { 18 | cnf := config.C.Copy() 19 | return cnf.Site.IsValidReaction(str) 20 | } 21 | 22 | type voteStatus struct { 23 | Count int `json:"count"` 24 | Active bool `json:"active"` 25 | } 26 | 27 | // UpsertVote creates or removes a vote for given votable item<->user 28 | func UpsertVote(deps Deps, item Votable, userID primitive.ObjectID, kind string) (vote Vote, status voteStatus, err error) { 29 | if !isValidVoteType(kind) { 30 | err = errors.New("invalid vote type") 31 | return 32 | } 33 | 34 | ctx := context.TODO() 35 | criteria := bson.M{ 36 | "type": item.VotableType(), 37 | "related_id": item.VotableID(), 38 | "value": kind, 39 | "user_id": userID, 40 | } 41 | 42 | update := bson.M{ 43 | "$inc": bson.M{"changes": 1}, 44 | "$set": bson.M{ 45 | "type": item.VotableType(), 46 | "related_id": item.VotableID(), 47 | "user_id": userID, 48 | "value": kind, 49 | "updated_at": time.Now(), 50 | }, 51 | "$setOnInsert": bson.M{ 52 | "created_at": time.Now(), 53 | }, 54 | } 55 | 56 | upsertTrue := true 57 | result, err := coll(deps).UpdateOne(ctx, criteria, update, &options.UpdateOptions{Upsert: &upsertTrue}) 58 | if err != nil { 59 | return 60 | } 61 | 62 | status = voteStatus{ 63 | Active: true, 64 | Count: 0, 65 | } 66 | 67 | // Get current vote status from remote. 68 | err = coll(deps).FindOne(ctx, criteria).Decode(&vote) 69 | if err != nil { 70 | panic(err) 71 | } 72 | delete(criteria, "user_id") 73 | criteria["deleted_at"] = bson.M{"$exists": false} 74 | c, err := coll(deps).CountDocuments(ctx, criteria) 75 | if err != nil { 76 | panic(err) 77 | } 78 | status.Count = int(c) 79 | 80 | // Delete when the vote is not new. (toggle) 81 | if result.MatchedCount > 0 && vote.Deleted == nil { 82 | deleted := time.Now() 83 | vote.Deleted = &deleted 84 | status.Active = false 85 | status.Count-- 86 | 87 | _, err = coll(deps).UpdateOne(ctx, bson.M{"_id": vote.ID}, bson.M{"$set": bson.M{"deleted_at": deleted}}) 88 | if err != nil { 89 | panic(err) 90 | } 91 | return 92 | } 93 | if vote.Deleted != nil { 94 | status.Count++ 95 | } 96 | _, err = coll(deps).UpdateOne(ctx, bson.M{"_id": vote.ID}, bson.M{"$unset": bson.M{"deleted_at": 1}}) 97 | if err != nil { 98 | panic(err) 99 | } 100 | vote.Deleted = nil 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /modules/api/controller/posts/download_assets.go: -------------------------------------------------------------------------------- 1 | package posts 2 | 3 | import ( 4 | "context" 5 | "github.com/tryanzu/core/board/legacy/model" 6 | "github.com/tryanzu/core/deps" 7 | "go.mongodb.org/mongo-driver/bson" 8 | "go.mongodb.org/mongo-driver/bson/primitive" 9 | 10 | "crypto/tls" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "net/http" 15 | "net/url" 16 | "path/filepath" 17 | "strings" 18 | ) 19 | 20 | func (this API) savePostImages(from string, post_id primitive.ObjectID) error { 21 | 22 | defer this.Errors.Recover() 23 | 24 | // Get the database interface from the DI 25 | ctx := context.Background() 26 | database := deps.Container.Mgo() 27 | amazon_url, err := this.Config.String("amazon.url") 28 | 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | tr := &http.Transport{ 34 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 35 | } 36 | client := &http.Client{Transport: tr} 37 | 38 | // Download the file 39 | response, err := client.Get(from) 40 | if err != nil { 41 | return errors.New(fmt.Sprint("Error while downloading", from, "-", err)) 42 | } 43 | 44 | // Read all the bytes to the image 45 | data, err := io.ReadAll(response.Body) 46 | if err != nil { 47 | return errors.New(fmt.Sprint("Error while downloading", from, "-", err)) 48 | } 49 | 50 | // Detect the downloaded file type 51 | dataType := http.DetectContentType(data) 52 | 53 | if dataType[0:5] == "image" { 54 | 55 | var extension, name string 56 | 57 | // Parse the filename 58 | u, err := url.Parse(from) 59 | 60 | if err != nil { 61 | return errors.New(fmt.Sprint("Error while parsing url", from, "-", err)) 62 | } 63 | 64 | extension = filepath.Ext(u.Path) 65 | name = primitive.NewObjectID().Hex() 66 | 67 | if extension != "" { 68 | 69 | name = name + extension 70 | } else { 71 | 72 | // If no extension is provided on the url then add a dummy one 73 | name = name + ".jpg" 74 | } 75 | 76 | path := "posts/" + name 77 | err = this.S3.PutObject(path, data, dataType) 78 | 79 | if err != nil { 80 | 81 | panic(err) 82 | } 83 | 84 | var post model.Post 85 | 86 | err = database.Collection("posts").FindOne(ctx, bson.M{"_id": post_id}).Decode(&post) 87 | 88 | if err == nil { 89 | 90 | post_content := post.Content 91 | 92 | // Replace the url on the comment 93 | if strings.Contains(post_content, from) { 94 | 95 | content := strings.Replace(post_content, from, amazon_url+path, -1) 96 | 97 | // Update the comment 98 | _, err = database.Collection("posts").UpdateOne(ctx, bson.M{"_id": post_id}, bson.M{"$set": bson.M{"content": content}}) 99 | if err != nil { 100 | panic(err) 101 | } 102 | } 103 | 104 | } 105 | } 106 | 107 | response.Body.Close() 108 | 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /modules/gaming/init.go: -------------------------------------------------------------------------------- 1 | package gaming 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | 8 | "github.com/olebedev/config" 9 | "github.com/tryanzu/core/deps" 10 | "github.com/tryanzu/core/modules/exceptions" 11 | "github.com/tryanzu/core/modules/feed" 12 | "github.com/tryanzu/core/modules/user" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/bson/primitive" 15 | ) 16 | 17 | func Boot(file string) *Module { 18 | module := &Module{} 19 | data, err := os.ReadFile(file) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | // Unmarshal file with gaming rules 25 | if err := json.Unmarshal(data, &module.Rules); err != nil { 26 | panic(err) 27 | } 28 | 29 | return module 30 | } 31 | 32 | 33 | type Module struct { 34 | User *user.Module `inject:""` 35 | Feed *feed.FeedModule `inject:""` 36 | Config *config.Config `inject:""` 37 | Errors *exceptions.ExceptionsModule `inject:""` 38 | Rules Rules 39 | } 40 | 41 | // Get user gaming struct 42 | func (self *Module) Get(usr interface{}) *User { 43 | 44 | module := self 45 | 46 | switch usr := usr.(type) { 47 | case primitive.ObjectID: 48 | 49 | // Use user module reference to get the user and then create the user gaming instance 50 | user_obj, err := self.User.Get(usr) 51 | 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | user_gaming := &User{user: user_obj, di: module} 57 | 58 | return user_gaming 59 | 60 | case *user.One: 61 | 62 | user_gaming := &User{user: usr, di: module} 63 | 64 | return user_gaming 65 | 66 | default: 67 | panic("Unkown argument") 68 | } 69 | } 70 | 71 | // Get post gaming struct 72 | func (self *Module) Post(post interface{}) *Post { 73 | 74 | module := self 75 | 76 | switch post := post.(type) { 77 | case primitive.ObjectID: 78 | 79 | // Use user module reference to get the user and then create the user gaming instance 80 | post_object, err := self.Feed.Post(post) 81 | 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | post_gaming := &Post{post: post_object, di: module} 87 | 88 | return post_gaming 89 | 90 | case *feed.Post: 91 | 92 | post_gaming := &Post{post: post, di: module} 93 | 94 | return post_gaming 95 | 96 | default: 97 | panic("Unkown argument") 98 | } 99 | } 100 | 101 | // Get gamification model with badges 102 | func (self *Module) GetRules() Rules { 103 | 104 | ctx := context.Background() 105 | database := deps.Container.Mgo() 106 | rules := self.Rules 107 | 108 | cursor, err := database.Collection("badges").Find(ctx, bson.M{}) 109 | if err != nil { 110 | panic(err) 111 | } 112 | defer cursor.Close(ctx) 113 | 114 | err = cursor.All(ctx, &rules.Badges) 115 | if err != nil { 116 | panic(err) 117 | } 118 | 119 | return rules 120 | } 121 | -------------------------------------------------------------------------------- /core/http/middlewares.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gin-gonic/contrib/sessions" 8 | "github.com/gin-gonic/gin" 9 | "github.com/tryanzu/core/core/config" 10 | "github.com/tryanzu/core/core/events" 11 | "github.com/tryanzu/core/core/user" 12 | "github.com/tryanzu/core/deps" 13 | "github.com/tryanzu/core/modules/acl" 14 | "go.mongodb.org/mongo-driver/bson/primitive" 15 | ) 16 | 17 | // SiteMiddleware loads site config into middlewares pipe context. 18 | func SiteMiddleware() gin.HandlerFunc { 19 | return func(c *gin.Context) { 20 | bucket := sessions.Default(c) 21 | if token := bucket.Get("jwt"); token != nil { 22 | c.Set("jwt", token.(string)) 23 | bucket.Delete("jwt") 24 | _ = bucket.Save() 25 | } 26 | cnf := config.C.Copy() 27 | c.Set("config", cnf) 28 | c.Set("siteName", cnf.Site.Name) 29 | c.Set("siteDescription", cnf.Site.Description) 30 | c.Set("siteUrl", cnf.Site.Url) 31 | c.Next() 32 | } 33 | } 34 | 35 | // Limit number of simultaneous connections. 36 | func MaxAllowed(n int) gin.HandlerFunc { 37 | sem := make(chan struct{}, n) 38 | acquire := func() { sem <- struct{}{} } 39 | release := func() { <-sem } 40 | return func(c *gin.Context) { 41 | acquire() // before request 42 | defer release() // after request 43 | c.Next() 44 | } 45 | } 46 | 47 | func TitleMiddleware(title string) gin.HandlerFunc { 48 | return func(c *gin.Context) { 49 | c.Set("siteName", title) 50 | c.Next() 51 | } 52 | } 53 | 54 | func Can(permission string) gin.HandlerFunc { 55 | return func(c *gin.Context) { 56 | users := acl.LoadedACL.User(c.MustGet("userID").(primitive.ObjectID)) 57 | if !users.Can(permission) { 58 | c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"status": "error", "message": "Not allowed to perform this operation"}) 59 | return 60 | } 61 | c.Set("acl", users) 62 | c.Next() 63 | } 64 | } 65 | 66 | // User middleware loads signed user data for further use. 67 | func UserMiddleware() gin.HandlerFunc { 68 | return func(c *gin.Context) { 69 | sid := c.MustGet("user_id").(string) 70 | oid, _ := primitive.ObjectIDFromHex(sid) 71 | 72 | // Attempt to retrieve user data otherwise abort request. 73 | usr, err := user.FindId(deps.Container, oid) 74 | if err != nil { 75 | _ = c.AbortWithError(412, err) 76 | return 77 | } 78 | sign := events.UserSign{ 79 | UserID: oid, 80 | } 81 | if r := c.Query("reason"); len(r) > 0 { 82 | sign.Reason = r 83 | } 84 | 85 | if usr.Banned && usr.BannedUntil != nil { 86 | if time.Now().Before(*usr.BannedUntil) { 87 | c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ 88 | "status": "error", 89 | "message": "You are banned for now... check again later!", 90 | "until": usr.BannedUntil, 91 | }) 92 | return 93 | } 94 | } 95 | 96 | c.Set("sign", sign) 97 | c.Set("user", usr) 98 | c.Next() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /static/templates/pages/index.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "pages/index.tmpl" }} 2 | 3 | 4 | 5 | {{ .title }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{ if .config.Site.Services.Analytics }} 32 | 33 | 34 | 40 | {{ end }} 41 | 42 | 43 |
44 |
45 | 46 | 47 | 54 | {{ if .jwt }} 55 | 56 | {{ end }} 57 | 58 | 59 | 60 | {{ end }} -------------------------------------------------------------------------------- /core/events/actions.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/tryanzu/core/board/legacy/model" 5 | "github.com/tryanzu/core/board/votes" 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | func PostNew(id primitive.ObjectID) Event { 10 | return Event{ 11 | Name: POSTS_NEW, 12 | Params: map[string]interface{}{ 13 | "id": id, 14 | }, 15 | } 16 | } 17 | 18 | func PostView(sign UserSign, id primitive.ObjectID) Event { 19 | return Event{ 20 | Name: POST_VIEW, 21 | Sign: &sign, 22 | Params: map[string]interface{}{ 23 | "id": id, 24 | }, 25 | } 26 | } 27 | 28 | func PostsReached(sign UserSign, list []primitive.ObjectID) Event { 29 | return Event{ 30 | Name: POSTS_REACHED, 31 | Sign: &sign, 32 | Params: map[string]interface{}{ 33 | "list": list, 34 | }, 35 | } 36 | } 37 | 38 | func PostComment(id primitive.ObjectID) Event { 39 | return Event{ 40 | Name: POSTS_COMMENT, 41 | Params: map[string]interface{}{ 42 | "id": id, 43 | }, 44 | } 45 | } 46 | 47 | func NewFlag(id primitive.ObjectID) Event { 48 | return Event{ 49 | Name: NEW_FLAG, 50 | Params: map[string]interface{}{ 51 | "id": id, 52 | }, 53 | } 54 | } 55 | 56 | func NewBanFlag(userID primitive.ObjectID) Event { 57 | return Event{ 58 | Name: NEW_BAN, 59 | Params: map[string]interface{}{ 60 | "userId": userID, 61 | }, 62 | } 63 | } 64 | 65 | func DeletePost(sign UserSign, id primitive.ObjectID) Event { 66 | return Event{ 67 | Name: POST_DELETED, 68 | Sign: &sign, 69 | Params: map[string]interface{}{ 70 | "id": id, 71 | }, 72 | } 73 | } 74 | 75 | func DeleteComment(sign UserSign, postId, id primitive.ObjectID) Event { 76 | return Event{ 77 | Name: COMMENT_DELETE, 78 | Sign: &sign, 79 | Params: map[string]interface{}{ 80 | "id": id, 81 | "post_id": postId, 82 | }, 83 | } 84 | } 85 | 86 | func UpdateComment(sign UserSign, postId, id primitive.ObjectID) Event { 87 | return Event{ 88 | Name: COMMENT_UPDATE, 89 | Sign: &sign, 90 | Params: map[string]interface{}{ 91 | "id": id, 92 | "post_id": postId, 93 | }, 94 | } 95 | } 96 | 97 | func Vote(vote votes.Vote) Event { 98 | return Event{ 99 | Name: VOTE, 100 | Params: map[string]interface{}{ 101 | "vote": vote, 102 | }, 103 | } 104 | } 105 | 106 | func RawEmit(channel, event string, params map[string]interface{}) Event { 107 | return Event{ 108 | Name: RAW_EMIT, 109 | Params: map[string]interface{}{ 110 | "channel": channel, 111 | "event": event, 112 | "params": params, 113 | }, 114 | } 115 | } 116 | 117 | func TrackMention(userID, relatedID primitive.ObjectID, related string, usersID []primitive.ObjectID) Event { 118 | return Event{ 119 | Name: NEW_MENTION, 120 | Params: map[string]interface{}{ 121 | "user_id": userID, 122 | "related": related, 123 | "related_id": relatedID, 124 | "users": usersID, 125 | }, 126 | } 127 | } 128 | 129 | func TrackActivity(m model.Activity) Event { 130 | return Event{ 131 | Name: RECENT_ACTIVITY, 132 | Params: map[string]interface{}{ 133 | "activity": m, 134 | }, 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /board/comments/mutators.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "context" 5 | "html" 6 | "time" 7 | 8 | "github.com/tryanzu/core/core/content" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | ) 13 | 14 | // Delete comment. 15 | func Delete(deps Deps, c Comment) error { 16 | if c.Deleted != nil { 17 | return nil 18 | } 19 | ctx := context.TODO() 20 | _, err := deps.Mgo().Collection("comments").UpdateOne(ctx, bson.M{"_id": c.Id}, bson.M{ 21 | "$set": bson.M{"deleted_at": time.Now()}, 22 | }) 23 | if err != nil { 24 | return err 25 | } 26 | if c.ReplyType == "post" { 27 | _, err = deps.Mgo().Collection("posts").UpdateOne(ctx, bson.M{"_id": c.ReplyTo}, bson.M{"$inc": bson.M{"comments.count": -1}}) 28 | return err 29 | } 30 | return nil 31 | } 32 | 33 | func DeletePostComments(deps Deps, postID primitive.ObjectID) error { 34 | ctx := context.TODO() 35 | _, err := deps.Mgo().Collection("comments").UpdateMany(ctx, 36 | bson.M{"$or": []bson.M{ 37 | {"post_id": postID}, 38 | {"reply_to": postID}, 39 | }}, bson.M{ 40 | "$set": bson.M{"deleted_at": time.Now()}, 41 | }) 42 | return err 43 | } 44 | 45 | // UpsertComment performs validations before upserting data struct 46 | func UpsertComment(deps Deps, c Comment) (comment Comment, err error) { 47 | ctx := context.TODO() 48 | isNew := false 49 | if c.Id.IsZero() { 50 | c.Id = primitive.NewObjectID() 51 | c.Created = time.Now() 52 | isNew = true 53 | } 54 | 55 | if c.ReplyType == "comment" && c.PostId.IsZero() { 56 | id := c.ReplyTo 57 | for { 58 | var ref Comment 59 | ref, err = FindId(deps, id) 60 | if err != nil { 61 | return 62 | } 63 | id = ref.ReplyTo 64 | if ref.ReplyType == "post" { 65 | c.PostId = ref.ReplyTo 66 | break 67 | } 68 | continue 69 | } 70 | } 71 | 72 | c.Content = html.EscapeString(c.Content) 73 | c.Updated = time.Now() 74 | 75 | // Pre-process comment content. 76 | processed, err := content.Preprocess(deps, c) 77 | if err != nil { 78 | return 79 | } 80 | 81 | c = processed.(Comment) 82 | upsertTrue := true 83 | _, err = deps.Mgo().Collection("comments").ReplaceOne(ctx, bson.M{"_id": c.Id}, c, &options.ReplaceOptions{Upsert: &upsertTrue}) 84 | if err != nil { 85 | return 86 | } 87 | 88 | if isNew { 89 | if c.ReplyType == "post" { 90 | _, err = deps.Mgo().Collection("posts").UpdateOne(ctx, bson.M{"_id": c.ReplyTo}, bson.M{ 91 | "$inc": bson.M{"comments.count": 1}, 92 | "$set": bson.M{"updated_at": time.Now()}, 93 | "$addToSet": bson.M{"users": c.UserId}, 94 | }) 95 | } else { 96 | _, err = deps.Mgo().Collection("posts").UpdateOne(ctx, bson.M{"_id": c.PostId}, bson.M{ 97 | "$addToSet": bson.M{"users": c.UserId}, 98 | "$set": bson.M{"updated_at": time.Now()}, 99 | }) 100 | } 101 | } 102 | 103 | if err != nil { 104 | return 105 | } 106 | 107 | // Pre-process comment content. 108 | processed, err = content.Postprocess(deps, c) 109 | if err != nil { 110 | return 111 | } 112 | c = processed.(Comment) 113 | comment = c 114 | return 115 | } 116 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Development Commands 6 | 7 | ### Building and Running 8 | - **Build backend**: `go build -o anzu` (creates executable) 9 | - **Run API server**: `./anzu api` (starts HTTP server on port 3200) 10 | - **Development mode**: `go build -o anzu && ./anzu api` (manual restart required) 11 | - **Interactive shell**: `./anzu shell` (maintenance and admin tools) 12 | 13 | ### Frontend Development 14 | ```bash 15 | cd static/frontend 16 | npm install # Install dependencies 17 | npm run build # Production build 18 | npm start # Development build with watch 19 | npm run eslint # Lint JavaScript code 20 | ``` 21 | 22 | ### Code Quality 23 | - **Lint Go code**: `golangci-lint run` (install with `go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest`) 24 | - **Quick lint**: `golangci-lint run --fast` (faster checks only) 25 | 26 | ### Database and Services 27 | - **Start MongoDB**: `docker compose up` (includes MongoDB 8, mongo-express, and MinIO) 28 | - **Sync ranking**: `./anzu sync-ranking` (recalculates gaming rankings) 29 | 30 | ## Architecture Overview 31 | 32 | ### Backend Structure 33 | - **Go 1.23+ application** using dependency injection (facebookgo/inject) 34 | - **Modular architecture** with clear separation of concerns 35 | - **Event-driven system** with centralized event handlers in `board/events/` 36 | 37 | ### Key Components 38 | 39 | #### Core Modules (`modules/`) 40 | - **API**: HTTP router and REST endpoints (Gin framework) 41 | - **User**: Authentication, profiles, and user management 42 | - **Gaming**: Ranking system and gamification features 43 | - **ACL**: Permission and role-based access control 44 | - **Security**: Trust network and user trust calculations 45 | - **Notifications**: Email and in-app notification system 46 | - **Feed**: Post aggregation and activity feeds 47 | 48 | #### Board Domain (`board/`) 49 | - **Posts**: Content creation and management 50 | - **Comments**: Threaded discussions 51 | - **Categories**: Content organization 52 | - **Votes**: Reaction system (upvotes/downvotes) 53 | - **Flags**: Content moderation 54 | - **Realtime**: WebSocket communication using Glue 55 | 56 | #### Core Services (`core/`) 57 | - **Config**: Application configuration management 58 | - **Events**: Centralized event handling system 59 | - **HTTP**: Middleware and HTTP utilities 60 | - **Content**: Text processing and mention parsing 61 | 62 | ### Frontend 63 | - **React-based SPA** in `static/frontend/` 64 | - **Webpack build system** with development and production configs 65 | - **SCSS theming** with multiple theme support 66 | - **Real-time features** via WebSocket integration 67 | 68 | ### Database 69 | - **MongoDB** as primary database 70 | - **Redis** for caching and sessions 71 | - **MinIO** for S3-compatible object storage 72 | 73 | ### Key Patterns 74 | - **Dependency Injection**: Uses Facebook's inject library for DI 75 | - **Event System**: Centralized event handling for cross-module communication 76 | - **Module Pattern**: Each feature is a self-contained module with deps, finders, model, and mutators 77 | - **Trust Network**: User trust calculation system for content moderation 78 | 79 | ### Authentication 80 | - **JWT tokens** for API authentication 81 | - **OAuth integration** (Google, Facebook) via Goth library 82 | - **Session-based** web authentication 83 | 84 | ### Development Notes 85 | - Default admin credentials: `admin@local.domain` / `admin` 86 | - Uses Cobra for CLI commands 87 | - Follows Conventional Commits specification 88 | - Real-time features via `/glue/` WebSocket endpoint -------------------------------------------------------------------------------- /core/events/init.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/op/go-logging" 8 | "github.com/tryanzu/core/core/config" 9 | "github.com/tryanzu/core/deps" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | ) 13 | 14 | var ( 15 | log = logging.MustGetLogger("coreEvents") 16 | 17 | // In -put channel for incoming events. 18 | In chan Event 19 | 20 | // On "event" channel. Register event handlers using channels. 21 | On chan EventHandler 22 | ) 23 | 24 | type Handler func(Event) error 25 | 26 | // Map of handlers that will react to events. 27 | var Handlers map[string][]Handler 28 | 29 | type EventHandler struct { 30 | On string 31 | Handler Handler 32 | } 33 | 34 | type Event struct { 35 | Name string 36 | Sign *UserSign 37 | Params map[string]interface{} 38 | } 39 | 40 | type eventLog struct { 41 | ID primitive.ObjectID `bson:"_id,omitempty"` 42 | Name string `bson:"name"` 43 | Sign *UserSign `bson:"sign,omitempty"` 44 | Params map[string]interface{} `bson:"params,omitempty"` 45 | Created time.Time `bson:"created_at,omitempty"` 46 | Finished *time.Time `bson:"finished_at,omitempty"` 47 | } 48 | 49 | type UserSign struct { 50 | Reason string 51 | UserID primitive.ObjectID 52 | } 53 | 54 | func execHandlers(list []Handler, event Event) { 55 | starts := time.Now() 56 | ref := eventLog{ 57 | ID: primitive.NewObjectID(), 58 | Name: event.Name, 59 | Sign: event.Sign, 60 | Params: event.Params, 61 | Created: time.Now(), 62 | } 63 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 64 | defer cancel() 65 | _, err := deps.Container.Mgo().Collection("events").InsertOne(ctx, &ref) 66 | if err != nil { 67 | log.Errorf("events insert failed err=%v", err) 68 | return 69 | } 70 | var failed uint16 71 | for h := range list { 72 | err = list[h](event) 73 | if err != nil { 74 | log.Errorf("events run handler failed event=%s params=%v err=%v", event.Name, event.Params, err) 75 | failed++ 76 | } 77 | } 78 | finished := time.Now() 79 | elapsed := finished.Sub(starts) 80 | updateCtx, updateCancel := context.WithTimeout(context.Background(), 5*time.Second) 81 | defer updateCancel() 82 | _, err = deps.Container.Mgo().Collection("events").UpdateOne(updateCtx, bson.M{"_id": ref.ID}, bson.M{"$set": bson.M{ 83 | "finished_at": finished, 84 | "elapsed": elapsed, 85 | "handlers": len(list), 86 | "failed": failed, 87 | }}) 88 | if err != nil { 89 | log.Errorf("events finish update failed err=%v", err) 90 | } else { 91 | log.Debugf("event processed id=%s", ref.ID) 92 | } 93 | } 94 | 95 | func sink(in chan Event, on chan EventHandler) { 96 | for { 97 | select { 98 | case event := <-in: // For incoming events spawn a goroutine running handlers. 99 | if ls, exists := Handlers[event.Name]; exists { 100 | go execHandlers(ls, event) 101 | } else { 102 | go execHandlers([]Handler{}, event) 103 | } 104 | case h := <-on: // Register new handlers. 105 | if _, exists := Handlers[h.On]; !exists { 106 | Handlers[h.On] = []Handler{} 107 | } 108 | 109 | Handlers[h.On] = append(Handlers[h.On], h.Handler) 110 | } 111 | } 112 | } 113 | 114 | // init channel for input events, consumers & map of handlers. 115 | func init() { 116 | In = make(chan Event, 10) 117 | On = make(chan EventHandler) 118 | Handlers = make(map[string][]Handler) 119 | 120 | go sink(In, On) 121 | } 122 | 123 | func Boot() { 124 | log.SetBackend(config.LoggingBackend) 125 | } 126 | -------------------------------------------------------------------------------- /board/legacy/model/part.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson/primitive" 5 | ) 6 | 7 | type PartByModel struct { 8 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id"` 9 | Name string `bson:"name" json:"name"` 10 | Type string `bson:"type" json:"type"` 11 | Slug string `bson:"slug" json:"slug"` 12 | Price float64 `bson:"price" json:"price"` 13 | Manufacturer string `bson:"manufacturer,omitempty" json:"manufacturer,omitempty"` 14 | PartNumber string `bson:"partnumber,omitempty" json:"partnumber,omitempty"` 15 | Model string `bson:"model,omitempty" json:"model,omitempty"` 16 | Socket string `bson:"supported_sockets,omitempty" json:"supported_sockets,omitempty"` 17 | LiquidCooled bool `bson:"liquid_cooled,omitempty" json:"liquid_cooled,omitempty"` 18 | BearingType string `bson:"bearing_type,omitempty" json:"bearing_type,omitempty"` 19 | NoiseLevel string `bson:"noise_level,omitempty" json:"noise_level,omitempty"` 20 | FanRpm string `bson:"fan_rpm,omitempty" json:"fan_rpm,omitempty"` 21 | Speed string `bson:"speed,omitempty" json:"speed,omitempty"` 22 | Size string `bson:"size,omitempty" json:"size,omitempty"` 23 | PriceGb string `bson:"gb_price,omitempty" json:"gb_price,omitempty"` 24 | Cas string `bson:"cas,omitempty" json:"cas,omitempty"` 25 | Voltage string `bson:"voltage,omitempty" json:"voltage,omitempty"` 26 | HeatSpreader bool `bson:"heat_spreader,omitempty" json:"heat_spreader,omitempty"` 27 | Ecc bool `bson:"ecc,omitempty" json:"ecc,omitempty"` 28 | Registered bool `bson:"registered,omitempty" json:"registered,omitempty"` 29 | Color string `bson:"color,omitempty" json:"color,omitempty"` 30 | Chipset string `bson:"chipset,omitempty" json:"chipset,omitempty"` 31 | MemorySlots string `bson:"memory_slots,omitempty" json:"memory_slots,omitempty"` 32 | MemoryType string `bson:"memory_type,omitempty" json:"memory_type,omitempty"` 33 | MaxMemory string `bson:"memory_max,omitempty" json:"memory_max,omitempty"` 34 | RaidSupport bool `bson:"raid_support,omitempty" json:"raid_support,omitempty"` 35 | OnboardVideo bool `bson:"onboard_video,omitempty" json:"onboard_video,omitempty"` 36 | Crossfire bool `bson:"crossfire_support,omitempty" json:"crossfire_support,omitempty"` 37 | SliSupport bool `bson:"sli_support,omitempty" json:"sli_support,omitempty"` 38 | SATA string `bson:"sata_6gbs" json:"sata_6gbs"` 39 | OnboardEthernet string `bson:"onboard_ethernet,omitempty" json:"onboard_ethernet,omitempty"` 40 | OnboardUsb3 bool `bson:"onboard_usb_3,omitempty" json:"onboard_usb_3,omitempty"` 41 | Capacity string `bson:"capacity,omitempty" json:"capacity,omitempty"` 42 | Interface string `bson:"interface,omitempty" json:"interface,omitempty"` 43 | Cache string `bson:"cache,omitempty" json:"cache,omitempty"` 44 | SsdController string `bson:"ssd_controller,omitempty" json:"ssd_controller,omitempty"` 45 | FormFactor string `bson:"form_factor,omitempty" json:"form_factor,omitempty"` 46 | GbPerDollar string `bson:"gb_per_dollar,omitempty" json:"gb_per_dollar,omitempty"` 47 | } 48 | -------------------------------------------------------------------------------- /modules/gaming/model.go: -------------------------------------------------------------------------------- 1 | package gaming 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | type Rules struct { 10 | Updated time.Time `json:"updated_at"` 11 | Rules []RuleModel `json:"rules"` 12 | Badges []BadgeModel `json:"badges,omitempty"` 13 | } 14 | 15 | type RuleModel struct { 16 | Level int `json:"level"` 17 | Name string `json:"name"` 18 | Start int `json:"swords_start"` 19 | End int `json:"swords_end"` 20 | Tribute int `json:"tribute"` 21 | Shit int `json:"shit"` 22 | Coins int `json:"coins"` 23 | } 24 | 25 | type BadgeModel struct { 26 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 27 | Type string `bson:"type" json:"type"` 28 | TypeLabel string `bson:"type_label" json:"type_label"` 29 | Slug string `bson:"slug" json:"slug"` 30 | Name string `bson:"name" json:"name"` 31 | Description string `bson:"description" json:"description"` 32 | Coins int `bson:"coins,omitempty" json:"coins,omitempty"` 33 | RequiredBadge primitive.ObjectID `bson:"required_badge,omitempty" json:"required_badge,omitempty"` 34 | RequiredLevel int `bson:"required_level,omitempty" json:"required_level,omitempty"` 35 | Avaliable bool `bson:"available" json:"available"` 36 | } 37 | 38 | type RankingModel struct { 39 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 40 | UserId primitive.ObjectID `bson:"user_id" json:"user_id"` 41 | Badges int `bson:"badges" json:"badges"` 42 | Swords int `bson:"swords" json:"swords"` 43 | Coins int `bson:"coins" json:"coins"` 44 | Position RankingPositionModel `bson:"position" json:"position"` 45 | Before RankingPositionModel `bson:"before" json:"before"` 46 | User RankingUserModel `bson:"-" json:"user,omitempty"` 47 | Created time.Time `bson:"created_at" json:"created_at"` 48 | } 49 | 50 | type RankingPositionModel struct { 51 | Wealth int `bson:"wealth" json:"wealth"` 52 | Swords int `bson:"swords" json:"swords"` 53 | Badges int `bson:"badges" json:"badges"` 54 | } 55 | 56 | type RankingUserModel struct { 57 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id"` 58 | UserName string `bson:"username" json:"username"` 59 | Image string `bson:"image" json:"image,omitempty"` 60 | Gaming map[string]interface{} `bson:"gaming" json:"gaming,omitempty"` 61 | } 62 | 63 | type RankPosition struct { 64 | Id string 65 | Value int 66 | } 67 | 68 | type RankPositions []RankPosition 69 | 70 | func (a RankPositions) Len() int { return len(a) } 71 | func (a RankPositions) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 72 | func (a RankPositions) Less(i, j int) bool { return a[i].Value > a[j].Value } 73 | 74 | type RankBySwords []RankingModel 75 | 76 | func (a RankBySwords) Len() int { return len(a) } 77 | func (a RankBySwords) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 78 | func (a RankBySwords) Less(i, j int) bool { return a[i].Swords > a[j].Swords } 79 | 80 | type RankByCoins []RankingModel 81 | 82 | func (a RankByCoins) Len() int { return len(a) } 83 | func (a RankByCoins) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 84 | func (a RankByCoins) Less(i, j int) bool { return a[i].Coins > a[j].Coins } 85 | 86 | type RankByBadges []RankingModel 87 | 88 | func (a RankByBadges) Len() int { return len(a) } 89 | func (a RankByBadges) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 90 | func (a RankByBadges) Less(i, j int) bool { return a[i].Badges > a[j].Badges } 91 | -------------------------------------------------------------------------------- /modules/helpers/init.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "math/rand" 7 | "reflect" 8 | "regexp" 9 | "strings" 10 | "time" 11 | "unicode" 12 | 13 | "golang.org/x/crypto/bcrypt" 14 | "golang.org/x/text/unicode/norm" 15 | ) 16 | 17 | var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 18 | var lat = []*unicode.RangeTable{unicode.Letter, unicode.Number} 19 | var nop = []*unicode.RangeTable{unicode.Mark, unicode.Sk, unicode.Lm} 20 | var email_exp = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) 21 | 22 | func InArray(val interface{}, array interface{}) (exists bool, index int) { 23 | exists = false 24 | index = -1 25 | 26 | switch reflect.TypeOf(array).Kind() { 27 | case reflect.Slice: 28 | s := reflect.ValueOf(array) 29 | 30 | for i := 0; i < s.Len(); i++ { 31 | if reflect.DeepEqual(val, s.Index(i).Interface()) { 32 | index = i 33 | exists = true 34 | return 35 | } 36 | } 37 | } 38 | 39 | return 40 | } 41 | 42 | func Truncate(s string, length int) string { 43 | var numRunes = 0 44 | for index := range s { 45 | numRunes++ 46 | if numRunes > length { 47 | return s[:index] 48 | } 49 | } 50 | return s 51 | } 52 | 53 | func StrSlug(s string) string { 54 | 55 | // Trim before counting 56 | s = strings.Trim(s, " ") 57 | 58 | buf := make([]rune, 0, len(s)) 59 | dash := false 60 | for _, r := range norm.NFKD.String(s) { 61 | switch { 62 | // unicode 'letters' like mandarin characters pass through 63 | case unicode.IsOneOf(lat, r): 64 | buf = append(buf, unicode.ToLower(r)) 65 | dash = true 66 | case unicode.IsOneOf(nop, r): 67 | // skip 68 | case dash: 69 | buf = append(buf, '-') 70 | dash = false 71 | } 72 | } 73 | if i := len(buf) - 1; i >= 0 && buf[i] == '-' { 74 | buf = buf[:i] 75 | } 76 | return string(buf) 77 | } 78 | 79 | func StrSlugRandom(s string) string { 80 | 81 | var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 82 | 83 | b := make([]rune, 6) 84 | for i := range b { 85 | b[i] = letters[rand.Intn(len(letters))] 86 | } 87 | 88 | suffix := string(b) 89 | 90 | return StrSlug(s) + suffix 91 | } 92 | 93 | func StrRandom(length int) string { 94 | r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(length))) 95 | b := make([]rune, length) 96 | for i := range b { 97 | b[i] = letters[r.Intn(len(letters))] 98 | } 99 | return string(b) 100 | } 101 | 102 | func StrCapRandom(length int) string { 103 | 104 | var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") 105 | 106 | b := make([]rune, length) 107 | for i := range b { 108 | b[i] = letters[rand.Intn(len(letters))] 109 | } 110 | 111 | generated := string(b) 112 | 113 | return generated 114 | } 115 | 116 | func StrNumRandom(length int) string { 117 | 118 | var letters = []rune("0123456789") 119 | 120 | b := make([]rune, length) 121 | for i := range b { 122 | b[i] = letters[rand.Intn(len(letters))] 123 | } 124 | 125 | generated := string(b) 126 | 127 | return generated 128 | } 129 | 130 | func Sha256(s string) string { 131 | encrypted := []byte(s) 132 | sha256 := sha256.New() 133 | sha256.Write(encrypted) 134 | return hex.EncodeToString(sha256.Sum(nil)) 135 | } 136 | 137 | func HashPassword(password string) (string, error) { 138 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) 139 | return string(bytes), err 140 | } 141 | 142 | func CheckPasswordHash(password, hash string) bool { 143 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 144 | return err == nil 145 | } 146 | 147 | func IsEmail(s string) bool { 148 | return email_exp.MatchString(s) 149 | } 150 | -------------------------------------------------------------------------------- /modules/api/controller/user.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/contrib/sessions" 8 | "github.com/gin-gonic/gin" 9 | "github.com/tryanzu/core/board/search" 10 | "github.com/tryanzu/core/core/config" 11 | "github.com/tryanzu/core/core/user" 12 | "github.com/tryanzu/core/deps" 13 | "go.mongodb.org/mongo-driver/bson/primitive" 14 | ) 15 | 16 | // Users paginated fetch. 17 | func Users(c *gin.Context) { 18 | var ( 19 | limit = 10 20 | sort = c.Query("sort") 21 | before *primitive.ObjectID 22 | after *primitive.ObjectID 23 | ) 24 | if n, err := strconv.Atoi(c.Query("limit")); err == nil && n <= 50 { 25 | limit = n 26 | } 27 | 28 | if bid := c.Query("before"); len(bid) > 0 { 29 | if _, err := primitive.ObjectIDFromHex(bid); err == nil { 30 | id, _ := primitive.ObjectIDFromHex(bid) 31 | before = &id 32 | } 33 | } 34 | 35 | if bid := c.Query("after"); len(bid) > 0 { 36 | if _, err := primitive.ObjectIDFromHex(bid); err == nil { 37 | id, _ := primitive.ObjectIDFromHex(bid) 38 | after = &id 39 | } 40 | } 41 | 42 | set, err := user.FetchBy( 43 | deps.Container, 44 | user.Page(limit, sort == "reverse", before, after), 45 | ) 46 | if err != nil { 47 | panic(err) 48 | } 49 | c.JSON(200, set) 50 | } 51 | 52 | func SearchUsers(c *gin.Context) { 53 | match := c.Param("name") 54 | if len(match) > 20 || len(match) == 0 { 55 | jsonErr(c, http.StatusBadRequest, "invalid match string") 56 | return 57 | } 58 | results := search.Users(match) 59 | c.JSON(200, gin.H{"list": results}) 60 | } 61 | 62 | type upsertBanForm struct { 63 | RelatedTo string `json:"related_to" binding:"required,eq=site|eq=post|eq=comment"` 64 | RelatedID *primitive.ObjectID `json:"related_id"` 65 | UserID primitive.ObjectID `json:"user_id"` 66 | Reason string `json:"reason" binding:"required"` 67 | Content string `json:"content" binding:"max=255"` 68 | } 69 | 70 | // Ban endpoint. 71 | func Ban(c *gin.Context) { 72 | var form upsertBanForm 73 | if err := c.BindJSON(&form); err != nil { 74 | jsonBindErr(c, http.StatusBadRequest, "Invalid ban request, check parameters", err) 75 | return 76 | } 77 | rules := config.C.Rules() 78 | if _, exists := rules.BanReasons[form.Reason]; !exists { 79 | jsonErr(c, http.StatusBadRequest, "Invalid ban category") 80 | return 81 | } 82 | ban, err := user.UpsertBan(deps.Container, user.Ban{ 83 | UserID: form.UserID, 84 | RelatedID: form.RelatedID, 85 | RelatedTo: form.RelatedTo, 86 | Content: form.Content, 87 | Reason: form.Reason, 88 | }) 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | //events.In <- events.NewFlag(flag.ID) 94 | c.JSON(200, gin.H{"status": "okay", "ban": ban}) 95 | } 96 | 97 | // BanReasons endpoint. 98 | func BanReasons(c *gin.Context) { 99 | rules := config.C.Rules() 100 | reasons := []string{} 101 | for k := range rules.BanReasons { 102 | reasons = append(reasons, k) 103 | } 104 | c.JSON(200, gin.H{"status": "okay", "reasons": reasons}) 105 | } 106 | 107 | // RecoveryLink handler. Validates and performs proper flow forwards. 108 | func RecoveryLink(c *gin.Context) { 109 | var ( 110 | token = c.Param("token") 111 | ) 112 | if len(token) == 0 { 113 | c.Redirect(http.StatusTemporaryRedirect, "/") 114 | return 115 | } 116 | usr, auth, err := user.UseRecoveryToken(deps.Container, c.ClientIP(), token) 117 | if err != nil { 118 | c.Redirect(http.StatusTemporaryRedirect, "/") 119 | return 120 | } 121 | bucket := sessions.Default(c) 122 | bucket.Set("jwt", auth) 123 | _ = bucket.Save() 124 | c.Redirect(http.StatusTemporaryRedirect, "/u/"+usr.UserNameSlug+"/"+usr.Id.Hex()) 125 | } 126 | -------------------------------------------------------------------------------- /core/config/model.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strings" 4 | 5 | // Anzu config params struct. 6 | type Anzu struct { 7 | Site anzuSite 8 | Homedir string 9 | Security anzuSecurity 10 | Mail anzuMail 11 | Oauth OauthConfig 12 | Runtime anzuRuntime 13 | } 14 | 15 | type OauthConfig struct { 16 | Facebook OauthKeys 17 | Google OauthKeys 18 | } 19 | 20 | type OauthKeys struct { 21 | Key string 22 | Secret string 23 | Callback string 24 | } 25 | 26 | type chatChan struct { 27 | Name string `json:"name"` 28 | Description string `json:"description"` 29 | Youtube string `json:"youtubeVideo" toml:"youtubeVideo"` 30 | Twitch string `json:"twitchVideo" toml:"twitchVideo"` 31 | } 32 | 33 | type anzuSecurity struct { 34 | Secret string `json:"secret"` 35 | StrictIPCheck bool `json:"strictIPCheck"` 36 | } 37 | 38 | type anzuRuntime struct { 39 | LoggingLevel string `json:"logLevel"` 40 | } 41 | 42 | type Flag struct { 43 | ShouldRemove string `hcl:"shouldRemove"` 44 | ShouldBan string `hcl:"shouldBan"` 45 | } 46 | 47 | type Rules struct { 48 | Reactions map[string]*ReactionEffect `hcl:"reaction"` 49 | BanReasons map[string]*BanReason `hcl:"banReason"` 50 | Flags map[string]*Flag `hcl:"flag"` 51 | } 52 | 53 | type anzuSite struct { 54 | Name string `json:"name"` 55 | TitleMotto string `json:"titleMotto"` 56 | Description string `json:"description"` 57 | Url string `json:"url"` 58 | LogoUrl string `json:"logoUrl"` 59 | Theme string `json:"theme"` 60 | Nav []siteLink `json:"nav"` 61 | Chat []chatChan `json:"chat"` 62 | Services siteServices `json:"services"` 63 | Quickstart siteQuickstart `json:"quickstart"` 64 | Reactions [][]string `json:"reactions"` 65 | ThirdPartyAuth []string `json:"thirdPartyAuth"` 66 | } 67 | 68 | func (site anzuSite) MakeURL(url string) string { 69 | u := site.Url 70 | if !strings.HasSuffix(u, "/") { 71 | u = u + "/" 72 | } 73 | return u + url 74 | } 75 | 76 | func (site anzuSite) IsValidReaction(name string) bool { 77 | for _, rs := range site.Reactions { 78 | if len(rs) == 0 { 79 | continue 80 | } 81 | for _, v := range rs[1:] { 82 | if v == name { 83 | return true 84 | } 85 | } 86 | } 87 | return false 88 | } 89 | 90 | func (site anzuSite) MakeReactions(names []string) []string { 91 | m := make(map[string][]string, len(site.Reactions)) 92 | for _, rs := range site.Reactions { 93 | if len(rs) == 0 { 94 | continue 95 | } 96 | // Assign in map 97 | m[rs[0]] = rs[1:] 98 | } 99 | defaults, hasDefaults := m["default"] 100 | list := []string{} 101 | for _, ns := range names { 102 | if reactions, exists := m[ns]; exists { 103 | list = append(list, reactions...) 104 | } 105 | } 106 | if len(list) == 0 && hasDefaults { 107 | list = defaults 108 | } 109 | return list 110 | } 111 | 112 | type siteLink struct { 113 | Name string `json:"name"` 114 | Href string `json:"href"` 115 | } 116 | 117 | type anzuMail struct { 118 | Server string 119 | User string 120 | Password string 121 | Port int 122 | From string 123 | ReplyTo string 124 | } 125 | 126 | type siteServices struct { 127 | Analytics string `json:"-"` 128 | } 129 | 130 | type siteQuickstart struct { 131 | Headline string `json:"headline"` 132 | Description string `json:"description"` 133 | Links []quickstartLink `json:"links"` 134 | } 135 | 136 | type quickstartLink struct { 137 | Name string `json:"name"` 138 | Href string `json:"href"` 139 | Description string `json:"description"` 140 | } 141 | -------------------------------------------------------------------------------- /board/notifications/model.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/tryanzu/core/board/comments" 7 | posts "github.com/tryanzu/core/board/posts" 8 | "github.com/tryanzu/core/core/common" 9 | "github.com/tryanzu/core/core/user" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | ) 12 | 13 | type Notification struct { 14 | Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` 15 | UserId primitive.ObjectID `bson:"user_id" json:"user_id"` 16 | Type string `bson:"type" json:"type"` 17 | RelatedId primitive.ObjectID `bson:"related_id" json:"related_id"` 18 | Users []primitive.ObjectID `bson:"users" json:"users"` 19 | Seen bool `bson:"seen" json:"seen"` 20 | Created time.Time `bson:"created_at" json:"created_at"` 21 | Updated time.Time `bson:"updated_at" json:"updated_at"` 22 | } 23 | 24 | type Notifications []Notification 25 | 26 | func (all Notifications) UsersScope() common.Scope { 27 | users := map[primitive.ObjectID]bool{} 28 | for _, n := range all { 29 | users[n.UserId] = true 30 | for _, id := range n.Users { 31 | users[id] = true 32 | } 33 | } 34 | 35 | list := make([]primitive.ObjectID, len(users)) 36 | index := 0 37 | for k := range users { 38 | list[index] = k 39 | index++ 40 | } 41 | 42 | return common.WithinID(list) 43 | } 44 | 45 | func (all Notifications) CommentsScope() common.Scope { 46 | comments := map[primitive.ObjectID]struct{}{} 47 | for _, n := range all { 48 | if n.Type != "comment" && n.Type != "mention" { 49 | continue 50 | } 51 | 52 | comments[n.RelatedId] = struct{}{} 53 | } 54 | 55 | list := make([]primitive.ObjectID, len(comments)) 56 | index := 0 57 | for k := range comments { 58 | list[index] = k 59 | index++ 60 | } 61 | 62 | return common.WithinID(list) 63 | } 64 | 65 | func (all Notifications) Humanize(deps Deps) (list []map[string]interface{}, err error) { 66 | ulist, err := user.FindList(deps, all.UsersScope()) 67 | if err != nil { 68 | panic(err) 69 | 70 | } 71 | 72 | clist, err := comments.FindList(deps, all.CommentsScope()) 73 | if err != nil { 74 | panic(err) 75 | 76 | } 77 | 78 | plist, err := posts.FindList(deps, clist.PostsScope()) 79 | if err != nil { 80 | panic(err) 81 | 82 | } 83 | 84 | umap := ulist.Map() 85 | cmap := clist.Map() 86 | pmap := plist.Map() 87 | 88 | for _, n := range all { 89 | switch n.Type { 90 | case "comment": 91 | comment := cmap[n.RelatedId] 92 | post := pmap[comment.RelatedPost()] 93 | user := umap[comment.UserId] 94 | 95 | list = append(list, map[string]interface{}{ 96 | "id": n.Id.Hex(), 97 | "target": "/p/" + post.Slug + "/" + post.Id.Hex() + "#" + n.RelatedId.Hex(), 98 | "title": "Nuevo comentario de @" + user.UserName, 99 | "subtitle": post.Title, 100 | "createdAt": n.Created, 101 | }) 102 | case "mention": 103 | comment := cmap[n.RelatedId] 104 | post := pmap[comment.ReplyTo] 105 | user := umap[comment.UserId] 106 | 107 | list = append(list, map[string]interface{}{ 108 | "id": n.Id.Hex(), 109 | "target": "/p/" + post.Slug + "/" + post.Id.Hex(), /*+ "#c" + comment.Id.Hex()*/ 110 | "title": "@" + user.UserName + " te mencionó en un comentario", 111 | "subtitle": post.Title, 112 | "createdAt": n.Created, 113 | }) 114 | case "chat": 115 | user := umap[n.Users[0]] 116 | list = append(list, map[string]interface{}{ 117 | "id": n.Id.Hex(), 118 | "target": "/chat", 119 | "title": "@" + user.UserName + " te mencionó en el chat", 120 | "createdAt": n.Created, 121 | }) 122 | } 123 | } 124 | 125 | return 126 | } 127 | 128 | type Socket struct { 129 | Chan string `json:"c"` 130 | Action string `json:"action"` 131 | Params map[string]interface{} `json:"p"` 132 | } 133 | -------------------------------------------------------------------------------- /modules/feed/rates.go: -------------------------------------------------------------------------------- 1 | package feed 2 | 3 | import ( 4 | "context" 5 | "github.com/tryanzu/core/board/legacy/model" 6 | "github.com/tryanzu/core/deps" 7 | "go.mongodb.org/mongo-driver/bson" 8 | "go.mongodb.org/mongo-driver/bson/primitive" 9 | "strconv" 10 | ) 11 | 12 | func (di *FeedModule) UpdateFeedRates(list []model.FeedPost) { 13 | 14 | // Recover from any panic even inside this isolated process 15 | defer di.Errors.Recover() 16 | 17 | // Services we will need along the runtime 18 | redis := di.CacheService 19 | 20 | // Sorted list items (redis ZADD) 21 | zadd := make(map[string]map[string]float64) 22 | 23 | for _, post := range list { 24 | 25 | var reached, viewed int 26 | 27 | // Get reach and views 28 | reached, viewed = di.getPostReachViews(post.Id) 29 | 30 | total := reached + viewed 31 | 32 | if total > 101 { 33 | 34 | // Calculate the rates 35 | view_rate := (100.0 / float64(reached)) * float64(viewed) 36 | comment_rate := (100.0 / float64(viewed)) * float64(post.Comments.Count) 37 | participants_rate := (100.0 / float64(post.Comments.Count)) * float64(len(post.Users)) 38 | 39 | final_rate := (view_rate + comment_rate + participants_rate) / 3.0 40 | date := post.Created.Format("2006-01-02") 41 | 42 | if _, okay := zadd[date]; !okay { 43 | 44 | zadd[date] = map[string]float64{} 45 | } 46 | 47 | zadd[date][post.Id.Hex()] = final_rate 48 | } 49 | } 50 | 51 | for date, items := range zadd { 52 | 53 | _, err := redis.ZAdd("feed:relevant:"+date, items) 54 | 55 | if err != nil { 56 | panic(err) 57 | } 58 | } 59 | } 60 | 61 | func (di *FeedModule) UpdatePostRate(post model.Post) { 62 | 63 | // Recover from any panic even inside this isolated process 64 | defer di.Errors.Recover() 65 | 66 | // Services we will need along the runtime 67 | redis := di.CacheService 68 | 69 | // Sorted list items (redis ZADD) 70 | zadd := make(map[string]float64) 71 | 72 | // Get reach and views 73 | reached, viewed := di.getPostReachViews(post.Id) 74 | 75 | total := reached + viewed 76 | 77 | if total > 101 { 78 | 79 | // Calculate the rates 80 | view_rate := 100.0 / float64(reached) * float64(viewed) 81 | comment_rate := 100.0 / float64(viewed) * float64(post.Comments.Count) 82 | participants_rate := 100.0 / float64(post.Comments.Count) * float64(len(post.Users)) 83 | 84 | final_rate := (view_rate + comment_rate + participants_rate) / 3.0 85 | date := post.Created.Format("2006-01-02") 86 | 87 | zadd[post.Id.Hex()] = final_rate 88 | 89 | _, err := redis.ZAdd("feed:relevant:"+date, zadd) 90 | 91 | if err != nil { 92 | panic(err) 93 | } 94 | } 95 | } 96 | 97 | func (di *FeedModule) getPostReachViews(id primitive.ObjectID) (int, int) { 98 | 99 | var reached, viewed int 100 | 101 | // Services we will need along the runtime 102 | ctx := context.Background() 103 | database := deps.Container.Mgo() 104 | redis := di.CacheService 105 | 106 | list_count, _ := redis.Get("feed:count:list:" + id.Hex()) 107 | 108 | if list_count == nil { 109 | 110 | reached64, _ := database.Collection("activity").CountDocuments(ctx, bson.M{"list": id, "event": "feed"}) 111 | reached = int(reached64) 112 | err := redis.Set("feed:count:list:"+id.Hex(), strconv.Itoa(reached), 1800, 0, false, false) 113 | 114 | if err != nil { 115 | panic(err) 116 | } 117 | } else { 118 | 119 | reached, _ = strconv.Atoi(string(list_count)) 120 | } 121 | 122 | viewed_count, _ := redis.Get("feed:count:post:" + id.Hex()) 123 | 124 | if viewed_count == nil { 125 | 126 | viewed64, _ := database.Collection("activity").CountDocuments(ctx, bson.M{"related_id": id, "event": "post"}) 127 | viewed = int(viewed64) 128 | err := redis.Set("feed:count:post:"+id.Hex(), strconv.Itoa(viewed), 1800, 0, false, false) 129 | 130 | if err != nil { 131 | panic(err) 132 | } 133 | } else { 134 | 135 | viewed, _ = strconv.Atoi(string(viewed_count)) 136 | } 137 | 138 | return reached, viewed 139 | } 140 | -------------------------------------------------------------------------------- /core/user/mutators.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "html" 7 | "time" 8 | 9 | "github.com/tryanzu/core/core/config" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/bson/primitive" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | ) 14 | 15 | // ErrInvalidBanReason not present in config 16 | var ErrInvalidBanReason = errors.New("invalid ban reason") 17 | 18 | // ErrInvalidUser user cannot be found 19 | var ErrInvalidUser = errors.New("invalid user to ban") 20 | 21 | func ResetNotifications(d deps, id primitive.ObjectID) (err error) { 22 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 23 | defer cancel() 24 | 25 | _, err = d.Mgo().Collection("users").UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": bson.M{"notifications": 0}}) 26 | return 27 | } 28 | 29 | // LastSeenAt mutation 30 | func LastSeenAt(d deps, id primitive.ObjectID, t time.Time) (err error) { 31 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 32 | defer cancel() 33 | 34 | _, err = d.Mgo().Collection("users").UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": bson.M{"last_seen_at": t}}) 35 | return 36 | } 37 | 38 | // UpsertBan performs validations before upserting data struct 39 | func UpsertBan(d deps, ban Ban) (Ban, error) { 40 | if ban.ID.IsZero() { 41 | ban.ID = primitive.NewObjectID() 42 | ban.Created = time.Now() 43 | ban.Status = ACTIVE 44 | } 45 | usr, err := FindId(d, ban.UserID) 46 | if err != nil { 47 | return ban, ErrInvalidUser 48 | } 49 | rules := config.C.Rules() 50 | rule, exists := rules.BanReasons[ban.Reason] 51 | if !exists { 52 | return ban, ErrInvalidBanReason 53 | } 54 | effects, err := rule.Effects(ban.RelatedTo, usr.BannedTimes) 55 | if err != nil { 56 | panic(err) 57 | } 58 | mins := time.Minute * time.Duration(effects.Duration) 59 | ban.Until = time.Now().Add(mins) 60 | ban.Content = html.EscapeString(ban.Content) 61 | ban.Updated = time.Now() 62 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 63 | defer cancel() 64 | 65 | opts := options.Update().SetUpsert(true) 66 | result, err := d.Mgo().Collection("bans").UpdateOne(ctx, bson.M{"_id": ban.ID}, bson.M{"$set": ban}, opts) 67 | if err != nil { 68 | return ban, err 69 | } 70 | if result.MatchedCount == 0 && ban.Status == ACTIVE { 71 | _, err = d.Mgo().Collection("users").UpdateOne(ctx, bson.M{"_id": ban.UserID}, bson.M{ 72 | "$set": bson.M{ 73 | "banned_at": ban.Created, 74 | "banned": true, 75 | "banned_re": ban.Reason, 76 | "banned_until": ban.Until, 77 | }, 78 | "$inc": bson.M{ 79 | "banned_times": 1, 80 | }, 81 | }) 82 | if err != nil { 83 | return ban, err 84 | } 85 | k := []byte("ban:") 86 | k = append(k, []byte(usr.Id.Hex())...) 87 | err = d.LedisDB().Set(k, []byte{}) 88 | if err != nil { 89 | return ban, err 90 | } 91 | _, err = d.LedisDB().Expire(k, effects.Duration*60) 92 | if err != nil { 93 | return ban, err 94 | } 95 | } 96 | return ban, nil 97 | } 98 | 99 | // UseRecoveryToken to generate auth token. 100 | func UseRecoveryToken(d deps, clientIP, token string) (user User, jwtAuthToken string, err error) { 101 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 102 | defer cancel() 103 | 104 | _, err = d.Mgo().Collection("user_recovery_tokens").UpdateOne(ctx, 105 | bson.M{ 106 | "token": token, 107 | "used": false, 108 | "created_at": bson.M{"$gte": time.Now().Add(-15 * time.Minute)}, 109 | }, 110 | bson.M{ 111 | "$set": bson.M{ 112 | "used_at": time.Now(), 113 | "used": true, 114 | }, 115 | }, 116 | ) 117 | if err != nil { 118 | return 119 | } 120 | var t recoveryToken 121 | err = d.Mgo().Collection("user_recovery_tokens").FindOne(ctx, bson.M{"token": token}).Decode(&t) 122 | if err != nil { 123 | return 124 | } 125 | user, err = FindId(d, t.UserID) 126 | if err != nil { 127 | return 128 | } 129 | jwtAuthToken = genToken(clientIP, t.UserID, user.Roles, 1) 130 | return 131 | } 132 | -------------------------------------------------------------------------------- /deps/s3.go: -------------------------------------------------------------------------------- 1 | package deps 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/credentials" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/s3" 12 | ) 13 | 14 | var ( 15 | // The default value is a legacy bucket used in spartangeek. 16 | AwsS3Bucket = "spartan-board" 17 | // S3Endpoint for configurable endpoint (AWS S3, DigitalOcean Spaces, etc.) 18 | S3Endpoint = "" 19 | // S3Region for configurable region 20 | S3Region = "us-west-1" 21 | ) 22 | 23 | func IgniteS3(container Deps) (Deps, error) { 24 | // Get configuration from environment variables 25 | if bucket := os.Getenv("AWS_S3_BUCKET"); bucket != "" { 26 | AwsS3Bucket = bucket 27 | } 28 | if endpoint := os.Getenv("AWS_S3_ENDPOINT"); endpoint != "" { 29 | S3Endpoint = endpoint 30 | } 31 | if region := os.Getenv("AWS_S3_REGION"); region != "" { 32 | S3Region = region 33 | } 34 | 35 | // Configure AWS session 36 | config := &aws.Config{ 37 | Region: aws.String(S3Region), 38 | } 39 | 40 | // If custom endpoint is provided (for DigitalOcean Spaces, MinIO, etc.) 41 | if S3Endpoint != "" { 42 | config.Endpoint = aws.String(S3Endpoint) 43 | config.S3ForcePathStyle = aws.Bool(true) 44 | } 45 | 46 | // Use default credential chain (env vars, IAM roles, etc.) 47 | // AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY will be picked up automatically 48 | if accessKey := os.Getenv("AWS_ACCESS_KEY_ID"); accessKey != "" { 49 | secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") 50 | config.Credentials = credentials.NewStaticCredentials(accessKey, secretKey, "") 51 | } 52 | 53 | sess, err := session.NewSession(config) 54 | if err != nil { 55 | return container, err 56 | } 57 | 58 | service := s3.New(sess) 59 | container.S3Provider = &S3Service{ 60 | client: service, 61 | bucket: AwsS3Bucket, 62 | } 63 | return container, nil 64 | } 65 | 66 | // S3Service wraps AWS SDK S3 client to provide similar interface to goamz 67 | type S3Service struct { 68 | client *s3.S3 69 | bucket string 70 | } 71 | 72 | // GetBucketName returns the configured bucket name 73 | func (s *S3Service) GetBucketName() string { 74 | return s.bucket 75 | } 76 | 77 | // PutObject uploads an object to S3 78 | func (s *S3Service) PutObject(key string, data []byte, contentType string) error { 79 | _, err := s.client.PutObject(&s3.PutObjectInput{ 80 | Bucket: aws.String(s.bucket), 81 | Key: aws.String(key), 82 | Body: aws.ReadSeekCloser(strings.NewReader(string(data))), 83 | ContentType: aws.String(contentType), 84 | }) 85 | return err 86 | } 87 | 88 | // GetObject retrieves an object from S3 89 | func (s *S3Service) GetObject(key string) ([]byte, error) { 90 | result, err := s.client.GetObject(&s3.GetObjectInput{ 91 | Bucket: aws.String(s.bucket), 92 | Key: aws.String(key), 93 | }) 94 | if err != nil { 95 | return nil, err 96 | } 97 | defer result.Body.Close() 98 | 99 | return io.ReadAll(result.Body) 100 | } 101 | 102 | // DeleteObject deletes an object from S3 103 | func (s *S3Service) DeleteObject(key string) error { 104 | _, err := s.client.DeleteObject(&s3.DeleteObjectInput{ 105 | Bucket: aws.String(s.bucket), 106 | Key: aws.String(key), 107 | }) 108 | return err 109 | } 110 | 111 | // PutReader uploads an object to S3 from a reader 112 | func (s *S3Service) PutReader(key string, reader io.ReadSeeker, size int64, contentType string) error { 113 | _, err := s.client.PutObject(&s3.PutObjectInput{ 114 | Bucket: aws.String(s.bucket), 115 | Key: aws.String(key), 116 | Body: reader, 117 | ContentType: aws.String(contentType), 118 | ContentLength: aws.Int64(size), 119 | }) 120 | return err 121 | } 122 | 123 | // GetURL returns the public URL for an object 124 | func (s *S3Service) GetURL(key string) string { 125 | if S3Endpoint != "" { 126 | // For custom endpoints like DigitalOcean Spaces 127 | return S3Endpoint + "/" + s.bucket + "/" + key 128 | } 129 | // Default AWS S3 URL format 130 | return "https://s3-" + S3Region + ".amazonaws.com/" + s.bucket + "/" + key 131 | } 132 | -------------------------------------------------------------------------------- /modules/user/one.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/tryanzu/core/deps" 8 | "github.com/tryanzu/core/modules/helpers" 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | ) 12 | 13 | type One struct { 14 | data *UserPrivate 15 | di *Module 16 | } 17 | 18 | // Return data model 19 | func (self *One) Data() *UserPrivate { 20 | return self.data 21 | } 22 | 23 | // Data update only persistent on runtime 24 | func (self *One) RUpdate(data *UserPrivate) { 25 | self.data = data 26 | } 27 | 28 | func (self *One) Email() string { 29 | if self.data.Facebook != nil { 30 | fb := self.data.Facebook.(bson.M) 31 | if email, exists := fb["email"]; exists { 32 | return email.(string) 33 | } 34 | } 35 | return self.data.Email 36 | } 37 | 38 | func (self *One) Name() string { 39 | return self.data.UserName 40 | } 41 | 42 | // Helper method to track a signin from the user 43 | func (self *One) TrackUserSignin(client_address string) { 44 | ctx := context.Background() 45 | record := &CheckinModel{ 46 | UserId: self.data.Id, 47 | Address: client_address, 48 | Date: time.Now(), 49 | } 50 | 51 | database := deps.Container.Mgo() 52 | collection := database.Collection("checkins") 53 | _, err := collection.InsertOne(ctx, record) 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | 59 | // Helper method to track a signin from the user 60 | func (self *One) ROwns(entity string, id primitive.ObjectID) { 61 | ctx := context.Background() 62 | database := deps.Container.Mgo() 63 | collection := database.Collection("user_owns") 64 | filter := bson.M{ 65 | "related": entity, 66 | "related_id": id, 67 | "user_id": self.data.Id, 68 | "removed": bson.M{"$exists": false}, 69 | } 70 | update := bson.M{ 71 | "$set": bson.M{"removed": true, "removed_at": time.Now()}, 72 | } 73 | _, err := collection.UpdateMany(ctx, filter, update) 74 | if err != nil { 75 | panic(err) 76 | } 77 | } 78 | 79 | func (self *One) TrackView(entity string, entity_id primitive.ObjectID) { 80 | ctx := context.Background() 81 | database := deps.Container.Mgo() 82 | record := &ViewModel{ 83 | UserId: self.data.Id, 84 | Related: entity, 85 | RelatedId: entity_id, 86 | Created: time.Now(), 87 | } 88 | 89 | userViewsCollection := database.Collection("user_views") 90 | _, err := userViewsCollection.InsertOne(ctx, record) 91 | if err != nil { 92 | panic(err) 93 | } 94 | 95 | if entity == "component" { 96 | componentsCollection := database.Collection("components") 97 | filter := bson.M{"_id": entity_id} 98 | update := bson.M{"$inc": bson.M{"views": 1}} 99 | _, err := componentsCollection.UpdateOne(ctx, filter, update) 100 | if err != nil { 101 | panic(err) 102 | } 103 | } 104 | } 105 | 106 | func (self *One) MarkAsValidated() { 107 | ctx := context.Background() 108 | database := deps.Container.Mgo() 109 | collection := database.Collection("users") 110 | filter := bson.M{"_id": self.data.Id} 111 | update := bson.M{"$set": bson.M{"validated": true}} 112 | _, err := collection.UpdateOne(ctx, filter, update) 113 | if err != nil { 114 | panic(err) 115 | } 116 | 117 | self.data.Validated = true 118 | 119 | // Confirm the referral in case it exists 120 | self.followReferral() 121 | } 122 | 123 | func (o *One) IsValidated() bool { 124 | return o.data.Validated 125 | } 126 | 127 | func (self *One) Update(data map[string]interface{}) (err error) { 128 | ctx := context.Background() 129 | if password, exists := data["password"]; exists { 130 | data["password"] = helpers.Sha256(password.(string)) 131 | } 132 | 133 | database := deps.Container.Mgo() 134 | collection := database.Collection("users") 135 | filter := bson.M{"_id": self.data.Id} 136 | update := bson.M{"$set": data} 137 | _, err = collection.UpdateOne(ctx, filter, update) 138 | return 139 | } 140 | 141 | func (self *One) followReferral() { 142 | ctx := context.Background() 143 | // Just update blindly 144 | database := deps.Container.Mgo() 145 | collection := database.Collection("referrals") 146 | filter := bson.M{"user_id": self.data.Id} 147 | update := bson.M{"$set": bson.M{"confirmed": true}} 148 | _, _ = collection.UpdateOne(ctx, filter, update) 149 | } 150 | --------------------------------------------------------------------------------