nakama

package module
v0.0.0-...-6f28fd0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 3, 2024 License: ISC Imports: 39 Imported by: 0

README

join slack join discord

Nakama

banner

Source code of the next social network for anime fans. Still on development.

New work is being done at next branch.

Docker build

The easies way to start the server and its dependencies is by using Docker.

docker-compose up --build

Building

Instead of Docker, you can also install and build stuff by yourself, that way you have complete control.

So, besides having Go installed, the server needs CockroachDB and NATS. Also Node.js and npm for the front-end.

First, you need a cockroach node running.

cockroach start-single-node --insecure --listen-addr 127.0.0.1

Then, you need to create the database and tables.

cat schema.sql | cockroach sql --insecure

Then you need to start NATS server.

nats-server

Now, you can build and run the server.

go build ./cmd/nakama
./nakama

For the front-end you need to install dependencies.

cd web/app
npm i

Now you can either build the entire front-end, or run a dev server:

npm run build

or

npm run dev

Database Backups

Instructions to perform a database backup and restore.
Have a running S3 compatible instance, then:

BACKUP DATABASE nakama INTO 's3://${S3_BUCKET}?AWS_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}&AWS_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}&AWS_REGION=${S3_REGION}&AWS_ENDPOINT=${S3_ENDPOINT}';
RESTORE DATABASE nakama FROM LATEST IN 's3://${S3_BUCKET}?AWS_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}&AWS_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}&AWS_REGION=${S3_REGION}&AWS_ENDPOINT=${S3_ENDPOINT}';

CockroachDB follows a YY.R.PP year, release and patch versioning system. After each release, we should perform a backup before upgrading.


Eva Icons are being used in the front-end. Thank you.

Documentation

Index

Constants

View Source
const (
	MaxMediaItemBytes = 5 << 20  // 5MB
	MaxMediaBytes     = 15 << 20 // 15MB
)
View Source
const (
	// MaxAvatarBytes to read.
	MaxAvatarBytes = 5 << 20 // 5MB
	// MaxCoverBytes to read.
	MaxCoverBytes = 20 << 20 // 20MB

	AvatarsBucket = "avatars"
	CoversBucket  = "covers"
)
View Source
const KeyAuthUserID = ctxkey("auth_user_id")

KeyAuthUserID to use in context.

View Source
const MediaBucket = "media"

Variables

