Merge branch 'main' of git.mstar.dev:mstar/linstrom
Some checks failed
/ test (push) Has been cancelled

This commit is contained in:
Melody Becker 2025-03-21 07:47:12 +01:00
commit 2c57f668e0
33 changed files with 1186 additions and 51 deletions

View file

@ -1,28 +1,50 @@
# Contribution Guide
Thank you for your interest in contributing to Linstrom! All contributors are welcome, regardless of their level of experience.
Thank you for your interest in contributing to Linstrom!
All contributors are welcome, regardless of their level of experience.
Unless it's primarely AI generated. Then you're going to be blocked immediately
and publicly shamed.
## Bug Reports
Use the [bug report issue template](https://git.mstar.dev/mstar/linstrom/issues/new?template=bug-report.md) to file a bug report. Please include a detailed description of the events leading up to the problem, your system configuration, and the program logs. If you're able to reproduce the bug reliably, attaching a debugger to the program, triggering it, and uploading the results would be very helpful.
Use the [bug report issue template](https://git.mstar.dev/mstar/linstrom/issues/new?template=bug-report.md)
to file a bug report. Please include a detailed description of the events
leading up to the problem, your system configuration, and the program logs.
If you're able to reproduce the bug reliably, attaching a debugger to the program,
triggering it, and uploading the results would be very helpful.
This section *should* tell you how to find your logs, attach the debugger, and do whatever else you need for a detailed bug report. But nobody filled it out. Attach a picture of Goatse to your bug reports until we fix this.
This section _should_ tell you how to find your logs, attach the debugger,
and do whatever else you need for a detailed bug report. But nobody filled it out.
Attach a picture of Goatse to your bug reports until we fix this.
## Feature Requests
Use the [feature request issue template](https://git.mstar.dev/mstar/linstrom/issues/new?template=suggestion.md) to suggest new features. Please note that we haven't replaced this placeholder text with the actual criteria we're looking for, which means you should spam us with utterly nonsensical ideas.
Use the [feature request issue template](https://git.mstar.dev/mstar/linstrom/issues/new?template=suggestion.md)
to suggest new features. Please note that we haven't replaced this placeholder
text with the actual criteria we're looking for, which means
you should spam us with utterly nonsensical ideas.
## Submitting Translations
Translation files are part of the project codebase, so you'll have to fork the repository and file a pull request (see [Contributing Code](CONTRIBUTING.md#contributing-code) below). You don't need any programming knowledge to edit the translation files, though.
Translation files are part of the project codebase, so you'll have to
fork the repository and file a pull request
(see [Contributing Code](CONTRIBUTING.md#contributing-code) below).
You don't need any programming knowledge to edit the translation files, though.
This should have been removed and replaced with a quick overview of where the files are and what translators need to do in order to edit them. Nobody did that, so think of this as a free pass to scream profanities into the issue tracker in your native language.
This should have been removed and replaced with a quick overview
of where the files are and what translators need to do in order to edit them.
Nobody did that, so think of this as a free pass to scream profanities
into the issue tracker in your native language.
## Contributing Code
### Forking
If you'd like to have a go at writing some code for Linstrom, fork the repository, then create a new branch with a name that describes the changes you're making. If there's a [relevant issue](https://git.mstar.dev/mstar/linstrom/issues), include the issue number in the branch name:
If you'd like to have a go at writing some code for Linstrom,
fork the repository, then create a new branch with a name that
describes the changes you're making. If there's a [relevant issue](https://git.mstar.dev/mstar/linstrom/issues),
include the issue number in the branch name:
```sh
git checkout -b 1337-prevent-computer-from-exploding
@ -30,10 +52,14 @@ git checkout -b 1337-prevent-computer-from-exploding
### Development Environment
The project utilises Go (version 1.23+) for almost everything and node/npm (version 18+) for building the frontend.
The go side also makes use of `go generate` for multiple things, primarely in the storage module
The project utilises Go (version 1.23+) for almost everything and
node/npm (version 18+) for building the frontend.
The go side also makes use of `go generate` for multiple things,
primarely in the storage module
We don't have a development environment, because nobody bothered to fill this out. Please add a new build system to the project specifically for your modifications. Bonus points if it's entirely nonsensical, like `npm` in a C project.
We don't have a development environment, because nobody bothered to fill this out.
Please add a new build system to the project specifically for your modifications.
Bonus points if it's entirely nonsensical, like `npm` in a C project.
### Code Style
@ -43,15 +69,18 @@ For anything node: uhhh, yolo, idk yet
### Pull Requests
Once your modifications are complete, you'll want to fetch the latest changes from this repository, rebase your branch, and publish your changes:
Once your modifications are complete, you'll want to fetch the latest changes
from this repository, rebase your branch, and publish your changes:
```sh
git remote add upstream https://git.mstar.dev/mstar/linstrom.git
git checkout main
git pull upstream main
git checkout 1337-prevent-computer-from-exploding
git rebase master
git rebase main
git push --set-upstream origin 1337-prevent-computer-from-exploding
```
Finally, you can [create a pull request](https://git.mstar.dev/mstar/linstrom/pulls). It might not get approved, or you might have to make some additional changes to your code - but don't give up!
Finally, you can [create a pull request](https://git.mstar.dev/mstar/linstrom/pulls).
It might not get approved, or you might have to make some additional changes
to your code - but don't give up!

View file

@ -1,5 +1,8 @@
Linstrom is a new social media server with the focus on providing users, moderators and admins alike
simple, yet powerful features to moderate the experience.
# Linstrom
Linstrom is a new social media server with the focus on providing
users, moderators and admins alike simple, yet powerful features
to moderate the experience.
## Federation
@ -20,3 +23,18 @@ And they all have different woes with their software.
I want to try and make a server that combines the strengths of various
project's tooling while also hopefully solving
some of the weaknesses.
## Permission system
All permissions operate on a role based system, similar to what Discord offers.
Accounts in power (admins and moderators) are only able to manipulate other accounts
via this role system. This prevents moderators from directly manipulating accounts,
such as changing their username, info fields or otherwise, while still providing
ample control over the activities these accounts can perform.
These same roles, while most powerful for local accounts, also apply to remote accounts
as much as realisticly possible. Preventing a login on a remote server for example
might not be possible, but blocking all inbound traffic from that account is.
However, roles are not only for moderators and admins to use.
Normal accounts can also make use of them, all be it in a more limited way.

17
auth/auth.go Normal file
View file

@ -0,0 +1,17 @@
package auth
import (
"git.mstar.dev/mstar/linstrom/storage"
"gorm.io/gorm"
)
type Authentication struct {
// For when in-depth access is needed
db *gorm.DB
// Primary method to acquire account data
store *storage.Storage
}
func NewAuth(db *gorm.DB, store *storage.Storage) *Authentication {
return &Authentication{db, store}
}

118
auth/checks.go Normal file
View file

@ -0,0 +1,118 @@
package auth
import (
"git.mstar.dev/mstar/goutils/sliceutils"
"git.mstar.dev/mstar/linstrom/storage"
"github.com/rs/zerolog/log"
)
// Can actorId read the account with targetId?
func (a *Authentication) CanReadAccount(actorId *string, targetId string) bool {
targetAccount, err := a.store.FindAccountById(targetId)
if err != nil {
if err == storage.ErrEntryNotFound {
return true
}
log.Error().
Err(err).
Str("account-id", targetId).
Msg("Failed to receive account for permission check")
return false
}
if actorId == nil {
// TODO: Decide if roles should have a field to declare an account as follow only/hidden
// and then check for that flag here
return true
}
roles, err := a.store.FindRolesByNames(targetAccount.Roles)
if err != nil {
log.Error().
Err(err).
Strs("role-names", targetAccount.Roles).
Msg("Failed to get roles for target account")
return false
}
combined := storage.CollapseRolesIntoOne(roles...)
if sliceutils.Contains(combined.BlockedUsers, *actorId) {
return false
}
return true
}
// Can actorId edit the account with targetId?
// If actorId is nil, it is assumed to be an anonymous user trying to edit the target account
// if targetId is nil, it is assumed that the actor is editing themselves
func (a *Authentication) CanEditAccount(actorId *string, targetId *string) bool {
// FIXME: This entire function feels wrong, idk
// Only the owner of an account should be able to edit said account's data
// But how do moderation actions play with this? Do they count as edit or as something separate?
if actorId == nil {
return false
}
if targetId == nil {
targetId = actorId
}
targetAccount, err := a.store.FindAccountById(*targetId)
if err != nil {
if err != storage.ErrEntryNotFound {
log.Error().
Err(err).
Str("target-id", *targetId).
Msg("Failed to receive account for permission checks")
}
return false
}
if targetId == actorId {
targetRoles, err := a.store.FindRolesByNames(targetAccount.Roles)
if err != nil {
log.Error().
Err(err).
Strs("role-names", targetAccount.Roles).
Msg("Failed to get roles from storage")
return false
}
combined := storage.CollapseRolesIntoOne(targetRoles...)
return *combined.CanLogin
} else {
return false
}
}
// Can actorId delete the account with targetId?
// If actorId is nil, it is assumed to be an anonymous user trying to delete the target account
// if targetId is nil, it is assumed that the actor is deleting themselves
func (a *Authentication) CanDeleteAccount(actorId *string, targetId *string) bool {
if actorId == nil {
return false
}
acc, err := a.store.FindAccountById(*actorId)
if err != nil {
// TODO: Logging
return false
}
roles, err := a.store.FindRolesByNames(acc.Roles)
if err != nil {
// TODO: Logging
return false
}
collapsed := storage.CollapseRolesIntoOne(roles...)
if targetId == nil {
return *collapsed.CanLogin
} else {
return *collapsed.CanDeleteAccounts
}
}
// Can actorId create a new post at all?
// Specific restrictions regarding the content are not checked
func (a *Authentication) CanCreatePost(actorId string) bool { return true }
// Ensures that a given post conforms with all roles attached to the author account.
// Returns the conforming note (or nil of it can't be changed to conform)
// and whether the note was changed
func (a *Authentication) EnsureNoteConformsWithRoles(note *storage.Note) (*storage.Note, bool) {
return note, false
}
// Does the given note conform with the roles attached to the author account?
func (a *Authentication) DoesNoteConform(note *storage.Note) bool { return true }

64
storage-new/gormLogger.go Normal file
View file

@ -0,0 +1,64 @@
package storage
import (
"context"
"time"
"github.com/rs/zerolog"
"gorm.io/gorm/logger"
)
type gormLogger struct {
logger zerolog.Logger
}
func newGormLogger(zerologger zerolog.Logger) *gormLogger {
return &gormLogger{zerologger}
}
func (g *gormLogger) LogMode(newLevel logger.LogLevel) logger.Interface {
switch newLevel {
case logger.Error:
g.logger = g.logger.Level(zerolog.ErrorLevel)
case logger.Warn:
g.logger = g.logger.Level(zerolog.WarnLevel)
case logger.Info:
g.logger = g.logger.Level(zerolog.InfoLevel)
case logger.Silent:
g.logger = g.logger.Level(zerolog.Disabled)
}
return g
}
func (g *gormLogger) Info(ctx context.Context, format string, args ...interface{}) {
g.logger.Info().Ctx(ctx).Msgf(format, args...)
}
func (g *gormLogger) Warn(ctx context.Context, format string, args ...interface{}) {
g.logger.Warn().Ctx(ctx).Msgf(format, args...)
}
func (g *gormLogger) Error(ctx context.Context, format string, args ...interface{}) {
g.logger.Error().Ctx(ctx).Msgf(format, args...)
}
func (g *gormLogger) Trace(
ctx context.Context,
begin time.Time,
fc func() (sql string, rowsAffected int64),
err error,
) {
sql, rowsAffected := fc()
g.logger.Trace().
Ctx(ctx).
Time("gorm-begin", begin).
Err(err).
Str("gorm-query", sql).
Int64("gorm-rows-affected", rowsAffected).
Send()
}
func (g *gormLogger) OverwriteLoggingLevel(new zerolog.Level) {
g.logger = g.logger.Level(new)
}
func (g *gormLogger) OverwriteLogger(new zerolog.Logger) {
g.logger = new
}

102
storage-new/migrations.go Normal file
View file

@ -0,0 +1,102 @@
package storage
import (
"fmt"
"strings"
"git.mstar.dev/mstar/goutils/sliceutils"
"gorm.io/gorm"
"git.mstar.dev/mstar/linstrom/storage-new/models"
)
func migrateTypes(db *gorm.DB) error {
if err := db.AutoMigrate(
&models.Emote{},
&models.MediaMetadata{},
&models.Note{},
&models.NoteToAttachment{},
&models.NoteToEmote{},
&models.NoteToPing{},
&models.NoteTag{},
&models.Reaction{},
&models.RemoteServer{},
&models.Role{},
&models.User{},
&models.UserAuthMethod{},
&models.UserBeings{},
&models.UserInfoField{},
&models.UserRelation{},
&models.UserRemoteLinks{},
&models.UserRole{},
&models.UserTag{},
); err != nil {
return fmt.Errorf("storage: automigrate structs: %w", err)
}
return nil
}
// Ensure the being enum exists for the user
func createBeingType(db *gorm.DB) error {
return migrateEnum(
db,
"being_type",
sliceutils.Map(models.AllBeings, func(t models.BeingType) string { return string(t) }),
)
}
func createAccountRelationType(db *gorm.DB) error {
return migrateEnum(
db,
"relation_type",
sliceutils.Map(
models.AllRelations,
func(t models.RelationType) string { return string(t) },
),
)
}
func createAccountAuthMethodType(db *gorm.DB) error {
return migrateEnum(
db,
"auth_method_type",
sliceutils.Map(
models.AllAuthMethods,
func(t models.AuthenticationMethodType) string { return string(t) },
),
)
}
func createRemoteServerSoftwareType(db *gorm.DB) error {
return migrateEnum(
db,
"server_software_type",
sliceutils.Map(
models.AllServerSoftwareTypes,
func(t models.ServerSoftwareType) string { return string(t) },
),
)
}
// Helper function for ensuring the existence of an enum with the given values
func migrateEnum(db *gorm.DB, name string, values []string) error {
if err := db.Exec("DROP TYPE IF EXISTS " + name).Error; err != nil {
return fmt.Errorf("storage: migrate %s: %w", name, err)
}
queryBuilder := strings.Builder{}
queryBuilder.WriteString("CREATE TYPE")
queryBuilder.WriteString(name)
queryBuilder.WriteString("AS ENUM (")
blen := len(values)
for i, btype := range values {
queryBuilder.WriteString("'" + string(btype) + "'")
// Append comma everywhere except last entry
if i+1 < blen {
queryBuilder.WriteString(",")
}
}
if err := db.Exec(queryBuilder.String()).Error; err != nil {
return fmt.Errorf("storage: migrate %s: %w", name, err)
}
return nil
}

View file

@ -0,0 +1,12 @@
package models
import "gorm.io/gorm"
type Emote struct {
gorm.Model
// Metadata MediaMetadata // `gorm:"foreignKey:MetadataId"`
MetadataId string
Name string
// Server RemoteServer // `gorm:"foreignKey:ServerId;references:ID"`
ServerId uint
}

View file

@ -0,0 +1,29 @@
package models
import (
"time"
"gorm.io/gorm"
)
type MediaMetadata struct {
ID string `gorm:"primarykey"` // The unique ID of this media file
CreatedAt time.Time // When this entry was created
UpdatedAt time.Time // When this entry was last updated
// When this entry was deleted (for soft deletions)
// Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to
// If not null, this entry is marked as deleted
DeletedAt gorm.DeletedAt `gorm:"index"`
OwnedBy string // Account id this media belongs to
Remote bool // whether the attachment is a remote one
// Where the media is stored. Url
Location string
Type string // What media type this is following mime types, eg image/png
// Name of the file
// Could be <emote-name>.png, <server-name>.webp for example. Or the name the file was uploaded with
Name string
// Alternative description of the media file's content
AltText string
// Whether the media is to be blurred by default
Blurred bool
}

View file

@ -0,0 +1,33 @@
package models
import (
"time"
"gorm.io/gorm"
)
// Data defined in extra structs:
// - Attachments: models.NoteToAttachment
// - Emotes: models.NoteToEmote
// - Pings: models.NoteToPing
// - Tags: models.NoteTag
type Note struct {
ID string `gorm:"primarykey"` // Make ID a string (uuid) for other implementations
CreatedAt time.Time // When this entry was created
UpdatedAt time.Time // When this entry was last updated
// When this entry was deleted (for soft deletions)
// Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to
// If not null, this entry is marked as deleted
DeletedAt gorm.DeletedAt `gorm:"index"`
// Creator Account // `gorm:"foreignKey:CreatorId;references:ID"` // Account that created the post
CreatorId string
Remote bool // Whether the note is originally a remote one and just "cached"
// Raw content of the note. So without additional formatting applied
// Might already have formatting applied beforehand from the origin server
RawContent string
ContentWarning *string // Content warnings of the note, if it contains any
RepliesTo *string // Url of the message this replies to
Quotes *string // url of the message this note quotes
AccessLevel NoteAccessLevel // Where to send this message to (public, home, followers, dm)
OriginServer string // Url of the origin server. Also the primary key for those
}

View file

@ -0,0 +1,28 @@
package models
import "database/sql/driver"
type NoteAccessLevel uint8
const (
// The note is intended for the public
NOTE_TARGET_PUBLIC NoteAccessLevel = 0
// The note is intended only for the home screen
// not really any idea what the difference is compared to public
// Maybe home notes don't show up on the server feed but still for everyone's home feed if it reaches them via follow or boost
NOTE_TARGET_HOME NoteAccessLevel = 1 << iota
// The note is intended only for followers
NOTE_TARGET_FOLLOWERS
// The note is intended only for a DM to one or more targets
NOTE_TARGET_DM
)
// Converts the NoteTarget value into a type the DB can use
func (n *NoteAccessLevel) Value() (driver.Value, error) {
return n, nil
}
func (ct *NoteAccessLevel) Scan(value any) error {
*ct = NoteAccessLevel(value.(uint8))
return nil
}

View file

@ -0,0 +1,6 @@
package models
type NoteToAttachment struct {
UserId string
AttachmentId string
}

View file

@ -0,0 +1,6 @@
package models
type NoteToEmote struct {
UserId string
EmoteId string
}

View file

@ -0,0 +1,6 @@
package models
type NoteToPing struct {
UserId string
PingTargetId string
}

View file

@ -0,0 +1,6 @@
package models
type NoteTag struct {
UserId string
Tag string
}

View file

@ -0,0 +1,10 @@
package models
import "gorm.io/gorm"
type Reaction struct {
gorm.Model
NoteId string
ReactorId string
EmoteId uint
}

View file

@ -0,0 +1,12 @@
package models
import "gorm.io/gorm"
type RemoteServer struct {
gorm.Model
ServerType ServerSoftwareType // What software the server is running. Useful for formatting
Domain string // `gorm:"primaryKey"` // Domain the server exists under. Additional primary key
Name string // What the server wants to be known as (usually same as url)
Icon string // ID of a media file
IsSelf bool // Whether this server is yours truly
}

View file

@ -0,0 +1,37 @@
package models
import (
"database/sql/driver"
)
type ServerSoftwareType string
const (
// Includes forks like glitch-soc, etc
ServerSoftwareMastodon = ServerSoftwareType("Mastodon")
// Includes forks like Ice Shrimp, Sharkey, Cutiekey, etc
ServerSoftwareMisskey = ServerSoftwareType("Misskey")
// Includes Akkoma
ServerSoftwarePlemora = ServerSoftwareType("Plemora")
// Wafrn is a new entry
ServerSoftwareWafrn = ServerSoftwareType("Wafrn")
// And of course, yours truly
ServerSoftwareLinstrom = ServerSoftwareType("Linstrom")
)
var AllServerSoftwareTypes = []ServerSoftwareType{
ServerSoftwareMastodon,
ServerSoftwareMisskey,
ServerSoftwarePlemora,
ServerSoftwareWafrn,
ServerSoftwareLinstrom,
}
func (r *ServerSoftwareType) Value() (driver.Value, error) {
return r, nil
}
func (ct *ServerSoftwareType) Scan(value any) error {
*ct = ServerSoftwareType(value.(string))
return nil
}

182
storage-new/models/Role.go Normal file
View file

@ -0,0 +1,182 @@
package models
import (
"gorm.io/gorm"
)
// Could I collapse all these go:generate command into more condensed ones?
// Yes
// Will I do that?
// No
// This is explicit in what is being done. And easier to understand
//go:generate go build -o RolesGenerator ../cmd/RolesGenerator/main.go
//go:generate ./RolesGenerator -input=roles.go -output=rolesUtil_generated.go
//go:generate rm RolesGenerator
//go:generate go build -o ApiGenerator ../cmd/RolesApiTypeGenerator/main.go
//go:generate ./ApiGenerator -input=roles.go -output=../server/apiLinstromTypes_generated.go
//go:generate rm ApiGenerator
//go:generate go build -o HelperGenerator ../cmd/RolesApiConverter/main.go
//go:generate ./HelperGenerator -input=roles.go -output=../server/apiLinstromTypeHelpers_generated.go
//go:generate rm HelperGenerator
//go:generate go build -o FrontendGenerator ../cmd/RolesFrontendGenerator/main.go
//go:generate ./FrontendGenerator -input=roles.go -output=../frontend-reactive/app/models/role.ts
//go:generate rm FrontendGenerator
// A role is, in concept, similar to how Discord handles roles
// Some permission can be either disallowed (&false), don't care (nil) or allowed (&true)
// Don't care just says to use the value from the next lower role where it is set
// Blocks are part of the user relations
type Role struct {
// TODO: More control options
// Extend upon whatever Masto, Akkoma and Misskey have
// Lots of details please
// --- Role metadata ---
// Include full db model stuff
gorm.Model
// Name of the role
Name string `gorm:"primaryKey;unique"`
// Priority of the role
// Lower priority gets applied first and thus overwritten by higher priority ones
// If two roles have the same priority, the order is undetermined and may be random
// Default priority for new roles is 1 to always overwrite default user
// And full admin has max priority possible
Priority uint32
// Whether this role is for a for a single user only (like custom, per user permissions in Discord)
// If yes, Name will be the id of the user in question
IsUserRole bool
// Whether this role is one built into Linstrom from the start or not
// Note: Built-in roles can't be modified
IsBuiltIn bool
// --- User permissions ---
CanSendMedia *bool // Local & remote
CanSendCustomEmotes *bool // Local & remote
CanSendCustomReactions *bool // Local & remote
CanSendPublicNotes *bool // Local & remote
CanSendLocalNotes *bool // Local & remote
CanSendFollowerOnlyNotes *bool // Local & remote
CanSendPrivateNotes *bool // Local & remote
CanSendReplies *bool // Local & remote
CanQuote *bool // Local only
CanBoost *bool // Local only
CanIncludeLinks *bool // Local & remote
CanIncludeSurvey *bool // Local
CanFederateFedi *bool // Local & remote
CanFederateBsky *bool // Local
CanChangeDisplayName *bool // Local
CanSubmitReports *bool // Local & remote
// If disabled, an account can no longer be interacted with. The owner can no longer change anything about it
// And the UI will show a notice (and maybe include that info in the AP data too)
// Only moderators and admins will be able to edit the account's roles
CanLogin *bool // Local
CanMentionOthers *bool // Local & remote
HasMentionCountLimit *bool // Local & remote
MentionLimit *uint32 // Local & remote
// CanViewBoosts *bool
// CanViewQuotes *bool
// CanViewMedia *bool
// CanViewCustomEmotes *bool
// --- Automod ---
AutoNsfwMedia *bool // Local & remote
AutoCwPosts *bool // Local & remote
AutoCwPostsText *string // Local & remote
ScanCreatedPublicNotes *bool // Local & remote
ScanCreatedLocalNotes *bool // Local & remote
ScanCreatedFollowerOnlyNotes *bool // Local & remote
ScanCreatedPrivateNotes *bool // Local & remote
// Blocks all interactions and federation between users with the role and all included ids/handles
// TODO: Decide whether this is a list of handles or of account ids
// Handles would increase the load due to having to search for them first
// while ids would require to store every single account mentioned
// which could cause escalating storage costs
DisallowInteractionsWith []string `gorm:"type:bytes;serializer:gob"` // Local & remote
WithholdNotesForManualApproval *bool // Local & remote
WithholdNotesBasedOnRegex *bool // Local & remote
WithholdNotesRegexes []string `gorm:"type:bytes;serializer:gob"` // Local & remote
// --- Admin perms ---
// If set, counts as all permissions being set as given and all restrictions being disabled
FullAdmin *bool // Local
CanAffectOtherAdmins *bool // Local
CanDeleteNotes *bool // Local
CanConfirmWithheldNotes *bool // Local
CanAssignRoles *bool // Local
CanSupressInteractionsBetweenUsers *bool // Local
CanOverwriteDisplayNames *bool // Local
CanManageCustomEmotes *bool // Local
CanViewDeletedNotes *bool // Local
CanRecoverDeletedNotes *bool // Local
CanManageAvatarDecorations *bool // Local
CanManageAds *bool // Local
CanSendAnnouncements *bool // Local
CanDeleteAccounts *bool // Local
}
/*
Mastodon permissions (highest permission to lowest):
- Admin
- Devops
- View Audit log
- View Dashboard
- Manage Reports
- Manage Federation
- Manage Settings
- Manage Blocks
- Manage Taxonomies
- Manage Appeals
- Manage Users
- Manage Invites
- Manage Rules
- Manage Announcements
- Manage Custom Emojis
- Manage Webhooks
- Invite Users
- Manage Roles
- Manage User Access
- Delete User Data
*/
/*
Misskey "permissions" (no order):
- Global timeline available (interact with global timeline I think)
- Local timeline available (same as global, but for local)
- b-something timeline available
- Can send public notes
- How many mentions a note can have
- Can invite others
- How many invites can be sent
- InviteLimitCycle (whatever that means)
- Invite Expiration time (duration of how long invites stay valid I think)
- Manage custom emojis
- Manage custom avatar decorations
- Seach for notes
- Use translator
- Hide ads from self
- How much storage space the user has
- Whether to mark all posts from account as nsfw
- max number of pinned messages
- max number of antennas
- max number of muted words
- max number of webhooks
- max number of clips
- max number of notes contained in a clip (? I think. Don't know enough about clips)
- Max number of lists of users
- max number of users in a user list
- rate limit multiplier
- max number of applied avatar decorations
*/

View file

@ -0,0 +1,240 @@
package models
import (
"math"
"git.mstar.dev/mstar/goutils/other"
)
// Default role every user has. Defines sane defaults for a normal user
// Will get overwritten by just about every other role due to every other role having higher priority
var DefaultUserRole = Role{
Name: "Default",
Priority: 0,
IsUserRole: false,
IsBuiltIn: true,
CanSendMedia: other.IntoPointer(true),
CanSendCustomEmotes: other.IntoPointer(true),
CanSendCustomReactions: other.IntoPointer(true),
CanSendPublicNotes: other.IntoPointer(true),
CanSendLocalNotes: other.IntoPointer(true),
CanSendFollowerOnlyNotes: other.IntoPointer(true),
CanSendPrivateNotes: other.IntoPointer(true),
CanSendReplies: other.IntoPointer(true),
CanQuote: other.IntoPointer(true),
CanBoost: other.IntoPointer(true),
CanIncludeLinks: other.IntoPointer(true),
CanIncludeSurvey: other.IntoPointer(true),
CanFederateFedi: other.IntoPointer(true),
CanFederateBsky: other.IntoPointer(true),
CanChangeDisplayName: other.IntoPointer(true),
CanSubmitReports: other.IntoPointer(true),
CanLogin: other.IntoPointer(true),
CanMentionOthers: other.IntoPointer(true),
HasMentionCountLimit: other.IntoPointer(false),
MentionLimit: other.IntoPointer(
uint32(math.MaxUint32),
), // Set this to max, even if not used due to *HasMentionCountLimit == false
AutoNsfwMedia: other.IntoPointer(false),
AutoCwPosts: other.IntoPointer(false),
AutoCwPostsText: nil,
WithholdNotesForManualApproval: other.IntoPointer(false),
ScanCreatedPublicNotes: other.IntoPointer(false),
ScanCreatedLocalNotes: other.IntoPointer(false),
ScanCreatedFollowerOnlyNotes: other.IntoPointer(false),
ScanCreatedPrivateNotes: other.IntoPointer(false),
DisallowInteractionsWith: []string{},
FullAdmin: other.IntoPointer(false),
CanAffectOtherAdmins: other.IntoPointer(false),
CanDeleteNotes: other.IntoPointer(false),
CanConfirmWithheldNotes: other.IntoPointer(false),
CanAssignRoles: other.IntoPointer(false),
CanSupressInteractionsBetweenUsers: other.IntoPointer(false),
CanOverwriteDisplayNames: other.IntoPointer(false),
CanManageCustomEmotes: other.IntoPointer(false),
CanViewDeletedNotes: other.IntoPointer(false),
CanRecoverDeletedNotes: other.IntoPointer(false),
CanManageAvatarDecorations: other.IntoPointer(false),
CanManageAds: other.IntoPointer(false),
CanSendAnnouncements: other.IntoPointer(false),
}
// Role providing maximum permissions
var FullAdminRole = Role{
Name: "fullAdmin",
Priority: math.MaxUint32,
IsUserRole: false,
IsBuiltIn: true,
CanSendMedia: other.IntoPointer(true),
CanSendCustomEmotes: other.IntoPointer(true),
CanSendCustomReactions: other.IntoPointer(true),
CanSendPublicNotes: other.IntoPointer(true),
CanSendLocalNotes: other.IntoPointer(true),
CanSendFollowerOnlyNotes: other.IntoPointer(true),
CanSendPrivateNotes: other.IntoPointer(true),
CanQuote: other.IntoPointer(true),
CanBoost: other.IntoPointer(true),
CanIncludeLinks: other.IntoPointer(true),
CanIncludeSurvey: other.IntoPointer(true),
CanChangeDisplayName: other.IntoPointer(true),
CanSubmitReports: other.IntoPointer(true),
CanLogin: other.IntoPointer(true),
CanMentionOthers: other.IntoPointer(true),
HasMentionCountLimit: other.IntoPointer(false),
MentionLimit: other.IntoPointer(
uint32(math.MaxUint32),
), // Set this to max, even if not used due to *HasMentionCountLimit == false
AutoNsfwMedia: other.IntoPointer(false),
AutoCwPosts: other.IntoPointer(false),
AutoCwPostsText: nil,
WithholdNotesForManualApproval: other.IntoPointer(false),
ScanCreatedPublicNotes: other.IntoPointer(false),
ScanCreatedLocalNotes: other.IntoPointer(false),
ScanCreatedFollowerOnlyNotes: other.IntoPointer(false),
ScanCreatedPrivateNotes: other.IntoPointer(false),
DisallowInteractionsWith: []string{},
FullAdmin: other.IntoPointer(true),
CanAffectOtherAdmins: other.IntoPointer(true),
CanDeleteNotes: other.IntoPointer(true),
CanConfirmWithheldNotes: other.IntoPointer(true),
CanAssignRoles: other.IntoPointer(true),
CanSupressInteractionsBetweenUsers: other.IntoPointer(true),
CanOverwriteDisplayNames: other.IntoPointer(true),
CanManageCustomEmotes: other.IntoPointer(true),
CanViewDeletedNotes: other.IntoPointer(true),
CanRecoverDeletedNotes: other.IntoPointer(true),
CanManageAvatarDecorations: other.IntoPointer(true),
CanManageAds: other.IntoPointer(true),
CanSendAnnouncements: other.IntoPointer(true),
}
// Role for totally freezing an account, blocking all activity from it
var AccountFreezeRole = Role{
Name: "accountFreeze",
Priority: math.MaxUint32 - 1,
IsUserRole: false,
IsBuiltIn: true,
CanSendMedia: other.IntoPointer(false),
CanSendCustomEmotes: other.IntoPointer(false),
CanSendCustomReactions: other.IntoPointer(false),
CanSendPublicNotes: other.IntoPointer(false),
CanSendLocalNotes: other.IntoPointer(false),
CanSendFollowerOnlyNotes: other.IntoPointer(false),
CanSendPrivateNotes: other.IntoPointer(false),
CanSendReplies: other.IntoPointer(false),
CanQuote: other.IntoPointer(false),
CanBoost: other.IntoPointer(false),
CanIncludeLinks: other.IntoPointer(false),
CanIncludeSurvey: other.IntoPointer(false),
CanFederateBsky: other.IntoPointer(false),
CanFederateFedi: other.IntoPointer(false),
CanChangeDisplayName: other.IntoPointer(false),
CanSubmitReports: other.IntoPointer(false),
CanLogin: other.IntoPointer(false),
CanMentionOthers: other.IntoPointer(false),
HasMentionCountLimit: other.IntoPointer(false),
MentionLimit: other.IntoPointer(
uint32(math.MaxUint32),
), // Set this to max, even if not used due to *HasMentionCountLimit == false
AutoNsfwMedia: other.IntoPointer(true),
AutoCwPosts: other.IntoPointer(false),
AutoCwPostsText: other.IntoPointer("Account frozen"),
WithholdNotesForManualApproval: other.IntoPointer(true),
ScanCreatedPublicNotes: other.IntoPointer(false),
ScanCreatedLocalNotes: other.IntoPointer(false),
ScanCreatedFollowerOnlyNotes: other.IntoPointer(false),
ScanCreatedPrivateNotes: other.IntoPointer(false),
DisallowInteractionsWith: []string{},
FullAdmin: other.IntoPointer(false),
CanAffectOtherAdmins: other.IntoPointer(false),
CanDeleteNotes: other.IntoPointer(false),
CanConfirmWithheldNotes: other.IntoPointer(false),
CanAssignRoles: other.IntoPointer(false),
CanSupressInteractionsBetweenUsers: other.IntoPointer(false),
CanOverwriteDisplayNames: other.IntoPointer(false),
CanManageCustomEmotes: other.IntoPointer(false),
CanViewDeletedNotes: other.IntoPointer(false),
CanRecoverDeletedNotes: other.IntoPointer(false),
CanManageAvatarDecorations: other.IntoPointer(false),
CanManageAds: other.IntoPointer(false),
CanSendAnnouncements: other.IntoPointer(false),
}
var ServerActorRole = Role{
Name: "ServerActor",
Priority: math.MaxUint32,
IsUserRole: true,
IsBuiltIn: true,
CanSendMedia: other.IntoPointer(true),
CanSendCustomEmotes: other.IntoPointer(true),
CanSendCustomReactions: other.IntoPointer(true),
CanSendPublicNotes: other.IntoPointer(true),
CanSendLocalNotes: other.IntoPointer(true),
CanSendFollowerOnlyNotes: other.IntoPointer(true),
CanSendPrivateNotes: other.IntoPointer(true),
CanQuote: other.IntoPointer(true),
CanBoost: other.IntoPointer(true),
CanIncludeLinks: other.IntoPointer(true),
CanIncludeSurvey: other.IntoPointer(true),
CanChangeDisplayName: other.IntoPointer(true),
CanSubmitReports: other.IntoPointer(true),
CanLogin: other.IntoPointer(true),
CanMentionOthers: other.IntoPointer(true),
HasMentionCountLimit: other.IntoPointer(false),
MentionLimit: other.IntoPointer(
uint32(math.MaxUint32),
), // Set this to max, even if not used due to *HasMentionCountLimit == false
AutoNsfwMedia: other.IntoPointer(false),
AutoCwPosts: other.IntoPointer(false),
AutoCwPostsText: nil,
WithholdNotesForManualApproval: other.IntoPointer(false),
ScanCreatedPublicNotes: other.IntoPointer(false),
ScanCreatedLocalNotes: other.IntoPointer(false),
ScanCreatedFollowerOnlyNotes: other.IntoPointer(false),
ScanCreatedPrivateNotes: other.IntoPointer(false),
DisallowInteractionsWith: []string{},
FullAdmin: other.IntoPointer(true),
CanAffectOtherAdmins: other.IntoPointer(true),
CanDeleteNotes: other.IntoPointer(true),
CanConfirmWithheldNotes: other.IntoPointer(true),
CanAssignRoles: other.IntoPointer(true),
CanSupressInteractionsBetweenUsers: other.IntoPointer(true),
CanOverwriteDisplayNames: other.IntoPointer(true),
CanManageCustomEmotes: other.IntoPointer(true),
CanViewDeletedNotes: other.IntoPointer(true),
CanRecoverDeletedNotes: other.IntoPointer(true),
CanManageAvatarDecorations: other.IntoPointer(true),
CanManageAds: other.IntoPointer(true),
CanSendAnnouncements: other.IntoPointer(true),
}
var allDefaultRoles = []*Role{
&DefaultUserRole,
&FullAdminRole,
&AccountFreezeRole,
&ServerActorRole,
}

View file

@ -0,0 +1,53 @@
package models
import (
"time"
"gorm.io/gorm"
)
// Data stored in external types:
// - Custom info fields
// - Being types
// - Tags
// - Relations
// - Pronouns
// - Roles
// - AP remote links
// - Auth methods and tokens (hashed pw, totp key, passkey id)
type User struct {
ID string `gorm:"primarykey"` // ID is a uuid for this account
// Username of the user (eg "max" if the full username is @max@example.com)
// Assume unchangable (once set by a user) to be kind to other implementations
// Would be an easy avenue to fuck with them though
Username string `gorm:"unique"`
CreatedAt time.Time // When this entry was created. Automatically set by gorm
// When this account was last updated. Will also be used for refreshing remote accounts. Automatically set by gorm
UpdatedAt time.Time
// When this entry was deleted (for soft deletions)
// Soft delete means that this entry still exists in the db, but gorm won't include it anymore unless specifically told to
// If not null, this entry is marked as deleted
DeletedAt gorm.DeletedAt `gorm:"index"`
// Server RemoteServer // `gorm:"foreignKey:ServerId;references:ID"` // The server this user is from
ServerId uint // Id of the server this user is from, needed for including RemoteServer
DisplayName string // The display name of the user. Can be different from the handle
Description string // The description of a user account
IsBot bool // Whether to mark this account as a script controlled one
Icon string // ID of a media file used as icon
Background *string // ID of a media file used as background image
Banner *string // ID of a media file used as banner
Indexable bool // Whether this account can be found by crawlers
PublicKey []byte // The public key of the account
// Whether this account restricts following
// If true, the owner must approve of a follow request first
RestrictedFollow bool
Location *string
Birthday *time.Time
// --- And internal account stuff ---
// Whether the account got verified and is allowed to be active
// For local accounts being active means being allowed to login and perform interactions
// For remote users, if an account is not verified, any interactions it sends are discarded
Verified bool
}

View file

@ -0,0 +1,7 @@
package models
type UserAuthMethod struct {
UserId string
AuthMethod AuthenticationMethodType `gorm:"type:auth_method_type"`
Token []byte
}

View file

@ -0,0 +1,26 @@
package models
import "database/sql/driver"
type AuthenticationMethodType string
const (
AuthMethodPassword AuthenticationMethodType = "password"
AuthMethodGAuth AuthenticationMethodType = "g-auth" // Google Authenticator / totp
AuthMethodMail AuthenticationMethodType = "mail"
AuthMethodPasskey2fa AuthenticationMethodType = "passkey-2fa" // Passkey used as 2fa factor
AuthMethodPasskey AuthenticationMethodType = "passkey" // Passkey as only auth key
)
var AllAuthMethods = []AuthenticationMethodType{
AuthMethodPassword, AuthMethodGAuth, AuthMethodMail, AuthMethodPasskey, AuthMethodPasskey2fa,
}
func (ct *AuthenticationMethodType) Scan(value any) error {
*ct = AuthenticationMethodType(value.([]byte))
return nil
}
func (ct AuthenticationMethodType) Value() (driver.Value, error) {
return string(ct), nil
}

View file

@ -0,0 +1,27 @@
package models
import (
"database/sql/driver"
)
type BeingType string
const (
BEING_HUMAN = BeingType("human")
BEING_CAT = BeingType("cat")
BEING_FOX = BeingType("fox")
BEING_DOG = BeingType("dog")
BEING_ROBOT = BeingType("robot")
BEING_DOLL = BeingType("doll")
)
var AllBeings = []BeingType{BEING_HUMAN, BEING_CAT, BEING_FOX, BEING_DOG, BEING_ROBOT, BEING_DOLL}
func (ct *BeingType) Scan(value any) error {
*ct = BeingType(value.([]byte))
return nil
}
func (ct BeingType) Value() (driver.Value, error) {
return string(ct), nil
}

View file

@ -0,0 +1,6 @@
package models
type UserBeings struct {
UserId string
Being BeingType `gorm:"type:being_type"`
}

View file

@ -0,0 +1,19 @@
package models
import (
"time"
"gorm.io/gorm"
)
type UserInfoField struct {
gorm.Model // Can actually just embed this as is here as those are not something directly exposed :3
Name string
Value string
LastUrlCheckDate *time.Time // Used if the value is an url to somewhere. Empty if value is not an url
// If the value is an url, this attribute indicates whether Linstrom was able to verify ownership
// of the provided url via the common method of
// "Does the target url contain a rel='me' link to the owner's account"
Confirmed bool
UserId string // Id of account this info field belongs to
}

View file

@ -0,0 +1,7 @@
package models
type UserRelation struct {
UserId string
TargetUserId string
Relation RelationType `gorm:"type:relation_type"`
}

View file

@ -0,0 +1,31 @@
package models
import "database/sql/driver"
type RelationType string
const (
RelationFollow RelationType = "follow"
RelationMute RelationType = "mute"
RelationNoBoosts RelationType = "no-boosts"
RelationBlock RelationType = "block"
RelationPreventFollow RelationType = "prevent-follow"
)
// var AllBeings = []BeingType{BEING_HUMAN, BEING_CAT, BEING_FOX, BEING_DOG, BEING_ROBOT, BEING_DOLL}
var AllRelations = []RelationType{
RelationFollow,
RelationMute,
RelationNoBoosts,
RelationBlock,
RelationPreventFollow,
}
func (ct *RelationType) Scan(value any) error {
*ct = RelationType(value.([]byte))
return nil
}
func (ct RelationType) Value() (driver.Value, error) {
return string(ct), nil
}

View file

@ -0,0 +1,21 @@
package models
import "gorm.io/gorm"
type UserRemoteLinks struct {
// ---- Section: gorm
// Sets this struct up as a value that an Account may have
gorm.Model
UserId string
// Just about every link here is optional to accomodate for servers with only minimal accounts
// Minimal being handle, ap link and inbox
ApLink string
ViewLink *string
FollowersLink *string
FollowingLink *string
InboxLink string
OutboxLink *string
FeaturedLink *string
FeaturedTagsLink *string
}

View file

@ -0,0 +1,6 @@
package models
type UserRole struct {
UserId string
RoleId uint
}

View file

@ -0,0 +1,6 @@
package models
type UserTag struct {
UserId string
Tag string
}

1
storage-new/storage.go Normal file
View file

@ -0,0 +1 @@
package storage

View file

@ -1,9 +1,9 @@
package storage
import (
"github.com/rs/zerolog/log"
"git.mstar.dev/mstar/goutils/sliceutils"
"git.mstar.dev/mstar/linstrom/util"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
@ -80,7 +80,10 @@ type Role struct {
// Internal ids of accounts blocked by this role
BlockedUsers []string `gorm:"type:bytes;serializer:gob"` // Local
CanSubmitReports *bool // Local & remote
CanLogin *bool // Local
// If disabled, an account can no longer be interacted with. The owner can no longer change anything about it
// And the UI will show a notice (and maybe include that info in the AP data too)
// Only moderators and admins will be able to edit the account's roles
CanLogin *bool // Local
CanMentionOthers *bool // Local & remote
HasMentionCountLimit *bool // Local & remote
@ -125,6 +128,7 @@ type Role struct {
CanManageAvatarDecorations *bool // Local
CanManageAds *bool // Local
CanSendAnnouncements *bool // Local
CanDeleteAccounts *bool // Local
}
/*

View file

@ -1,34 +0,0 @@
package storage
type AccountRestriction int64
const (
// Account has no restrictions applied
ACCOUNT_RESTRICTION_NONE = AccountRestriction(0)
// All messages of the account get a content warning applied if none is set
// Warning could be something like "Message auto-CWd by server"
ACCOUNT_RESTRICTION_AUTO_CW = AccountRestriction(1 << iota)
// Disable accessing the account via login or access token
ACCOUNT_RESTRICTION_DISABLE_LOGIN
// Disable sending activities to other servers if the account is local
// Or reject all activities if the account is remote
ACCOUNT_RESTRICTION_NO_FEDERATION
// Disallow sending direct messages from that account
ACCOUNT_RESTRICTION_NO_DMS
// Disallow sending follower only messages from that account
ACCOUNT_RESTRICTION_NO_FOLLOWER_POSTS
// Disable outbound follow requests (restricted account can't send follow requests)
ACCOUNT_RESTRICTION_DISABLE_OUTBOUND_FOLLOWS
// Disable inbound follow requests (all follow requests to that account are automatically rejected)
ACCOUNT_RESTRICTION_DISABLE_INBOUND_FOLLOWS
// Forces all posts by that account to be follower only
ACCOUNT_RESTRICTION_FORCE_FOLLOWERS_ONLY
// Disable all outbound activities of an account.
// Includes sending, updating or deleting own notes
// as well as boosting or reacting to any notes
ACCOUNT_RESTRICTION_DISABLE_ACTIVITIES
// Account can only be viewed while logged in or via verified requests to the AP endpoints
ACCOUNT_RESTRICTIONS_NO_PUBLIC_ACCESS
// Force tag all media the account posts as sensitive
ACCOUNT_RESTRICTIONS_FORCE_MEDIA_SENSITIVE
)