[feature] scheduled statuses (#4274)

An implementation of [`scheduled_statuses`](https://docs.joinmastodon.org/methods/scheduled_statuses/). Will fix #1006.

this is heavily WIP and I need to reorganize some of the code, working on this made me somehow familiar with the codebase and led to my other recent contributions
i told some fops on fedi i'd work on this so i have no choice but to complete it 🤷‍♀️
btw iirc my avatar presents me working on this branch

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4274
Co-authored-by: nicole mikołajczyk <git@mkljczk.pl>
Co-committed-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk 2025-08-12 14:05:15 +02:00 committed by kim
commit 660cf2c94c
46 changed files with 2354 additions and 68 deletions

View file

@ -131,6 +131,9 @@ type Configuration struct {
StatusesPollOptionMaxChars int `name:"statuses-poll-option-max-chars" usage:"Max amount of characters for a poll option"`
StatusesMediaMaxFiles int `name:"statuses-media-max-files" usage:"Maximum number of media files/attachments per status"`
ScheduledStatusesMaxTotal int `name:"scheduled-statuses-max-total" usage:"Maximum number of scheduled statuses per user"`
ScheduledStatusesMaxDaily int `name:"scheduled-statuses-max-daily" usage:"Maximum number of scheduled statuses per user for a single day"`
LetsEncryptEnabled bool `name:"letsencrypt-enabled" usage:"Enable letsencrypt TLS certs for this server. If set to true, then cert dir also needs to be set (or take the default)."`
LetsEncryptPort int `name:"letsencrypt-port" usage:"Port to listen on for letsencrypt certificate challenges. Must not be the same as the GtS webserver/API port."`
LetsEncryptCertDir string `name:"letsencrypt-cert-dir" usage:"Directory to store acquired letsencrypt certificates."`
@ -252,6 +255,7 @@ type CacheConfiguration struct {
PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"`
PollVoteIDsMemRatio float64 `name:"poll-vote-ids-mem-ratio"`
ReportMemRatio float64 `name:"report-mem-ratio"`
ScheduledStatusMemRatio float64 `name:"scheduled-status-mem-ratio"`
SinBinStatusMemRatio float64 `name:"sin-bin-status-mem-ratio"`
StatusMemRatio float64 `name:"status-mem-ratio"`
StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"`

View file

@ -105,6 +105,9 @@ var Defaults = Configuration{
StatusesPollOptionMaxChars: 50,
StatusesMediaMaxFiles: 6,
ScheduledStatusesMaxTotal: 300,
ScheduledStatusesMaxDaily: 25,
LetsEncryptEnabled: false,
LetsEncryptPort: 80,
LetsEncryptCertDir: "/gotosocial/storage/certs",
@ -217,6 +220,7 @@ var Defaults = Configuration{
PollVoteMemRatio: 2,
PollVoteIDsMemRatio: 2,
ReportMemRatio: 1,
ScheduledStatusMemRatio: 4,
SinBinStatusMemRatio: 0.5,
StatusMemRatio: 5,
StatusBookmarkMemRatio: 0.5,

View file

@ -99,6 +99,8 @@ const (
StatusesPollMaxOptionsFlag = "statuses-poll-max-options"
StatusesPollOptionMaxCharsFlag = "statuses-poll-option-max-chars"
StatusesMediaMaxFilesFlag = "statuses-media-max-files"
ScheduledStatusesMaxTotalFlag = "scheduled-statuses-max-total"
ScheduledStatusesMaxDailyFlag = "scheduled-statuses-max-daily"
LetsEncryptEnabledFlag = "letsencrypt-enabled"
LetsEncryptPortFlag = "letsencrypt-port"
LetsEncryptCertDirFlag = "letsencrypt-cert-dir"
@ -194,6 +196,7 @@ const (
CachePollVoteMemRatioFlag = "cache-poll-vote-mem-ratio"
CachePollVoteIDsMemRatioFlag = "cache-poll-vote-ids-mem-ratio"
CacheReportMemRatioFlag = "cache-report-mem-ratio"
CacheScheduledStatusMemRatioFlag = "cache-scheduled-status-mem-ratio"
CacheSinBinStatusMemRatioFlag = "cache-sin-bin-status-mem-ratio"
CacheStatusMemRatioFlag = "cache-status-mem-ratio"
CacheStatusBookmarkMemRatioFlag = "cache-status-bookmark-mem-ratio"
@ -296,6 +299,8 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
flags.Int("statuses-poll-max-options", cfg.StatusesPollMaxOptions, "Max amount of options permitted on a poll")
flags.Int("statuses-poll-option-max-chars", cfg.StatusesPollOptionMaxChars, "Max amount of characters for a poll option")
flags.Int("statuses-media-max-files", cfg.StatusesMediaMaxFiles, "Maximum number of media files/attachments per status")
flags.Int("scheduled-statuses-max-total", cfg.ScheduledStatusesMaxTotal, "Maximum number of scheduled statuses per user")
flags.Int("scheduled-statuses-max-daily", cfg.ScheduledStatusesMaxDaily, "Maximum number of scheduled statuses per user for a single day")
flags.Bool("letsencrypt-enabled", cfg.LetsEncryptEnabled, "Enable letsencrypt TLS certs for this server. If set to true, then cert dir also needs to be set (or take the default).")
flags.Int("letsencrypt-port", cfg.LetsEncryptPort, "Port to listen on for letsencrypt certificate challenges. Must not be the same as the GtS webserver/API port.")
flags.String("letsencrypt-cert-dir", cfg.LetsEncryptCertDir, "Directory to store acquired letsencrypt certificates.")
@ -391,6 +396,7 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
flags.Float64("cache-poll-vote-mem-ratio", cfg.Cache.PollVoteMemRatio, "")
flags.Float64("cache-poll-vote-ids-mem-ratio", cfg.Cache.PollVoteIDsMemRatio, "")
flags.Float64("cache-report-mem-ratio", cfg.Cache.ReportMemRatio, "")
flags.Float64("cache-scheduled-status-mem-ratio", cfg.Cache.ScheduledStatusMemRatio, "")
flags.Float64("cache-sin-bin-status-mem-ratio", cfg.Cache.SinBinStatusMemRatio, "")
flags.Float64("cache-status-mem-ratio", cfg.Cache.StatusMemRatio, "")
flags.Float64("cache-status-bookmark-mem-ratio", cfg.Cache.StatusBookmarkMemRatio, "")
@ -414,7 +420,7 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
}
func (cfg *Configuration) MarshalMap() map[string]any {
cfgmap := make(map[string]any, 194)
cfgmap := make(map[string]any, 197)
cfgmap["log-level"] = cfg.LogLevel
cfgmap["log-format"] = cfg.LogFormat
cfgmap["log-timestamp-format"] = cfg.LogTimestampFormat
@ -485,6 +491,8 @@ func (cfg *Configuration) MarshalMap() map[string]any {
cfgmap["statuses-poll-max-options"] = cfg.StatusesPollMaxOptions
cfgmap["statuses-poll-option-max-chars"] = cfg.StatusesPollOptionMaxChars
cfgmap["statuses-media-max-files"] = cfg.StatusesMediaMaxFiles
cfgmap["scheduled-statuses-max-total"] = cfg.ScheduledStatusesMaxTotal
cfgmap["scheduled-statuses-max-daily"] = cfg.ScheduledStatusesMaxDaily
cfgmap["letsencrypt-enabled"] = cfg.LetsEncryptEnabled
cfgmap["letsencrypt-port"] = cfg.LetsEncryptPort
cfgmap["letsencrypt-cert-dir"] = cfg.LetsEncryptCertDir
@ -580,6 +588,7 @@ func (cfg *Configuration) MarshalMap() map[string]any {
cfgmap["cache-poll-vote-mem-ratio"] = cfg.Cache.PollVoteMemRatio
cfgmap["cache-poll-vote-ids-mem-ratio"] = cfg.Cache.PollVoteIDsMemRatio
cfgmap["cache-report-mem-ratio"] = cfg.Cache.ReportMemRatio
cfgmap["cache-scheduled-status-mem-ratio"] = cfg.Cache.ScheduledStatusMemRatio
cfgmap["cache-sin-bin-status-mem-ratio"] = cfg.Cache.SinBinStatusMemRatio
cfgmap["cache-status-mem-ratio"] = cfg.Cache.StatusMemRatio
cfgmap["cache-status-bookmark-mem-ratio"] = cfg.Cache.StatusBookmarkMemRatio
@ -1186,6 +1195,22 @@ func (cfg *Configuration) UnmarshalMap(cfgmap map[string]any) error {
}
}
if ival, ok := cfgmap["scheduled-statuses-max-total"]; ok {
var err error
cfg.ScheduledStatusesMaxTotal, err = cast.ToIntE(ival)
if err != nil {
return fmt.Errorf("error casting %#v -> int for 'scheduled-statuses-max-total': %w", ival, err)
}
}
if ival, ok := cfgmap["scheduled-statuses-max-daily"]; ok {
var err error
cfg.ScheduledStatusesMaxDaily, err = cast.ToIntE(ival)
if err != nil {
return fmt.Errorf("error casting %#v -> int for 'scheduled-statuses-max-daily': %w", ival, err)
}
}
if ival, ok := cfgmap["letsencrypt-enabled"]; ok {
var err error
cfg.LetsEncryptEnabled, err = cast.ToBoolE(ival)
@ -1972,6 +1997,14 @@ func (cfg *Configuration) UnmarshalMap(cfgmap map[string]any) error {
}
}
if ival, ok := cfgmap["cache-scheduled-status-mem-ratio"]; ok {
var err error
cfg.Cache.ScheduledStatusMemRatio, err = cast.ToFloat64E(ival)
if err != nil {
return fmt.Errorf("error casting %#v -> float64 for 'cache-scheduled-status-mem-ratio': %w", ival, err)
}
}
if ival, ok := cfgmap["cache-sin-bin-status-mem-ratio"]; ok {
var err error
cfg.Cache.SinBinStatusMemRatio, err = cast.ToFloat64E(ival)
@ -3753,6 +3786,50 @@ func GetStatusesMediaMaxFiles() int { return global.GetStatusesMediaMaxFiles() }
// SetStatusesMediaMaxFiles safely sets the value for global configuration 'StatusesMediaMaxFiles' field
func SetStatusesMediaMaxFiles(v int) { global.SetStatusesMediaMaxFiles(v) }
// GetScheduledStatusesMaxTotal safely fetches the Configuration value for state's 'ScheduledStatusesMaxTotal' field
func (st *ConfigState) GetScheduledStatusesMaxTotal() (v int) {
st.mutex.RLock()
v = st.config.ScheduledStatusesMaxTotal
st.mutex.RUnlock()
return
}
// SetScheduledStatusesMaxTotal safely sets the Configuration value for state's 'ScheduledStatusesMaxTotal' field
func (st *ConfigState) SetScheduledStatusesMaxTotal(v int) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.ScheduledStatusesMaxTotal = v
st.reloadToViper()
}
// GetScheduledStatusesMaxTotal safely fetches the value for global configuration 'ScheduledStatusesMaxTotal' field
func GetScheduledStatusesMaxTotal() int { return global.GetScheduledStatusesMaxTotal() }
// SetScheduledStatusesMaxTotal safely sets the value for global configuration 'ScheduledStatusesMaxTotal' field
func SetScheduledStatusesMaxTotal(v int) { global.SetScheduledStatusesMaxTotal(v) }
// GetScheduledStatusesMaxDaily safely fetches the Configuration value for state's 'ScheduledStatusesMaxDaily' field
func (st *ConfigState) GetScheduledStatusesMaxDaily() (v int) {
st.mutex.RLock()
v = st.config.ScheduledStatusesMaxDaily
st.mutex.RUnlock()
return
}
// SetScheduledStatusesMaxDaily safely sets the Configuration value for state's 'ScheduledStatusesMaxDaily' field
func (st *ConfigState) SetScheduledStatusesMaxDaily(v int) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.ScheduledStatusesMaxDaily = v
st.reloadToViper()
}
// GetScheduledStatusesMaxDaily safely fetches the value for global configuration 'ScheduledStatusesMaxDaily' field
func GetScheduledStatusesMaxDaily() int { return global.GetScheduledStatusesMaxDaily() }
// SetScheduledStatusesMaxDaily safely sets the value for global configuration 'ScheduledStatusesMaxDaily' field
func SetScheduledStatusesMaxDaily(v int) { global.SetScheduledStatusesMaxDaily(v) }
// GetLetsEncryptEnabled safely fetches the Configuration value for state's 'LetsEncryptEnabled' field
func (st *ConfigState) GetLetsEncryptEnabled() (v bool) {
st.mutex.RLock()
@ -5859,6 +5936,28 @@ func GetCacheReportMemRatio() float64 { return global.GetCacheReportMemRatio() }
// SetCacheReportMemRatio safely sets the value for global configuration 'Cache.ReportMemRatio' field
func SetCacheReportMemRatio(v float64) { global.SetCacheReportMemRatio(v) }
// GetCacheScheduledStatusMemRatio safely fetches the Configuration value for state's 'Cache.ScheduledStatusMemRatio' field
func (st *ConfigState) GetCacheScheduledStatusMemRatio() (v float64) {
st.mutex.RLock()
v = st.config.Cache.ScheduledStatusMemRatio
st.mutex.RUnlock()
return
}
// SetCacheScheduledStatusMemRatio safely sets the Configuration value for state's 'Cache.ScheduledStatusMemRatio' field
func (st *ConfigState) SetCacheScheduledStatusMemRatio(v float64) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.Cache.ScheduledStatusMemRatio = v
st.reloadToViper()
}
// GetCacheScheduledStatusMemRatio safely fetches the value for global configuration 'Cache.ScheduledStatusMemRatio' field
func GetCacheScheduledStatusMemRatio() float64 { return global.GetCacheScheduledStatusMemRatio() }
// SetCacheScheduledStatusMemRatio safely sets the value for global configuration 'Cache.ScheduledStatusMemRatio' field
func SetCacheScheduledStatusMemRatio(v float64) { global.SetCacheScheduledStatusMemRatio(v) }
// GetCacheSinBinStatusMemRatio safely fetches the Configuration value for state's 'Cache.SinBinStatusMemRatio' field
func (st *ConfigState) GetCacheSinBinStatusMemRatio() (v float64) {
st.mutex.RLock()
@ -6545,6 +6644,7 @@ func (st *ConfigState) GetTotalOfMemRatios() (total float64) {
total += st.config.Cache.PollVoteMemRatio
total += st.config.Cache.PollVoteIDsMemRatio
total += st.config.Cache.ReportMemRatio
total += st.config.Cache.ScheduledStatusMemRatio
total += st.config.Cache.SinBinStatusMemRatio
total += st.config.Cache.StatusMemRatio
total += st.config.Cache.StatusBookmarkMemRatio
@ -7328,6 +7428,17 @@ func flattenConfigMap(cfgmap map[string]any) {
}
}
for _, key := range [][]string{
{"cache", "scheduled-status-mem-ratio"},
} {
ival, ok := mapGet(cfgmap, key...)
if ok {
cfgmap["cache-scheduled-status-mem-ratio"] = ival
nestedKeys[key[0]] = struct{}{}
break
}
}
for _, key := range [][]string{
{"cache", "sin-bin-status-mem-ratio"},
} {

View file

@ -49,6 +49,8 @@
"statuses-media-max-files": 6,
"statuses-poll-max-options": 6,
"statuses-poll-option-max-chars": 50,
"scheduled-statuses-max-total": 300,
"scheduled-statuses-max-daily": 25,
"storage-backend": "local",
"storage-local-base-path": "/gotosocial/storage",
"trusted-proxies": [

View file

@ -243,6 +243,16 @@ statuses-poll-option-max-chars: 50
# Default: 6
statuses-media-max-files: 6
# Int. Maximum number of statuses a user can schedule at time.
# Examples: [300]
# Default: 300
scheduled-statuses-max-total: 300
# Int. Maximum number of statuses a user can schedule for a single day.
# Examples: [25]
# Default: 25
scheduled-statuses-max-daily: 25
##############################
##### LETSENCRYPT CONFIG #####
##############################