View Source
var (
	// ErrInvalidRedirectURI denotes an invalid redirect URI.
	ErrInvalidRedirectURI = InvalidArgumentError("invalid redirect URI")
	// ErrUntrustedRedirectURI denotes an untrusted redirect URI.
	// That is an URI that is not in the same host as the nakama.
	ErrUntrustedRedirectURI = PermissionDeniedError("untrusted redirect URI")
	// ErrInvalidToken denotes an invalid token.
	ErrInvalidToken = InvalidArgumentError("invalid token")
	// ErrExpiredToken denotes that the token already expired.
	ErrExpiredToken = UnauthenticatedError("expired token")
	// ErrInvalidVerificationCode denotes an invalid verification code.
	ErrInvalidVerificationCode = InvalidArgumentError("invalid verification code")
	// ErrVerificationCodeNotFound denotes a not found verification code.
	ErrVerificationCodeNotFound = NotFoundError("verification code not found")
)
View Source
var (
	// ErrInvalidCommentID denotes an invalid comment ID; that is not uuid.
	ErrInvalidCommentID = InvalidArgumentError("invalid comment ID")
	// ErrCommentNotFound denotes a not found comment.
	ErrCommentNotFound     = NotFoundError("comment not found")
	ErrUpdateCommentDenied = PermissionDeniedError("update comment denied")
)
View Source
var (
	// ErrInvalidPostID denotes an invalid post ID; that is not uuid.
	ErrInvalidPostID = InvalidArgumentError("invalid post ID")
	// ErrInvalidContent denotes an invalid content.
	ErrInvalidContent = InvalidArgumentError("invalid content")
	// ErrInvalidSpoiler denotes an invalid spoiler title.
	ErrInvalidSpoiler = InvalidArgumentError("invalid spoiler")
	// ErrPostNotFound denotes a not found post.
	ErrPostNotFound = NotFoundError("post not found")
	// ErrInvalidUpdatePostParams denotes invalid params to update a post, that is no params altogether.
	ErrInvalidUpdatePostParams = InvalidArgumentError("invalid update post params")
	// ErrInvalidCursor denotes an invalid cursor, that is not base64 encoded and has a key and timestamp separated by comma.
	ErrInvalidCursor = InvalidArgumentError("invalid cursor")
	// ErrInvalidReaction denotes an invalid reaction, that may by an invalid reaction type, or invalid reaction by itslef,
	// not a valid emoji, or invalid reaction image URL.
	ErrInvalidReaction  = InvalidArgumentError("invalid reaction")
	ErrUpdatePostDenied = PermissionDeniedError("update post denied")
)
View Source
var (
	// ErrInvalidTimelineItemID denotes an invalid timeline item id; that is not uuid.
	ErrInvalidTimelineItemID = InvalidArgumentError("invalid timeline item ID")
	// ErrUnsupportedMediaItemFormat denotes an unsupported media item format.
	ErrUnsupportedMediaItemFormat = InvalidArgumentError("unsupported media item format")
	ErrMediaItemTooLarge          = InvalidArgumentError("media item too large")
	ErrMediaTooLarge              = InvalidArgumentError("media too large")
)
View Source
var (
	// ErrInvalidUserID denotes an invalid user id; that is not uuid.
	ErrInvalidUserID = InvalidArgumentError("invalid user ID")
	// ErrInvalidEmail denotes an invalid email address.
	ErrInvalidEmail = InvalidArgumentError("invalid email")
	// ErrInvalidUsername denotes an invalid username.
	ErrInvalidUsername = InvalidArgumentError("invalid username")
	// ErrEmailTaken denotes an email already taken.
	ErrEmailTaken = AlreadyExistsError("email taken")
	// ErrUsernameTaken denotes a username already taken.
	ErrUsernameTaken = AlreadyExistsError("username taken")
	// ErrUserNotFound denotes a not found user.
	ErrUserNotFound = NotFoundError("user not found")
	// ErrForbiddenFollow denotes a forbiden follow. Like following yourself.
	ErrForbiddenFollow = PermissionDeniedError("forbidden follow")
	// ErrUnsupportedAvatarFormat denotes an unsupported avatar image format.
	ErrUnsupportedAvatarFormat = InvalidArgumentError("unsupported avatar format")
	// ErrUnsupportedCoverFormat denotes an unsupported avatar image format.
	ErrUnsupportedCoverFormat = InvalidArgumentError("unsupported cover format")
	// ErrUserGone denotes that the user has already been deleted.
	ErrUserGone = GoneError("user gone")
	// ErrInvalidUpdateUserParams denotes invalid params to update a user, that is no params altogether.
	ErrInvalidUpdateUserParams = InvalidArgumentError("invalid update user params")
	// ErrInvalidUserBio denotes an invalid user bio. That is empty or it exceeds the max allowed characters (480).
	ErrInvalidUserBio = InvalidArgumentError("invalid user bio")
	// ErrInvalidUserWaifu denotes an invalid waifu name for an user.
	ErrInvalidUserWaifu = InvalidArgumentError("invalid user waifu")
	// ErrInvalidUserHusbando denotes an invalid husbando name for an user.
	ErrInvalidUserHusbando = InvalidArgumentError("invalid user husbando")
)
View Source
var ErrAlreadyExists = errors.New("already exists")
View Source
var ErrGone = errors.New("gone")
View Source
var ErrInvalidArgument = errors.New("invalid argument")
View Source
var ErrInvalidNotificationID = InvalidArgumentError("invalid notification ID")

