mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Add rate limiting, API response caching, and fix progress bar panic
- Rate limit !weather (5/day), !concerts (10/day), fix !translate default to 20/day - Add generic api_cache table with CacheGet/CacheSet helpers - Cache !weather (1h), !wiki, !define, !tv (24h), !upcoming movies, !anime season/upcoming (24h) - Fix ProgressBar panic on negative Repeat count when XP exceeds level threshold - All rate limits configurable via RATELIMIT_* env vars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,4 +36,6 @@ FEATURE_SHADE=
|
|||||||
FEATURE_TRIVIA=true # set to "false" to disable trivia
|
FEATURE_TRIVIA=true # set to "false" to disable trivia
|
||||||
|
|
||||||
# ---- Rate Limits (per user per day) ----
|
# ---- Rate Limits (per user per day) ----
|
||||||
|
RATELIMIT_WEATHER=5
|
||||||
RATELIMIT_TRANSLATE=20
|
RATELIMIT_TRANSLATE=20
|
||||||
|
RATELIMIT_CONCERTS=10
|
||||||
|
|||||||
@@ -82,6 +82,34 @@ func MarkJobCompleted(jobName, dateKey string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CacheGet returns cached data for the given key if it exists and is within ttlSeconds.
|
||||||
|
// Returns empty string if not cached or expired.
|
||||||
|
func CacheGet(key string, ttlSeconds int) string {
|
||||||
|
d := Get()
|
||||||
|
var data string
|
||||||
|
err := d.QueryRow(
|
||||||
|
`SELECT data FROM api_cache WHERE cache_key = ? AND cached_at > unixepoch() - ?`,
|
||||||
|
key, ttlSeconds,
|
||||||
|
).Scan(&data)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheSet stores data in the generic API cache.
|
||||||
|
func CacheSet(key, data string) {
|
||||||
|
d := Get()
|
||||||
|
_, err := d.Exec(
|
||||||
|
`INSERT INTO api_cache (cache_key, data, cached_at) VALUES (?, ?, unixepoch())
|
||||||
|
ON CONFLICT(cache_key) DO UPDATE SET data = ?, cached_at = unixepoch()`,
|
||||||
|
key, data, data,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("cache set", "key", key, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const schema = `
|
const schema = `
|
||||||
-- Users & XP
|
-- Users & XP
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
@@ -507,6 +535,13 @@ CREATE TABLE IF NOT EXISTS rate_limits (
|
|||||||
count INTEGER DEFAULT 0,
|
count INTEGER DEFAULT 0,
|
||||||
PRIMARY KEY (user_id, action, date)
|
PRIMARY KEY (user_id, action, date)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Generic API response cache
|
||||||
|
CREATE TABLE IF NOT EXISTS api_cache (
|
||||||
|
cache_key TEXT PRIMARY KEY,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
cached_at INTEGER DEFAULT (unixepoch())
|
||||||
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
// SeedSchedulerDefaults inserts default scheduler jobs if they don't exist.
|
// SeedSchedulerDefaults inserts default scheduler jobs if they don't exist.
|
||||||
|
|||||||
@@ -365,6 +365,12 @@ func (p *AnimePlugin) handleUnwatch(ctx MessageContext, idStr string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *AnimePlugin) handleSeason(ctx MessageContext) error {
|
func (p *AnimePlugin) handleSeason(ctx MessageContext) error {
|
||||||
|
// Check 6h cache
|
||||||
|
cacheKey := "anime_season"
|
||||||
|
if cached := db.CacheGet(cacheKey, 86400); cached != "" {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, cached)
|
||||||
|
}
|
||||||
|
|
||||||
apiURL := "https://api.jikan.moe/v4/seasons/now?limit=10&sfw=true"
|
apiURL := "https://api.jikan.moe/v4/seasons/now?limit=10&sfw=true"
|
||||||
|
|
||||||
var resp jikanSeasonResponse
|
var resp jikanSeasonResponse
|
||||||
@@ -392,10 +398,18 @@ func (p *AnimePlugin) handleSeason(ctx MessageContext) error {
|
|||||||
sb.WriteString(fmt.Sprintf("%d. %s [%s] - Score: %s\n", i+1, title, a.Type, scoreStr))
|
sb.WriteString(fmt.Sprintf("%d. %s [%s] - Score: %s\n", i+1, title, a.Type, scoreStr))
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
msg := sb.String()
|
||||||
|
db.CacheSet(cacheKey, msg)
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AnimePlugin) handleUpcoming(ctx MessageContext) error {
|
func (p *AnimePlugin) handleUpcoming(ctx MessageContext) error {
|
||||||
|
// Check 6h cache
|
||||||
|
cacheKey := "anime_upcoming"
|
||||||
|
if cached := db.CacheGet(cacheKey, 86400); cached != "" {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, cached)
|
||||||
|
}
|
||||||
|
|
||||||
apiURL := "https://api.jikan.moe/v4/seasons/upcoming?limit=10&sfw=true"
|
apiURL := "https://api.jikan.moe/v4/seasons/upcoming?limit=10&sfw=true"
|
||||||
|
|
||||||
var resp jikanSeasonResponse
|
var resp jikanSeasonResponse
|
||||||
@@ -423,7 +437,9 @@ func (p *AnimePlugin) handleUpcoming(ctx MessageContext) error {
|
|||||||
sb.WriteString(fmt.Sprintf("%d. %s [%s]\n", i+1, title, info))
|
sb.WriteString(fmt.Sprintf("%d. %s [%s]\n", i+1, title, info))
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
msg := sb.String()
|
||||||
|
db.CacheSet(cacheKey, msg)
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostDailyReleases checks broadcast days and DMs watchers about anime airing today.
|
// PostDailyReleases checks broadcast days and DMs watchers about anime airing today.
|
||||||
|
|||||||
@@ -44,17 +44,19 @@ type bandsintownEvent struct {
|
|||||||
// ConcertsPlugin provides concert lookups via Bandsintown.
|
// ConcertsPlugin provides concert lookups via Bandsintown.
|
||||||
type ConcertsPlugin struct {
|
type ConcertsPlugin struct {
|
||||||
Base
|
Base
|
||||||
apiKey string
|
apiKey string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
mu sync.Mutex
|
rateLimiter *RateLimitsPlugin
|
||||||
cooldowns map[string]time.Time // keyed by userID+artist
|
mu sync.Mutex
|
||||||
|
cooldowns map[string]time.Time // keyed by userID+artist
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConcertsPlugin creates a new ConcertsPlugin.
|
// NewConcertsPlugin creates a new ConcertsPlugin.
|
||||||
func NewConcertsPlugin(client *mautrix.Client) *ConcertsPlugin {
|
func NewConcertsPlugin(client *mautrix.Client, rateLimiter *RateLimitsPlugin) *ConcertsPlugin {
|
||||||
return &ConcertsPlugin{
|
return &ConcertsPlugin{
|
||||||
Base: NewBase(client),
|
Base: NewBase(client),
|
||||||
apiKey: os.Getenv("BANDSINTOWN_API_KEY"),
|
apiKey: os.Getenv("BANDSINTOWN_API_KEY"),
|
||||||
|
rateLimiter: rateLimiter,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
},
|
},
|
||||||
@@ -113,7 +115,18 @@ func (p *ConcertsPlugin) handleSearch(ctx MessageContext, artist string) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Concert lookups are not configured (missing API key).")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Concert lookups are not configured (missing API key).")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limit check
|
// Daily rate limit (configurable, default 10/day to match TS version)
|
||||||
|
concertLimit := 10
|
||||||
|
if v := os.Getenv("RATELIMIT_CONCERTS"); v != "" {
|
||||||
|
if n, err := fmt.Sscanf(v, "%d", &concertLimit); n != 1 || err != nil {
|
||||||
|
concertLimit = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.rateLimiter != nil && !p.rateLimiter.CheckLimit(ctx.Sender, "concerts", concertLimit) {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Concert search rate limit reached for today.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-search cooldown
|
||||||
cooldownKey := string(ctx.Sender) + ":" + strings.ToLower(artist)
|
cooldownKey := string(ctx.Sender) + ":" + strings.ToLower(artist)
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
if last, ok := p.cooldowns[cooldownKey]; ok && time.Since(last) < 30*time.Second {
|
if last, ok := p.cooldowns[cooldownKey]; ok && time.Since(last) < 30*time.Second {
|
||||||
|
|||||||
@@ -97,12 +97,14 @@ var diceRe = regexp.MustCompile(`(?i)^(\d+)?d(\d+)([+-]\d+)?$`)
|
|||||||
// FunPlugin provides various fun and utility commands.
|
// FunPlugin provides various fun and utility commands.
|
||||||
type FunPlugin struct {
|
type FunPlugin struct {
|
||||||
Base
|
Base
|
||||||
|
rateLimiter *RateLimitsPlugin
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFunPlugin creates a new FunPlugin.
|
// NewFunPlugin creates a new FunPlugin.
|
||||||
func NewFunPlugin(client *mautrix.Client) *FunPlugin {
|
func NewFunPlugin(client *mautrix.Client, rateLimiter *RateLimitsPlugin) *FunPlugin {
|
||||||
return &FunPlugin{
|
return &FunPlugin{
|
||||||
Base: NewBase(client),
|
Base: NewBase(client),
|
||||||
|
rateLimiter: rateLimiter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,6 +426,23 @@ func (p *FunPlugin) handleWeather(ctx MessageContext) error {
|
|||||||
return p.SendMessage(ctx.RoomID, "Weather service is not configured (missing API key).")
|
return p.SendMessage(ctx.RoomID, "Weather service is not configured (missing API key).")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rate limit (configurable, default 5/day)
|
||||||
|
weatherLimit := 5
|
||||||
|
if v := os.Getenv("RATELIMIT_WEATHER"); v != "" {
|
||||||
|
if n, err := fmt.Sscanf(v, "%d", &weatherLimit); n != 1 || err != nil {
|
||||||
|
weatherLimit = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.rateLimiter != nil && !p.rateLimiter.CheckLimit(ctx.Sender, "weather", weatherLimit) {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Weather lookup rate limit reached for today.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 1-hour cache (weather data doesn't change that fast)
|
||||||
|
cacheKey := "weather:" + strings.ToLower(location)
|
||||||
|
if cached := db.CacheGet(cacheKey, 3600); cached != "" {
|
||||||
|
return p.SendMessage(ctx.RoomID, cached)
|
||||||
|
}
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=metric",
|
apiURL := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=metric",
|
||||||
url.QueryEscape(location), apiKey)
|
url.QueryEscape(location), apiKey)
|
||||||
|
|
||||||
@@ -484,7 +503,9 @@ func (p *FunPlugin) handleWeather(ctx MessageContext) error {
|
|||||||
sb.WriteString(fmt.Sprintf(" Humidity: %d%%\n", weather.Main.Humidity))
|
sb.WriteString(fmt.Sprintf(" Humidity: %d%%\n", weather.Main.Humidity))
|
||||||
sb.WriteString(fmt.Sprintf(" Wind: %.1f m/s\n", weather.Wind.Speed))
|
sb.WriteString(fmt.Sprintf(" Wind: %.1f m/s\n", weather.Wind.Speed))
|
||||||
|
|
||||||
return p.SendMessage(ctx.RoomID, sb.String())
|
msg := sb.String()
|
||||||
|
db.CacheSet(cacheKey, msg)
|
||||||
|
return p.SendMessage(ctx.RoomID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FunPlugin) handleDadJoke(ctx MessageContext) error {
|
func (p *FunPlugin) handleDadJoke(ctx MessageContext) error {
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ func (p *LookupPlugin) handleWiki(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !wiki <topic>")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !wiki <topic>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check 24h cache
|
||||||
|
cacheKey := "wiki:" + strings.ToLower(topic)
|
||||||
|
if cached := db.CacheGet(cacheKey, 86400); cached != "" {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, cached)
|
||||||
|
}
|
||||||
|
|
||||||
encoded := url.PathEscape(strings.ReplaceAll(topic, " ", "_"))
|
encoded := url.PathEscape(strings.ReplaceAll(topic, " ", "_"))
|
||||||
apiURL := fmt.Sprintf("https://en.wikipedia.org/api/rest_v1/page/summary/%s", encoded)
|
apiURL := fmt.Sprintf("https://en.wikipedia.org/api/rest_v1/page/summary/%s", encoded)
|
||||||
|
|
||||||
@@ -108,6 +114,7 @@ func (p *LookupPlugin) handleWiki(ctx MessageContext) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf("%s\n\n%s\n\n%s", result.Title, extract, result.ContentURLs.Desktop.Page)
|
msg := fmt.Sprintf("%s\n\n%s\n\n%s", result.Title, extract, result.ContentURLs.Desktop.Page)
|
||||||
|
db.CacheSet(cacheKey, msg)
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +124,12 @@ func (p *LookupPlugin) handleDefine(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !define <word>")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !define <word>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check 24h cache
|
||||||
|
cacheKey := "define:" + strings.ToLower(word)
|
||||||
|
if cached := db.CacheGet(cacheKey, 86400); cached != "" {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, cached)
|
||||||
|
}
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("https://api.dictionaryapi.dev/api/v2/entries/en/%s", url.PathEscape(word))
|
apiURL := fmt.Sprintf("https://api.dictionaryapi.dev/api/v2/entries/en/%s", url.PathEscape(word))
|
||||||
|
|
||||||
resp, err := httpClient.Get(apiURL)
|
resp, err := httpClient.Get(apiURL)
|
||||||
@@ -170,7 +183,9 @@ func (p *LookupPlugin) handleDefine(ctx MessageContext) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
msg := sb.String()
|
||||||
|
db.CacheSet(cacheKey, msg)
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *LookupPlugin) handleUrban(ctx MessageContext) error {
|
func (p *LookupPlugin) handleUrban(ctx MessageContext) error {
|
||||||
@@ -280,9 +295,15 @@ func (p *LookupPlugin) handleTranslate(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Translation service is not configured.")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Translation service is not configured.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limit: 10 translations per day
|
// Rate limit (configurable, default 20/day to match TS version)
|
||||||
if p.rateLimiter != nil && !p.rateLimiter.CheckLimit(ctx.Sender, "translate", 10) {
|
translateLimit := 20
|
||||||
remaining := p.rateLimiter.Remaining(ctx.Sender, "translate", 10)
|
if v := os.Getenv("RATELIMIT_TRANSLATE"); v != "" {
|
||||||
|
if n, err := fmt.Sscanf(v, "%d", &translateLimit); n != 1 || err != nil {
|
||||||
|
translateLimit = 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.rateLimiter != nil && !p.rateLimiter.CheckLimit(ctx.Sender, "translate", translateLimit) {
|
||||||
|
remaining := p.rateLimiter.Remaining(ctx.Sender, "translate", translateLimit)
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
fmt.Sprintf("Translation rate limit reached. %d remaining today.", remaining))
|
fmt.Sprintf("Translation rate limit reached. %d remaining today.", remaining))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,6 +290,12 @@ func (p *MoviesPlugin) handleTV(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "TV lookups are not configured (missing API key).")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "TV lookups are not configured (missing API key).")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check 24h cache
|
||||||
|
cacheKey := "tv:" + strings.ToLower(query)
|
||||||
|
if cached := db.CacheGet(cacheKey, 86400); cached != "" {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, cached)
|
||||||
|
}
|
||||||
|
|
||||||
encoded := url.QueryEscape(query)
|
encoded := url.QueryEscape(query)
|
||||||
apiURL := fmt.Sprintf("https://api.themoviedb.org/3/search/tv?api_key=%s&query=%s&page=1",
|
apiURL := fmt.Sprintf("https://api.themoviedb.org/3/search/tv?api_key=%s&query=%s&page=1",
|
||||||
p.apiKey, encoded)
|
p.apiKey, encoded)
|
||||||
@@ -311,7 +317,9 @@ func (p *MoviesPlugin) handleTV(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to fetch TV show details.")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to fetch TV show details.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, p.formatTV(detail))
|
msg := p.formatTV(detail)
|
||||||
|
db.CacheSet(cacheKey, msg)
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MoviesPlugin) fetchTVDetail(tvID int) (*tmdbTVDetail, error) {
|
func (p *MoviesPlugin) fetchTVDetail(tvID int) (*tmdbTVDetail, error) {
|
||||||
@@ -474,6 +482,12 @@ func (p *MoviesPlugin) handleUpcoming(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Movie lookups are not configured (missing API key).")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Movie lookups are not configured (missing API key).")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check 6h cache
|
||||||
|
cacheKey := "upcoming_movies"
|
||||||
|
if cached := db.CacheGet(cacheKey, 86400); cached != "" {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, cached)
|
||||||
|
}
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("https://api.themoviedb.org/3/movie/upcoming?api_key=%s®ion=US&page=1", p.apiKey)
|
apiURL := fmt.Sprintf("https://api.themoviedb.org/3/movie/upcoming?api_key=%s®ion=US&page=1", p.apiKey)
|
||||||
|
|
||||||
var resp tmdbUpcomingResponse
|
var resp tmdbUpcomingResponse
|
||||||
@@ -511,7 +525,9 @@ func (p *MoviesPlugin) handleUpcoming(ctx MessageContext) error {
|
|||||||
sb.WriteString(fmt.Sprintf("%d. %s (%s)%s\n", i+1, title, dateStr, ratingStr))
|
sb.WriteString(fmt.Sprintf("%d. %s (%s)%s\n", i+1, title, dateStr, ratingStr))
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
msg := sb.String()
|
||||||
|
db.CacheSet(cacheKey, msg)
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MoviesPlugin) tmdbGet(apiURL string, target interface{}) error {
|
func (p *MoviesPlugin) tmdbGet(apiURL string, target interface{}) error {
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -86,7 +86,7 @@ func main() {
|
|||||||
registry.Register(plugin.NewTriviaPlugin(client))
|
registry.Register(plugin.NewTriviaPlugin(client))
|
||||||
registry.Register(plugin.NewRemindersPlugin(client))
|
registry.Register(plugin.NewRemindersPlugin(client))
|
||||||
registry.Register(plugin.NewPresencePlugin(client))
|
registry.Register(plugin.NewPresencePlugin(client))
|
||||||
registry.Register(plugin.NewFunPlugin(client))
|
registry.Register(plugin.NewFunPlugin(client, ratePlugin))
|
||||||
registry.Register(plugin.NewToolsPlugin(client))
|
registry.Register(plugin.NewToolsPlugin(client))
|
||||||
registry.Register(plugin.NewUserPlugin(client))
|
registry.Register(plugin.NewUserPlugin(client))
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ func main() {
|
|||||||
registry.Register(plugin.NewLookupPlugin(client, ratePlugin))
|
registry.Register(plugin.NewLookupPlugin(client, ratePlugin))
|
||||||
registry.Register(plugin.NewCountdownPlugin(client))
|
registry.Register(plugin.NewCountdownPlugin(client))
|
||||||
registry.Register(plugin.NewStocksPlugin(client))
|
registry.Register(plugin.NewStocksPlugin(client))
|
||||||
registry.Register(plugin.NewConcertsPlugin(client))
|
registry.Register(plugin.NewConcertsPlugin(client, ratePlugin))
|
||||||
registry.Register(plugin.NewAnimePlugin(client))
|
registry.Register(plugin.NewAnimePlugin(client))
|
||||||
registry.Register(plugin.NewMoviesPlugin(client))
|
registry.Register(plugin.NewMoviesPlugin(client))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user