├── 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 |
--------------------------------------------------------------------------------