ErrInvalidNotificationID denotes an invalid notification id; that is not uuid.

View Source
var ErrNotFound = errors.New("not found")
View Source
var ErrPermissionDenied = errors.New("permission denied")
View Source
var ErrUnauthenticated = errors.New("unauthenticated")

ErrUnauthenticated denotes no authenticated user in context.

View Source
var ErrUnimplemented = errors.New("unimplemented")
View Source
var Schema string

Functions

func ValidUsername

func ValidUsername(s string) bool

Types

type AlreadyExistsError

type AlreadyExistsError string

func (AlreadyExistsError) Error

func (e AlreadyExistsError) Error() string

func (AlreadyExistsError) Unwrap

func (e AlreadyExistsError) Unwrap() error

type AuthOutput

type AuthOutput struct {
	User      User      `json:"user"`
	Token     string    `json:"token"`
	ExpiresAt time.Time `json:"expiresAt"`
}

AuthOutput response.

type Comment

type Comment struct {
	ID        string     `json:"id"`
	UserID    string     `json:"-"`
	PostID    string     `json:"-"`
	Content   string     `json:"content"`
	Reactions []Reaction `json:"reactions"`
	CreatedAt time.Time  `json:"createdAt"`
	User      *User      `json:"user,omitempty"`
	Mine      bool       `json:"mine"`
}

Comment model.

type Comments

type Comments []Comment

func (Comments) EndCursor

func (cc Comments) EndCursor() *string

type GoneError

type GoneError string

func (GoneError) Error

func (e GoneError) Error() string

func (GoneError) Unwrap

func (e GoneError) Unwrap() error

type InvalidArgumentError

type InvalidArgumentError string

func (InvalidArgumentError) Error

func (e InvalidArgumentError) Error() string

func (InvalidArgumentError) Unwrap

func (e InvalidArgumentError) Unwrap() error

type NotFoundError

type NotFoundError string

func (NotFoundError) Error

func (e NotFoundError) Error() string

func (NotFoundError) Unwrap

func (e NotFoundError) Unwrap() error

type Notification

type Notification struct {
	ID       string    `json:"id"`
	UserID   string    `json:"-"`
	Actors   []string  `json:"actors"`
	Type     string    `json:"type"`
	PostID   *string   `json:"postID,omitempty"`
	Read     bool      `json:"read"`
	IssuedAt time.Time `json:"issuedAt"`
}

Notification model.

type Notifications

type Notifications []Notification

func (Notifications) EndCursor

func (pp Notifications) EndCursor() *string

type PermissionDeniedError

type PermissionDeniedError string

func (PermissionDeniedError) Error

func (e PermissionDeniedError) Error() string

func (PermissionDeniedError) Unwrap

func (e PermissionDeniedError) Unwrap() error

type Post

type Post struct {
	ID            string     `json:"id"`
	UserID        string     `json:"-"`
	Content       string     `json:"content"`
	SpoilerOf     *string    `json:"spoilerOf"`
	NSFW          bool       `json:"nsfw"`
	Reactions     []Reaction `json:"reactions"`
	CommentsCount int        `json:"commentsCount"`
	MediaURLs     []string   `json:"mediaURLs"`
	CreatedAt     time.Time  `json:"createdAt"`
	UpdatedAt     time.Time  `json:"updatedAt"`
	User          *User      `json:"user,omitempty"`
	Mine          bool       `json:"mine"`
	Subscribed    bool       `json:"subscribed"`
}

Post model.

type Posts

type Posts []Post

func (Posts) EndCursor

func (pp Posts) EndCursor() *string

type PostsOpt

type PostsOpt func(*PostsOpts)

func PostsFromUser

func PostsFromUser(username string) PostsOpt

func PostsTagged

func PostsTagged(tag string) PostsOpt

type PostsOpts

type PostsOpts struct {
	Username *string
	Tag      *string
}

type ProvidedUser

type ProvidedUser struct {
	ID       string
	Email    string
	Username *string
}

type Reaction

type Reaction struct {
	Type     string `json:"type"`
	Reaction string `json:"reaction"`
	Count    uint64 `json:"count"`
	Reacted  *bool  `json:"reacted,omitempty"`
}

type ReactionInput

type ReactionInput struct {
	Type     string `json:"type"`
	Reaction string `json:"reaction"`
}
type SendMagicLink struct {
	UpdateEmail bool   `json:"updateEmail"`
	Email       string `json:"email"`
	RedirectURI string `json:"redirectURI"`
}

type Service

type Service struct {
	Logger           log.Logger
	DB               *sql.DB
	Sender           mailing.Sender
	Origin           *url.URL
	TokenKey         string
	PubSub           pubsub.PubSub
	Store            storage.Store
	AvatarURLPrefix  string
	CoverURLPrefix   string
	MediaURLPrefix   string
	DisabledDevLogin bool
	AllowedOrigins   []string
	VAPIDPrivateKey  string
	VAPIDPublicKey   string
	// contains filtered or unexported fields
}

Service contains the core business logic separated from the transport layer. You can use it to back a REST, gRPC or GraphQL API. You must call RunBackgroundJobs afterward.

func (*Service) AddWebPushSubscription

func (svc *Service) AddWebPushSubscription(ctx context.Context, sub webpush.Subscription) error

func (*Service) AuthUser

func (s *Service) AuthUser(ctx context.Context) (User, error)

AuthUser is the current authenticated user.

func (*Service) AuthUserIDFromToken

func (s *Service) AuthUserIDFromToken(token string) (string, error)

AuthUserIDFromToken decodes the token into a user ID.

func (*Service) CommentStream

func (s *Service) CommentStream(ctx context.Context, postID string) (<-chan Comment, error)

CommentStream to receive comments in realtime.

func (*Service) Comments

func (s *Service) Comments(ctx context.Context, postID string, last uint64, before *string) (Comments, error)

Comments from a post in descending order with backward pagination.

func (*Service) CreateComment

func (s *Service) CreateComment(ctx context.Context, postID string, content string) (Comment, error)

CreateComment on a post.

func (*Service) CreateTimelineItem

func (s *Service) CreateTimelineItem(ctx context.Context, content string, spoilerOf *string, nsfw bool, media []io.ReadSeeker) (TimelineItem, error)

CreateTimelineItem publishes a post to the user timeline and fan-outs it to his followers.

func (*Service) DeleteComment

func (s *Service) DeleteComment(ctx context.Context, commentID string) error

func (*Service) DeletePost

func (s *Service) DeletePost(ctx context.Context, postID string) error

func (*Service) DeleteTimelineItem

func (s *Service) DeleteTimelineItem(ctx context.Context, timelineItemID string) error

DeleteTimelineItem from the auth user timeline.

func (*Service) DevLogin

func (s *Service) DevLogin(ctx context.Context, email string) (AuthOutput, error)

DevLogin is a login for development purposes only. TODO: disable dev login on production.

func (*Service) Followees

func (s *Service) Followees(ctx context.Context, username string, first uint64, after *string) (UserProfiles, error)

Followees in ascending order with forward pagination.

func (*Service) Followers

func (s *Service) Followers(ctx context.Context, username string, first uint64, after *string) (UserProfiles, error)

Followers in ascending order with forward pagination.

func (*Service) HasUnreadNotifications

func (s *Service) HasUnreadNotifications(ctx context.Context) (bool, error)

HasUnreadNotifications checks if the authenticated user has any unread notification.

func (*Service) LoginFromProvider

func (svc *Service) LoginFromProvider(ctx context.Context, name string, providedUser ProvidedUser) (User, error)

func (*Service) MarkNotificationAsRead

func (s *Service) MarkNotificationAsRead(ctx context.Context, notificationID string) error

MarkNotificationAsRead sets a notification from the authenticated user as read.

func (*Service) MarkNotificationsAsRead

func (s *Service) MarkNotificationsAsRead(ctx context.Context) error

MarkNotificationsAsRead sets all notification from the authenticated user as read.

func (*Service) NotificationStream

func (s *Service) NotificationStream(ctx context.Context) (<-chan Notification, error)

NotificationStream to receive notifications in realtime.

func (*Service) Notifications

func (s *Service) Notifications(ctx context.Context, last uint64, before *string) (Notifications, error)

Notifications from the authenticated user in descending order with backward pagination.

func (*Service) ParseRedirectURI

func (s *Service) ParseRedirectURI(rawurl string) (*url.URL, error)

ParseRedirectURI the given redirect URI and validates it.

func (*Service) Post

func (s *Service) Post(ctx context.Context, postID string) (Post, error)

Post with the given ID.

func (*Service) PostStream

func (s *Service) PostStream(ctx context.Context) (<-chan Post, error)

PostStream to receive posts in realtime.

func (*Service) Posts

func (s *Service) Posts(ctx context.Context, last uint64, before *string, opts ...PostsOpt) (Posts, error)

Posts in descending order and with backward pagination. They can be filtered from a specific user by using `PostsFromUser` option in this late case, user field won't be populated. They can also be filtered by tag using `PostsTagged`.

func (s *Service) SendMagicLink(ctx context.Context, in SendMagicLink) error

SendMagicLink to login without passwords. Or to update and verify a new email address. A second endpoint GET /api/verify_magic_link?email&code&redirect_uri must exist.

func (*Service) Timeline

func (s *Service) Timeline(ctx context.Context, last uint64, before *string) (Timeline, error)

Timeline of the authenticated user in descending order and with backward pagination.

func (*Service) TimelineItemStream

func (s *Service) TimelineItemStream(ctx context.Context) (<-chan TimelineItem, error)

TimelineItemStream to receive timeline items in realtime.

func (*Service) ToggleCommentReaction

func (s *Service) ToggleCommentReaction(ctx context.Context, commentID string, in ReactionInput) ([]Reaction, error)

func (*Service) ToggleFollow

func (s *Service) ToggleFollow(ctx context.Context, username string) (ToggleFollowOutput, error)

ToggleFollow between two users.

func (*Service) TogglePostReaction

func (s *Service) TogglePostReaction(ctx context.Context, postID string, in ReactionInput) ([]Reaction, error)

func (*Service) TogglePostSubscription

func (s *Service) TogglePostSubscription(ctx context.Context, postID string) (ToggleSubscriptionOutput, error)

TogglePostSubscription so you can stop receiving notifications from a thread.

func (*Service) Token

func (s *Service) Token(ctx context.Context) (TokenOutput, error)

Token to authenticate requests.

func (*Service) UpdateAvatar

func (s *Service) UpdateAvatar(ctx context.Context, r io.ReadSeeker) (string, error)

UpdateAvatar of the authenticated user returning the new avatar URL. Please limit the reader before hand using MaxAvatarBytes.

func (*Service) UpdateComment

func (s *Service) UpdateComment(ctx context.Context, in UpdateComment) (UpdatedComment, error)

func (*Service) UpdateCover

func (s *Service) UpdateCover(ctx context.Context, r io.ReadSeeker) (string, error)

UpdateCover of the authenticated user returning the new cover URL. Please limit the reader before hand using MaxCoverBytes.

func (*Service) UpdatePost

func (s *Service) UpdatePost(ctx context.Context, postID string, params UpdatePost) (UpdatedPost, error)

func (*Service) UpdateUser

func (s *Service) UpdateUser(ctx context.Context, params UpdateUserParams) error

func (*Service) User

func (s *Service) User(ctx context.Context, username string) (UserProfile, error)

User with the given username.

func (*Service) Usernames

func (s *Service) Usernames(ctx context.Context, startingWith string, first uint64, after *string) (Usernames, error)

Usernames to autocomplete a mention box or something.

func (*Service) Users

func (s *Service) Users(ctx context.Context, search string, first uint64, after *string) (UserProfiles, error)

Users in ascending order with forward pagination and filtered by username.

func (s *Service) VerifyMagicLink(ctx context.Context, email, code string, username *string) (AuthOutput, error)

VerifyMagicLink checks whether the given email and verification code exists and issues a new auth token. If the user does not exists, it can create a new one with the given username.

type Timeline

type Timeline []TimelineItem

func (Timeline) EndCursor

func (tt Timeline) EndCursor() *string

type TimelineItem

type TimelineItem struct {
	ID     string `json:"timelineItemID"`
	UserID string `json:"-"`
	PostID string `json:"-"`
	*Post
}

TimelineItem model.

type ToggleFollowOutput

type ToggleFollowOutput struct {
	Following      bool `json:"following"`
	FollowersCount int  `json:"followersCount"`
}

ToggleFollowOutput response.

type ToggleSubscriptionOutput

type ToggleSubscriptionOutput struct {
	Subscribed bool `json:"subscribed"`
}

ToggleSubscriptionOutput response.

type TokenOutput

type TokenOutput struct {
	Token     string    `json:"token"`
	ExpiresAt time.Time `json:"expiresAt"`
}

TokenOutput response.

type UnauthenticatedError

type UnauthenticatedError string

func (UnauthenticatedError) Error

func (e UnauthenticatedError) Error() string

func (UnauthenticatedError) Unwrap

func (e UnauthenticatedError) Unwrap() error

type UnimplementedError

type UnimplementedError string

func (UnimplementedError) Error

func (e UnimplementedError) Error() string

func (UnimplementedError) Unwrap

func (e UnimplementedError) Unwrap() error

type UpdateComment

type UpdateComment struct {
	ID      string  `json:"-"`
	Content *string `json:"content"`
}

type UpdatePost

type UpdatePost struct {
	Content   *string `json:"content"`
	SpoilerOf *string `json:"spoilerOf"`
	NSFW      *bool   `json:"nsfw"`
}

func (UpdatePost) Empty

func (params UpdatePost) Empty() bool

type UpdateUserParams

type UpdateUserParams struct {
	Username *string `json:"username"`
	Bio      *string `json:"bio"`
	Waifu    *string `json:"waifu"`
	Husbando *string `json:"husbando"`
}

type UpdatedComment

type UpdatedComment struct {
	Content string `json:"content"`
}

type UpdatedPost

type UpdatedPost struct {
	Content   string    `json:"content"`
	SpoilerOf *string   `json:"spoilerOf"`
	NSFW      bool      `json:"nsfw"`
	UpdatedAt time.Time `json:"updatedAt"`
}

type User

type User struct {
	ID        string  `json:"id,omitempty"`
	Username  string  `json:"username"`
	AvatarURL *string `json:"avatarURL"`
}

User model.

type UserProfile

type UserProfile struct {
	User
	Email          string  `json:"email,omitempty"`
	CoverURL       *string `json:"coverURL"`
	Bio            *string `json:"bio"`
	Waifu          *string `json:"waifu"`
	Husbando       *string `json:"husbando"`
	FollowersCount int     `json:"followersCount"`
	FolloweesCount int     `json:"followeesCount"`
	Me             bool    `json:"me"`
	Following      bool    `json:"following"`
	Followeed      bool    `json:"followeed"`
}

UserProfile model.

type UserProfiles

type UserProfiles []UserProfile

func (UserProfiles) EndCursor

func (uu UserProfiles) EndCursor() *string

type Usernames

type Usernames []string

func (Usernames) EndCursor

func (uu Usernames) EndCursor() *string

Directories

Path Synopsis
cmd
fs
s3

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL