├── .gitignore ├── LICENSE_OF_DEPENDENCIES.md ├── main.go ├── temotes ├── interfaces.go ├── api │ ├── endpoints │ │ ├── channel_identifiers.go │ │ ├── global_emotes.go │ │ ├── channel_emotes.go │ │ ├── helpers.go │ │ └── channel_emote_proxy.go │ ├── dashboard.go │ ├── server.go │ ├── error_handler.go │ ├── routes.go │ └── rate_limiter.go ├── utils.go ├── emote.go ├── cache.go ├── cached_fetcher.go ├── config.go ├── fetcher.go └── providers │ ├── bttv.go │ ├── ffz.go │ ├── 7tv.go │ └── twitch.go ├── .env ├── Dockerfile ├── .github └── workflows │ └── docker-publish.yml ├── go.mod ├── README.md ├── go.sum ├── agpl-3.0.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE_OF_DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | ### Go 2 | 3 | * github.com/gofiber/fiber [MIT](https://github.com/gofiber/fiber/blob/master/LICENSE) 4 | * github.com/go-redis/redis [BSD-2-Clause](https://github.com/go-redis/redis/blob/master/LICENSE) -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "temotes/temotes" 6 | "temotes/temotes/api" 7 | ) 8 | 9 | func main() { 10 | app := api.SetupServer() 11 | log.Fatal(app.Listen(temotes.GetConfig().ServerAddr)) 12 | } 13 | -------------------------------------------------------------------------------- /temotes/interfaces.go: -------------------------------------------------------------------------------- 1 | package temotes 2 | 3 | type TwitchUserId int 4 | 5 | type EmoteFetcher interface { 6 | FetchGlobalEmotes() []Emote 7 | FetchChannelEmotes(id TwitchUserId) []Emote 8 | } 9 | 10 | //type PersonalDataFetcher interface { 11 | // FetchPersonalAvatar(id TwitchUserId) 12 | // FetchPersonalEmotes(id TwitchUserId) []Emote 13 | //} 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SERVER_ADDR=0.0.0.0:5000 2 | TWITCH_CLIENT_ID= 3 | TWITCH_CLIENT_SECRET= 4 | DASHBOARD_LOGIN=admin 5 | DASHBOARD_PASSWORD=admin 6 | PROXY_MAX_AGE=86400 7 | RATE_LIMIT_MAX=10 8 | RATE_LIMIT_TIME=10 9 | REDIS_ADDR=localhost:6379 10 | REDIS_PASSWORD= 11 | REDIS_DB=0 12 | CACHE_TTL_GLOBAL_EMOTES=900 13 | CACHE_TTL_CHANNEL_EMOTES=300 14 | CACHE_TTL_TWITCH_IDENTIFIERS=604800 15 | FETCHER_TIMEOUT=3 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine AS builder 2 | WORKDIR /app 3 | COPY go.mod go.sum ./ 4 | RUN go mod download 5 | COPY . . 6 | RUN go build -o server main.go 7 | 8 | FROM alpine:3.20 9 | WORKDIR /app 10 | RUN apk add --no-cache ca-certificates curl wget 11 | COPY --from=builder /app/server . 12 | RUN adduser -D app && chown app:app /app/server 13 | USER app 14 | EXPOSE 5000 15 | CMD ["./server"] -------------------------------------------------------------------------------- /temotes/api/endpoints/channel_identifiers.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "temotes/temotes/providers" 6 | ) 7 | 8 | func GetChannelIdentifiers(c *fiber.Ctx) error { 9 | twitchUser, err := providers.TwitchFetcher{}.FetchUserIdentifiers(c.Params("channel")) 10 | if err != nil { 11 | return fiber.NewError(fiber.StatusNotFound, "User not found") 12 | } 13 | 14 | return c.JSON(twitchUser) 15 | } 16 | -------------------------------------------------------------------------------- /temotes/utils.go: -------------------------------------------------------------------------------- 1 | package temotes 2 | 3 | func Contains(s []string, e string) bool { 4 | for _, a := range s { 5 | if a == e { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | 12 | func Unique(s []string) []string { 13 | keys := make(map[string]bool) 14 | var list []string 15 | for _, entry := range s { 16 | if _, value := keys[entry]; !value { 17 | keys[entry] = true 18 | list = append(list, entry) 19 | } 20 | } 21 | return list 22 | } 23 | -------------------------------------------------------------------------------- /temotes/api/dashboard.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/gofiber/fiber/v2/middleware/basicauth" 6 | "github.com/gofiber/fiber/v2/middleware/monitor" 7 | "temotes/temotes" 8 | ) 9 | 10 | func setupDashboard(app *fiber.App) { 11 | app.Get("/dashboard", basicauth.New(basicauth.Config{ 12 | Users: map[string]string{ 13 | temotes.GetConfig().DashboardLogin: temotes.GetConfig().DashboardPassword, 14 | }, 15 | }), monitor.New()) 16 | } 17 | -------------------------------------------------------------------------------- /temotes/api/endpoints/global_emotes.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "temotes/temotes" 6 | ) 7 | 8 | func GetGlobalEmotes(c *fiber.Ctx) error { 9 | fetchers, err := Helpers{}.ParseServices(c.Params("services")) 10 | if err != nil { 11 | return err 12 | } 13 | 14 | var emotes []temotes.Emote 15 | for _, fetcher := range *fetchers { 16 | emotes = append(emotes, fetcher.FetchGlobalEmotes()...) 17 | } 18 | 19 | return c.JSON(emotes) 20 | } 21 | -------------------------------------------------------------------------------- /temotes/api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/gofiber/fiber/v2/middleware/cors" 6 | "github.com/gofiber/fiber/v2/middleware/recover" 7 | ) 8 | 9 | func SetupServer() *fiber.App { 10 | app := fiber.New(fiber.Config{ 11 | ErrorHandler: errorHandler, 12 | }) 13 | 14 | app.Use(cors.New()) 15 | app.Use(recover.New()) 16 | 17 | rateLimiter(app) 18 | setupRoutes(app) 19 | setupDashboard(app) 20 | 21 | app.Use(notFoundHandler) 22 | 23 | return app 24 | } 25 | -------------------------------------------------------------------------------- /temotes/api/endpoints/channel_emotes.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "temotes/temotes" 6 | ) 7 | 8 | func GetChannelEmotes(c *fiber.Ctx) error { 9 | channelId, err := Helpers{}.GetTwitchUserId(c.Params("channel")) 10 | if err != nil { 11 | return err 12 | } 13 | 14 | fetchers, err := Helpers{}.ParseServices(c.Params("services")) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | emotes := make([]temotes.Emote, 0) 20 | for _, fetcher := range *fetchers { 21 | emotes = append(emotes, fetcher.FetchChannelEmotes(channelId)...) 22 | } 23 | 24 | return c.JSON(emotes) 25 | } 26 | -------------------------------------------------------------------------------- /temotes/api/error_handler.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/gofiber/fiber/v2" 4 | 5 | func errorHandler(c *fiber.Ctx, err error) error { 6 | code := fiber.StatusInternalServerError 7 | 8 | if e, ok := err.(*fiber.Error); ok { 9 | code = e.Code 10 | } 11 | 12 | err = c.Status(code).JSON(fiber.Map{ 13 | "error": err.Error(), 14 | }) 15 | if err != nil { 16 | return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ 17 | "error": "Internal Server Error", 18 | }) 19 | } 20 | 21 | return nil 22 | } 23 | 24 | func notFoundHandler(c *fiber.Ctx) error { 25 | return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ 26 | "error": "Not Found", 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /temotes/emote.go: -------------------------------------------------------------------------------- 1 | package temotes 2 | 3 | type EmoteSize string 4 | 5 | const ( 6 | Size1x EmoteSize = "1x" 7 | Size2x = "2x" 8 | Size3x = "3x" 9 | Size4x = "4x" 10 | ) 11 | 12 | type EmoteProvider int 13 | 14 | const ( 15 | ProviderTwitch EmoteProvider = 0 16 | Provider7tv = 1 17 | ProviderBttv = 2 18 | ProviderFfz = 3 19 | ) 20 | 21 | type EmoteUrl struct { 22 | Size EmoteSize `json:"size"` 23 | Url string `json:"url"` 24 | } 25 | 26 | type Emote struct { 27 | Provider EmoteProvider `json:"provider"` 28 | Code string `json:"code"` 29 | Urls []EmoteUrl `json:"urls"` 30 | } 31 | -------------------------------------------------------------------------------- /temotes/api/routes.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/gofiber/fiber/v2/middleware/healthcheck" 6 | "temotes/temotes/api/endpoints" 7 | ) 8 | 9 | func setupRoutes(app *fiber.App) { 10 | v1 := app.Group("/v1") 11 | v1Global := v1.Group("/global") 12 | v1Channel := v1.Group("/channel/:channel") 13 | 14 | // Global 15 | v1Global.Get("/emotes/:services", endpoints.GetGlobalEmotes) 16 | 17 | // Channel specific 18 | v1Channel.Get("/emotes/:services", endpoints.GetChannelEmotes) 19 | v1Channel.Get("/emotes/:services/proxy", endpoints.GetChannelEmoteProxy) 20 | 21 | v1Channel.Get("/id", endpoints.GetChannelIdentifiers) 22 | 23 | // Healthcheck 24 | app.Use(healthcheck.New()) 25 | } 26 | -------------------------------------------------------------------------------- /temotes/api/rate_limiter.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "github.com/gofiber/fiber/v2/middleware/limiter" 6 | "temotes/temotes" 7 | "time" 8 | ) 9 | 10 | func rateLimiter(app *fiber.App) { 11 | app.Use(limiter.New(limiter.Config{ 12 | KeyGenerator: func(c *fiber.Ctx) string { 13 | cloudflareIP := c.Get("CF-Connecting-IP") 14 | if cloudflareIP != "" { 15 | return cloudflareIP 16 | } 17 | 18 | return c.IP() 19 | }, 20 | Max: temotes.GetConfig().RateLimitMax, 21 | Expiration: time.Duration(temotes.GetConfig().RateLimitTime) * time.Second, 22 | LimitReached: func(c *fiber.Ctx) error { 23 | return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ 24 | "error": "Rate limit reached", 25 | }) 26 | }, 27 | })) 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build-and-push: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Log in to GitHub Container Registry 19 | uses: docker/login-action@v3 20 | with: 21 | registry: ghcr.io 22 | username: ${{ github.actor }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - name: Build and push Docker image 26 | uses: docker/build-push-action@v6 27 | with: 28 | context: . 29 | push: true 30 | tags: | 31 | ghcr.io/${{ github.repository }}/temotes:latest 32 | ghcr.io/${{ github.repository }}/temotes:${{ github.event.release.tag_name }} -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module temotes 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-redis/redis/v8 v8.11.5 7 | github.com/gofiber/fiber/v2 v2.52.9 8 | github.com/patrickmn/go-cache v2.1.0+incompatible 9 | ) 10 | 11 | require ( 12 | github.com/andybalholm/brotli v1.1.0 // indirect 13 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | github.com/google/uuid v1.6.0 // indirect 16 | github.com/klauspost/compress v1.17.9 // indirect 17 | github.com/mattn/go-colorable v0.1.13 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/mattn/go-runewidth v0.0.16 // indirect 20 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect 21 | github.com/rivo/uniseg v0.2.0 // indirect 22 | github.com/tinylib/msgp v1.2.5 // indirect 23 | github.com/valyala/bytebufferpool v1.0.0 // indirect 24 | github.com/valyala/fasthttp v1.51.0 // indirect 25 | github.com/valyala/tcplisten v1.0.0 // indirect 26 | golang.org/x/sys v0.28.0 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /temotes/cache.go: -------------------------------------------------------------------------------- 1 | package temotes 2 | 3 | import ( 4 | "github.com/go-redis/redis/v8" 5 | "github.com/patrickmn/go-cache" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | var redisClient *redis.Client 11 | var cacheClient *cache.Cache 12 | 13 | var redisLock = &sync.Mutex{} 14 | var cacheLock = &sync.Mutex{} 15 | 16 | type CacheService struct{} 17 | 18 | func (c CacheService) GetRedisClient() *redis.Client { 19 | if redisClient == nil { 20 | redisLock.Lock() 21 | defer redisLock.Unlock() 22 | if redisClient == nil { 23 | redisDb := int64(GetConfig().RedisDB) 24 | 25 | redisClient = redis.NewClient(&redis.Options{ 26 | Addr: GetConfig().RedisAddr, 27 | Password: GetConfig().RedisPassword, 28 | DB: int(redisDb), 29 | }) 30 | } 31 | } 32 | 33 | return redisClient 34 | } 35 | 36 | func (c CacheService) GetCacheClient() *cache.Cache { 37 | if cacheClient == nil { 38 | cacheLock.Lock() 39 | defer cacheLock.Unlock() 40 | if cacheClient == nil { 41 | cacheClient = cache.New(time.Hour, 2*time.Hour) 42 | } 43 | } 44 | 45 | return cacheClient 46 | } 47 | -------------------------------------------------------------------------------- /temotes/cached_fetcher.go: -------------------------------------------------------------------------------- 1 | package temotes 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | var ( 10 | fetcher *Fetcher 11 | GlobalEmotesTtl time.Duration 12 | ChannelEmotesTtl time.Duration 13 | TwitchIdTtl time.Duration 14 | ) 15 | 16 | type CachedFetcher struct{} 17 | 18 | func (f CachedFetcher) initTtl() { 19 | GlobalEmotesTtl = time.Duration(GetConfig().CacheTTLGlobalEmotes) * time.Second 20 | ChannelEmotesTtl = time.Duration(GetConfig().CacheTTLChannelEmotes) * time.Second 21 | TwitchIdTtl = time.Duration(GetConfig().CacheTTLTwitchIDs) * time.Second 22 | } 23 | 24 | func (f CachedFetcher) getFetcher() *Fetcher { 25 | if fetcher == nil { 26 | f.initTtl() 27 | fetcher = &Fetcher{} 28 | } 29 | 30 | return fetcher 31 | } 32 | 33 | func (f CachedFetcher) FetchGqlData(url string, query string, ttl time.Duration, cacheKey string) ([]byte, error) { 34 | req := f.getFetcher().GetGqlRequest(url, query) 35 | 36 | return f.FetchDataRequest(req, ttl, cacheKey) 37 | } 38 | 39 | func (f CachedFetcher) FetchData(url string, ttl time.Duration, cacheKey string) ([]byte, error) { 40 | req := f.getFetcher().GetRequest(url) 41 | 42 | return f.FetchDataRequest(req, ttl, cacheKey) 43 | } 44 | 45 | func (f CachedFetcher) FetchDataRequest(req *http.Request, ttl time.Duration, cacheKey string) ([]byte, error) { 46 | cache := CacheService{}.GetRedisClient() 47 | cacheData, err := cache.Get(context.Background(), cacheKey).Result() 48 | if err == nil { 49 | return []byte(cacheData), nil 50 | } 51 | 52 | body, err := f.getFetcher().FetchDataRequest(req) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | go cache.Set(context.Background(), cacheKey, string(body), ttl) 58 | 59 | return body, nil 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![](https://emotes.adamcy.pl/v1/channel/adiq/emotes/7tv/proxy?emote=Harambe&size=1x) tEmotes API 2 | 3 | Easy to use API for Twitch emotes 4 | 5 | [![Documentation](https://img.shields.io/badge/docs-see_how_to_use-brightgreen?style=for-the-badge&logo=readthedocs)](https://adiq.stoplight.io/docs/temotes/YXBpOjMyNjU2ODIx-t-emotes-api) 6 | 7 | We support: 8 | * Twitch 9 | * 7TV 10 | * BetterTTV 11 | * FrankerFaceZ 12 | 13 | ## Setup 14 | 15 | > Note: _Keep in mind that you don't need to install anything if you just want to consume the API._ 16 | > 17 | > You can use the public API that we expose on [emotes.adamcy.pl](https://adiq.stoplight.io/docs/temotes/YXBpOjMyNjU2ODIx-t-emotes-api) 18 | 19 | ### Requirements 20 | 21 | * Golang 22 | * Redis 23 | * Twitch API Access (Client ID and Client Secret) 24 | 25 | ### Configure 26 | 27 | Configuration is as easy as defining the environment variables from the `.env` file. 28 | 29 | #### Docker Swarm Secrets 30 | 31 | For Docker Swarm environments, secrets are also supported. If an environment variable's name ends with `_FILE` (e.g., `REDIS_PASSWORD_FILE=/run/secrets/redis_password`), the application will read the content of the specified file and use it as the value for that variable. 32 | 33 | **Precedence:** If both `FOO` and `FOO_FILE` are set, the application will use the value from `FOO` and ignore `FOO_FILE`. This matches the implementation in `getEnvOrFile` and avoids surprises for operators. 34 | ### Run & Build 35 | 36 | Running and building the application is as simple as in any other Go project. 37 | 38 | ### Health Endpoints 39 | 40 | The API provides two health check endpoints for monitoring: 41 | 42 | - **Liveness Probe** – checks if the server is running (`/livez`) 43 | - **Readiness Probe** – checks if the application and its dependencies are ready to handle requests (`/readyz`) 44 | 45 | # License 46 | 47 | This project is licensed under the terms of the [AGPL-3.0 license](agpl-3.0.md). 48 | -------------------------------------------------------------------------------- /temotes/api/endpoints/helpers.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gofiber/fiber/v2" 6 | "strconv" 7 | "strings" 8 | "temotes/temotes" 9 | "temotes/temotes/providers" 10 | "time" 11 | ) 12 | 13 | type Helpers struct{} 14 | 15 | func (h Helpers) GetTwitchUserId(input string) (temotes.TwitchUserId, error) { 16 | id, err := strconv.ParseInt(strings.ToLower(input), 10, 64) 17 | 18 | if id == 0 || err != nil { 19 | cache := temotes.CacheService{}.GetCacheClient() 20 | cacheKey := fmt.Sprintf("twitch_channel_id_%s", strings.ToLower(input)) 21 | channelId, channelIdCached := cache.Get(cacheKey) 22 | 23 | if channelIdCached { 24 | return channelId.(temotes.TwitchUserId), nil 25 | } 26 | 27 | twitchUser, err := providers.TwitchFetcher{}.FetchUserIdentifiers(input) 28 | if err != nil { 29 | return 0, fiber.NewError(fiber.StatusNotFound, "User not found") 30 | } 31 | 32 | cache.Set(cacheKey, twitchUser.ID, time.Hour) 33 | 34 | return twitchUser.ID, nil 35 | } 36 | 37 | return temotes.TwitchUserId(id), nil 38 | } 39 | 40 | func (h Helpers) ParseServices(input string) (*[]temotes.EmoteFetcher, error) { 41 | var fetchers []temotes.EmoteFetcher 42 | 43 | unfilteredServices := strings.Split(strings.ToLower(input), ".") 44 | services := temotes.Unique(unfilteredServices) 45 | 46 | if len(services) == 0 { 47 | return nil, fiber.NewError(fiber.StatusBadRequest, "No services specified") 48 | } 49 | 50 | if len(services) != len(unfilteredServices) { 51 | return nil, fiber.NewError(fiber.StatusBadRequest, "Duplicate services specified") 52 | } 53 | 54 | if temotes.Contains(services, "all") { 55 | if len(services) > 1 { 56 | return nil, fiber.NewError(fiber.StatusBadRequest, "Cannot specify all and other services") 57 | } 58 | 59 | services = []string{"twitch", "7tv", "bttv", "ffz"} 60 | } 61 | 62 | for _, serviceName := range services { 63 | switch serviceName { 64 | case "twitch": 65 | fetchers = append(fetchers, providers.TwitchFetcher{}) 66 | case "7tv": 67 | fetchers = append(fetchers, providers.SevenTvFetcher{}) 68 | case "bttv": 69 | fetchers = append(fetchers, providers.BttvFetcher{}) 70 | case "ffz": 71 | fetchers = append(fetchers, providers.FfzFetcher{}) 72 | default: 73 | return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid service: %s", serviceName)) 74 | } 75 | } 76 | 77 | return &fetchers, nil 78 | } 79 | -------------------------------------------------------------------------------- /temotes/config.go: -------------------------------------------------------------------------------- 1 | package temotes 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | func getEnvOrFile(key string) string { 12 | if val := os.Getenv(key); val != "" { 13 | return val 14 | } 15 | if filePath := os.Getenv(key + "_FILE"); filePath != "" { 16 | data, err := os.ReadFile(filePath) 17 | if err == nil { 18 | return strings.TrimSpace(string(data)) 19 | } 20 | } 21 | return "" 22 | } 23 | 24 | func getInt(key string, def int) int { 25 | val := getEnvOrFile(key) 26 | if val == "" { 27 | return def 28 | } 29 | i, err := strconv.Atoi(val) 30 | if err != nil { 31 | return def 32 | } 33 | return i 34 | } 35 | 36 | type Config struct { 37 | ServerAddr string 38 | TwitchClientID string 39 | TwitchClientSecret string 40 | DashboardLogin string 41 | DashboardPassword string 42 | ProxyMaxAge int 43 | RateLimitMax int 44 | RateLimitTime int 45 | RedisAddr string 46 | RedisPassword string 47 | RedisDB int 48 | CacheTTLGlobalEmotes int 49 | CacheTTLChannelEmotes int 50 | CacheTTLTwitchIDs int 51 | FetcherTimeout int 52 | } 53 | 54 | func LoadConfig() { 55 | cfg := &Config{ 56 | ServerAddr: getEnvOrFile("SERVER_ADDR"), 57 | TwitchClientID: getEnvOrFile("TWITCH_CLIENT_ID"), 58 | TwitchClientSecret: getEnvOrFile("TWITCH_CLIENT_SECRET"), 59 | DashboardLogin: getEnvOrFile("DASHBOARD_LOGIN"), 60 | DashboardPassword: getEnvOrFile("DASHBOARD_PASSWORD"), 61 | ProxyMaxAge: getInt("PROXY_MAX_AGE", 86400), 62 | RateLimitMax: getInt("RATE_LIMIT_MAX", 10), 63 | RateLimitTime: getInt("RATE_LIMIT_TIME", 10), 64 | RedisAddr: getEnvOrFile("REDIS_ADDR"), 65 | RedisPassword: getEnvOrFile("REDIS_PASSWORD"), 66 | RedisDB: getInt("REDIS_DB", 0), 67 | CacheTTLGlobalEmotes: getInt("CACHE_TTL_GLOBAL_EMOTES", 900), 68 | CacheTTLChannelEmotes: getInt("CACHE_TTL_CHANNEL_EMOTES", 300), 69 | CacheTTLTwitchIDs: getInt("CACHE_TTL_TWITCH_IDENTIFIERS", 604800), 70 | FetcherTimeout: getInt("FETCHER_TIMEOUT", 3), 71 | } 72 | SetConfig(cfg) 73 | log.Print("Config has been loaded.") 74 | } 75 | 76 | var GlobalConfig *Config 77 | 78 | func SetConfig(cfg *Config) { 79 | GlobalConfig = cfg 80 | } 81 | 82 | var once sync.Once 83 | 84 | func GetConfig() *Config { 85 | once.Do(LoadConfig) 86 | return GlobalConfig 87 | } 88 | -------------------------------------------------------------------------------- /temotes/fetcher.go: -------------------------------------------------------------------------------- 1 | package temotes 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "regexp" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type Fetcher struct{} 15 | 16 | func (f Fetcher) GetRequest(url string) *http.Request { 17 | req, err := http.NewRequest(http.MethodGet, url, nil) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36") 23 | 24 | return req 25 | } 26 | 27 | func (f Fetcher) GetGqlRequest(url string, query string) *http.Request { 28 | cleanQuery := strings.ReplaceAll(query, "\n", " ") 29 | cleanQuery = strings.ReplaceAll(cleanQuery, "\r", " ") 30 | cleanQuery = strings.ReplaceAll(cleanQuery, "\t", " ") 31 | 32 | spaceRegexp, err := regexp.Compile("\\s+") 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | cleanQuery = strings.TrimSpace(spaceRegexp.ReplaceAllString(cleanQuery, " ")) 38 | 39 | payload := map[string]interface{}{ 40 | "query": cleanQuery, 41 | } 42 | 43 | parsedPayload, err := json.Marshal(payload) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(string(parsedPayload))) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36") 54 | req.Header.Set("Content-Type", "application/json") 55 | 56 | return req 57 | } 58 | 59 | func (f Fetcher) FetchData(url string) ([]byte, error) { 60 | return f.FetchDataRequest(f.GetRequest(url)) 61 | } 62 | 63 | func (f Fetcher) FetchDataRequest(req *http.Request) ([]byte, error) { 64 | timeout := GetConfig().FetcherTimeout 65 | if timeout <= 0 { 66 | log.Print("FETCHER_TIMEOUT not specified or defined incorrectly. Defaulting to 3 seconds.") 67 | timeout = 3 68 | } 69 | 70 | client := http.Client{ 71 | Timeout: time.Duration(timeout) * time.Second, 72 | } 73 | 74 | res, getErr := client.Do(req) 75 | if getErr != nil { 76 | log.Fatal(getErr) 77 | } 78 | 79 | if res.StatusCode != 200 { 80 | return nil, errors.New("request returned non-successful response response") 81 | } 82 | 83 | if res.Body != nil { 84 | defer res.Body.Close() 85 | } 86 | 87 | body, readErr := ioutil.ReadAll(res.Body) 88 | if readErr != nil { 89 | return nil, readErr 90 | } 91 | 92 | return body, nil 93 | } 94 | -------------------------------------------------------------------------------- /temotes/providers/bttv.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "temotes/temotes" 8 | ) 9 | 10 | type BttvFetcher struct{} 11 | 12 | type bttvEmote struct { 13 | ID string `json:"id"` 14 | Code string `json:"code"` 15 | } 16 | 17 | func (t BttvFetcher) FetchGlobalEmotes() []temotes.Emote { 18 | response, err := temotes.CachedFetcher{}.FetchData("https://api.betterttv.net/3/cached/emotes/global", temotes.GlobalEmotesTtl, "bttv-global-emotes") 19 | var emotes []temotes.Emote 20 | if err != nil { 21 | return emotes 22 | } 23 | 24 | var bttvEmotes []bttvEmote 25 | jsonErr := json.Unmarshal(response, &bttvEmotes) 26 | if jsonErr != nil { 27 | log.Fatal(jsonErr) 28 | } 29 | 30 | for _, bttvEmote := range bttvEmotes { 31 | emotes = append(emotes, t.parseEmote(bttvEmote)) 32 | } 33 | 34 | return emotes 35 | } 36 | 37 | type bttvChannelEmotesResponse struct { 38 | ChannelEmotes []bttvEmote `json:"channelEmotes"` 39 | SharedEmotes []bttvEmote `json:"sharedEmotes"` 40 | } 41 | 42 | func (t BttvFetcher) FetchChannelEmotes(id temotes.TwitchUserId) []temotes.Emote { 43 | response, err := temotes.CachedFetcher{}.FetchData(fmt.Sprintf("https://api.betterttv.net/3/cached/users/twitch/%d", id), temotes.ChannelEmotesTtl, fmt.Sprintf("bttv-channel-emotes-%d", id)) 44 | var emotes []temotes.Emote 45 | if err != nil { 46 | return emotes 47 | } 48 | 49 | var bttvEmotesResponse bttvChannelEmotesResponse 50 | jsonErr := json.Unmarshal(response, &bttvEmotesResponse) 51 | if jsonErr != nil { 52 | log.Fatal(jsonErr) 53 | } 54 | 55 | for _, bttvEmote := range bttvEmotesResponse.ChannelEmotes { 56 | emotes = append(emotes, t.parseEmote(bttvEmote)) 57 | } 58 | 59 | for _, bttvEmote := range bttvEmotesResponse.SharedEmotes { 60 | emotes = append(emotes, t.parseEmote(bttvEmote)) 61 | } 62 | 63 | return emotes 64 | } 65 | 66 | func (t BttvFetcher) parseEmoteUrls(emote bttvEmote) []temotes.EmoteUrl { 67 | return []temotes.EmoteUrl{ 68 | { 69 | Size: temotes.Size1x, 70 | Url: fmt.Sprintf("https://cdn.betterttv.net/emote/%s/1x", emote.ID), 71 | }, 72 | { 73 | Size: temotes.Size2x, 74 | Url: fmt.Sprintf("https://cdn.betterttv.net/emote/%s/2x", emote.ID), 75 | }, 76 | { 77 | Size: temotes.Size3x, 78 | Url: fmt.Sprintf("https://cdn.betterttv.net/emote/%s/3x", emote.ID), 79 | }, 80 | } 81 | } 82 | 83 | func (t BttvFetcher) parseEmote(emote bttvEmote) temotes.Emote { 84 | return temotes.Emote{ 85 | Provider: temotes.ProviderBttv, 86 | Code: emote.Code, 87 | Urls: t.parseEmoteUrls(emote), 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /temotes/providers/ffz.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "sort" 8 | "temotes/temotes" 9 | "time" 10 | ) 11 | 12 | type FfzFetcher struct{} 13 | 14 | type ffzEmote struct { 15 | ID int `json:"id"` 16 | Code string `json:"name"` 17 | Urls map[string]string `json:"urls"` 18 | } 19 | 20 | type ffzSetsResponse struct { 21 | Emotes []ffzEmote `json:"emoticons"` 22 | } 23 | 24 | type ffzResponse struct { 25 | Sets map[string]ffzSetsResponse `json:"sets"` 26 | } 27 | 28 | func (t FfzFetcher) fetchEmotes(url string, ttl time.Duration, cacheKey string) []temotes.Emote { 29 | response, err := temotes.CachedFetcher{}.FetchData(url, ttl, cacheKey) 30 | var emotes []temotes.Emote 31 | if err != nil { 32 | return emotes 33 | } 34 | 35 | var ffzEmotesResponse ffzResponse 36 | jsonErr := json.Unmarshal(response, &ffzEmotesResponse) 37 | if jsonErr != nil { 38 | log.Fatal(jsonErr) 39 | } 40 | 41 | for _, emoteSet := range ffzEmotesResponse.Sets { 42 | for _, ffzEmote := range emoteSet.Emotes { 43 | emotes = append(emotes, t.parseEmote(ffzEmote)) 44 | } 45 | } 46 | 47 | return emotes 48 | } 49 | 50 | func (t FfzFetcher) FetchGlobalEmotes() []temotes.Emote { 51 | return t.fetchEmotes("https://api.frankerfacez.com/v1/set/global", temotes.GlobalEmotesTtl, "ffz-global-emotes") 52 | } 53 | 54 | func (t FfzFetcher) FetchChannelEmotes(id temotes.TwitchUserId) []temotes.Emote { 55 | return t.fetchEmotes(fmt.Sprintf("https://api.frankerfacez.com/v1/room/id/%d", id), temotes.ChannelEmotesTtl, fmt.Sprintf("ffz-channel-emotes-%d", id)) 56 | } 57 | 58 | func (t FfzFetcher) parseEmoteUrls(emote ffzEmote) []temotes.EmoteUrl { 59 | var urls []temotes.EmoteUrl 60 | 61 | getEmoteSize := func(scale string) temotes.EmoteSize { 62 | switch scale { 63 | case "1": 64 | return temotes.Size1x 65 | case "2": 66 | return temotes.Size2x 67 | case "3": 68 | return temotes.Size3x 69 | case "4": 70 | return temotes.Size4x 71 | default: 72 | return temotes.Size1x 73 | } 74 | } 75 | 76 | var keys []string 77 | for k := range emote.Urls { 78 | keys = append(keys, k) 79 | } 80 | sort.Strings(keys) 81 | for _, scale := range keys { 82 | urls = append(urls, temotes.EmoteUrl{ 83 | Size: getEmoteSize(scale), 84 | Url: emote.Urls[scale], 85 | }) 86 | } 87 | 88 | return urls 89 | } 90 | 91 | func (t FfzFetcher) parseEmote(emote ffzEmote) temotes.Emote { 92 | return temotes.Emote{ 93 | Provider: temotes.ProviderFfz, 94 | Code: emote.Code, 95 | Urls: t.parseEmoteUrls(emote), 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /temotes/api/endpoints/channel_emote_proxy.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/gofiber/fiber/v2" 7 | "temotes/temotes" 8 | ) 9 | 10 | func GetChannelEmoteProxy(c *fiber.Ctx) error { 11 | emoteCode := c.Query("emote") 12 | if emoteCode == "" { 13 | return fiber.NewError(fiber.StatusBadRequest, "emote is required") 14 | } 15 | 16 | rawSize := c.Query("size") 17 | sizeNone := temotes.EmoteSize("none") 18 | size := sizeNone 19 | if rawSize != "" { 20 | for _, emSize := range []temotes.EmoteSize{temotes.Size4x, temotes.Size3x, temotes.Size2x, temotes.Size1x} { 21 | if emSize == temotes.EmoteSize(rawSize) { 22 | size = emSize 23 | } 24 | } 25 | 26 | if size == sizeNone { 27 | return fiber.NewError(fiber.StatusBadRequest, "invalid size") 28 | } 29 | } 30 | 31 | channelId, err := Helpers{}.GetTwitchUserId(c.Params("channel")) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | fetchers, err := Helpers{}.ParseServices(c.Params("services")) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | for _, fetcher := range *fetchers { 42 | emotes := fetcher.FetchChannelEmotes(channelId) 43 | emote, err := getEmoteWithCode(emotes, emoteCode) 44 | if err != nil { 45 | continue 46 | } 47 | 48 | var url string 49 | if size != sizeNone { 50 | url, err = getEmoteUrlForSize(emote, size) 51 | if err != nil { 52 | continue 53 | } 54 | } else { 55 | url, err = getHighestAvailableEmoteSizeUrl(emote) 56 | if err != nil { 57 | continue 58 | } 59 | } 60 | 61 | if url != "" { 62 | return redirectWithCache(c, url) 63 | } 64 | } 65 | 66 | return fiber.NewError(fiber.StatusNotFound, "emote not found") 67 | } 68 | 69 | func redirectWithCache(c *fiber.Ctx, url string) error { 70 | c.Set("Cache-Control", fmt.Sprintf("public, max-age=%d", temotes.GetConfig().ProxyMaxAge)) 71 | return c.Redirect(url, fiber.StatusTemporaryRedirect) 72 | } 73 | 74 | func getEmoteWithCode(emotes []temotes.Emote, code string) (temotes.Emote, error) { 75 | for _, emote := range emotes { 76 | if emote.Code == code { 77 | return emote, nil 78 | } 79 | } 80 | 81 | return temotes.Emote{}, errors.New("emote not found") 82 | } 83 | 84 | func getEmoteUrlForSize(emote temotes.Emote, size temotes.EmoteSize) (string, error) { 85 | for _, url := range emote.Urls { 86 | if url.Size == size { 87 | return url.Url, nil 88 | } 89 | } 90 | 91 | return "", errors.New("emote in specified size not found") 92 | } 93 | 94 | func getHighestAvailableEmoteSizeUrl(emote temotes.Emote) (string, error) { 95 | for _, size := range []temotes.EmoteSize{temotes.Size4x, temotes.Size3x, temotes.Size2x, temotes.Size1x} { 96 | url, err := getEmoteUrlForSize(emote, size) 97 | if err == nil { 98 | return url, nil 99 | } 100 | } 101 | 102 | return "", errors.New("emote seems to be unavailable in any size") 103 | } 104 | -------------------------------------------------------------------------------- /temotes/providers/7tv.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "temotes/temotes" 8 | ) 9 | 10 | type SevenTvFetcher struct{} 11 | 12 | type sevenTvGlobalEmoteResponse struct { 13 | Data struct { 14 | EmoteSet struct { 15 | Emotes []sevenTvEmoteResponse `json:"emotes"` 16 | } `json:"namedEmoteSet"` 17 | } `json:"data"` 18 | } 19 | 20 | type sevenTvChannelEmoteResponse struct { 21 | Data struct { 22 | UserByConnection struct { 23 | Id string `json:"id"` 24 | Connections []struct { 25 | Platform string `json:"platform"` 26 | EmoteSetID string `json:"emote_set_id"` 27 | } `json:"connections"` 28 | EmoteSets []struct { 29 | Id string `json:"id"` 30 | Emotes []sevenTvEmoteResponse `json:"emotes"` 31 | } `json:"emote_sets"` 32 | } `json:"userByConnection"` 33 | } `json:"data"` 34 | } 35 | 36 | type sevenTvEmoteResponse struct { 37 | Id string `json:"id"` 38 | Name string `json:"name"` 39 | } 40 | 41 | func (t SevenTvFetcher) FetchGlobalEmotes() []temotes.Emote { 42 | query := ` 43 | query NamedEmoteSet { 44 | namedEmoteSet(name: GLOBAL) { 45 | emotes { 46 | id 47 | name 48 | } 49 | } 50 | } 51 | ` 52 | 53 | response, err := temotes.CachedFetcher{}.FetchGqlData("https://7tv.io/v3/gql", query, temotes.GlobalEmotesTtl, "7tv-global-emotes") 54 | var emotes []temotes.Emote 55 | if err != nil { 56 | return emotes 57 | } 58 | 59 | var sevenTvEmotes sevenTvGlobalEmoteResponse 60 | jsonErr := json.Unmarshal(response, &sevenTvEmotes) 61 | if jsonErr != nil { 62 | log.Fatal(jsonErr) 63 | } 64 | 65 | for _, sevenTvEmote := range sevenTvEmotes.Data.EmoteSet.Emotes { 66 | emotes = append(emotes, t.parseEmote(sevenTvEmote)) 67 | } 68 | 69 | return emotes 70 | } 71 | 72 | func (t SevenTvFetcher) FetchChannelEmotes(id temotes.TwitchUserId) []temotes.Emote { 73 | query := fmt.Sprintf(` 74 | query UserByConnection { 75 | userByConnection(platform: TWITCH, id: "%d") { 76 | id 77 | connections { 78 | platform 79 | emote_set_id 80 | } 81 | emote_sets { 82 | id 83 | emotes { 84 | id 85 | name 86 | } 87 | } 88 | } 89 | } 90 | `, id) 91 | 92 | response, err := temotes.CachedFetcher{}.FetchGqlData("https://7tv.io/v3/gql", query, temotes.ChannelEmotesTtl, fmt.Sprintf("7tv-channel-%d", id)) 93 | var emotes []temotes.Emote 94 | if err != nil { 95 | return emotes 96 | } 97 | 98 | var parsedResponse sevenTvChannelEmoteResponse 99 | jsonErr := json.Unmarshal(response, &parsedResponse) 100 | if jsonErr != nil { 101 | log.Fatal(jsonErr) 102 | } 103 | 104 | activeEmoteSetId := parsedResponse.Data.UserByConnection.Connections[0].EmoteSetID 105 | 106 | for _, emoteSet := range parsedResponse.Data.UserByConnection.EmoteSets { 107 | if emoteSet.Id != activeEmoteSetId { 108 | continue 109 | } 110 | 111 | for _, sevenTvEmote := range emoteSet.Emotes { 112 | emotes = append(emotes, t.parseEmote(sevenTvEmote)) 113 | } 114 | } 115 | 116 | return emotes 117 | } 118 | 119 | func (t SevenTvFetcher) parseEmoteUrls(emote sevenTvEmoteResponse) []temotes.EmoteUrl { 120 | var urls []temotes.EmoteUrl 121 | 122 | getEmoteSize := func(scale string) temotes.EmoteSize { 123 | switch scale { 124 | case "1": 125 | return temotes.Size1x 126 | case "2": 127 | return temotes.Size2x 128 | case "3": 129 | return temotes.Size3x 130 | case "4": 131 | return temotes.Size4x 132 | default: 133 | return temotes.Size1x 134 | } 135 | } 136 | 137 | urlSizes := []string{"1", "2", "3", "4"} 138 | for _, size := range urlSizes { 139 | urls = append(urls, temotes.EmoteUrl{ 140 | Size: getEmoteSize(size), 141 | Url: fmt.Sprintf("https://cdn.7tv.app/emote/%s/%sx.webp", emote.Id, size), 142 | }) 143 | } 144 | 145 | return urls 146 | } 147 | 148 | func (t SevenTvFetcher) parseEmote(emote sevenTvEmoteResponse) temotes.Emote { 149 | return temotes.Emote{ 150 | Provider: temotes.Provider7tv, 151 | Code: emote.Name, 152 | Urls: t.parseEmoteUrls(emote), 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 2 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 3 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 4 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 7 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 8 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 9 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 10 | github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= 11 | github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= 12 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 13 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 14 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 15 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 16 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 17 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 18 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 19 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 20 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 21 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 22 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 23 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 24 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 25 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 26 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 27 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 28 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= 29 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= 30 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 31 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 32 | github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= 33 | github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= 34 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 35 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 36 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 37 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 38 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 39 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 40 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 41 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 44 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 45 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 46 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 47 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 48 | -------------------------------------------------------------------------------- /temotes/providers/twitch.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "strconv" 11 | "strings" 12 | "temotes/temotes" 13 | "time" 14 | ) 15 | 16 | type TwitchFetcher struct{} 17 | 18 | type twitchEmote struct { 19 | ID string `json:"id"` 20 | Code string `json:"name"` 21 | Themes []string `json:"theme_mode"` 22 | Scales []string `json:"scale"` 23 | } 24 | 25 | type twitchEmoteResponse struct { 26 | Data []twitchEmote `json:"data"` 27 | } 28 | 29 | func getAuthorizedRequest(url string) *http.Request { 30 | req, err := http.NewRequest(http.MethodGet, url, nil) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | req.Header.Set("Client-Id", temotes.GetConfig().TwitchClientID) 36 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", getAccessToken(false))) 37 | 38 | return req 39 | } 40 | 41 | func fetchAccessToken() string { 42 | req, err := http.NewRequest(http.MethodPost, "https://id.twitch.tv/oauth2/token", strings.NewReader(fmt.Sprintf("client_id=%s&client_secret=%s&grant_type=client_credentials", temotes.GetConfig().TwitchClientID, temotes.GetConfig().TwitchClientSecret))) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 48 | 49 | client := &http.Client{} 50 | resp, err := client.Do(req) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | defer func(Body io.ReadCloser) { 56 | err := Body.Close() 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | }(resp.Body) 61 | 62 | var token struct { 63 | AccessToken string `json:"access_token"` 64 | } 65 | 66 | err = json.NewDecoder(resp.Body).Decode(&token) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | return token.AccessToken 72 | } 73 | 74 | func getAccessToken(force bool) string { 75 | var accessToken string 76 | cache := temotes.CacheService{}.GetCacheClient() 77 | 78 | if force == true { 79 | cache.Delete("twitch_access_token") 80 | } else { 81 | accessToken, accessTokenFound := cache.Get("twitch_access_token") 82 | 83 | if accessTokenFound { 84 | return accessToken.(string) 85 | } 86 | } 87 | 88 | accessToken = fetchAccessToken() 89 | cache.Set("twitch_access_token", accessToken, time.Hour*24*7) 90 | 91 | return accessToken 92 | } 93 | 94 | func (t TwitchFetcher) fetchEmotes(url string, ttl time.Duration, cacheKey string) []temotes.Emote { 95 | request := getAuthorizedRequest(url) 96 | response, err := temotes.CachedFetcher{}.FetchDataRequest(request, ttl, cacheKey) 97 | var emotes []temotes.Emote 98 | if err != nil { 99 | return emotes 100 | } 101 | 102 | var twitchEmotes twitchEmoteResponse 103 | jsonErr := json.Unmarshal(response, &twitchEmotes) 104 | if jsonErr != nil { 105 | log.Fatal(jsonErr) 106 | } 107 | 108 | for _, twitchEmote := range twitchEmotes.Data { 109 | emotes = append(emotes, t.parseEmote(twitchEmote)) 110 | } 111 | 112 | return emotes 113 | } 114 | 115 | func (t TwitchFetcher) FetchGlobalEmotes() []temotes.Emote { 116 | return t.fetchEmotes("https://api.twitch.tv/helix/chat/emotes/global", temotes.GlobalEmotesTtl, "twitch-global-emotes") 117 | } 118 | 119 | func (t TwitchFetcher) FetchChannelEmotes(id temotes.TwitchUserId) []temotes.Emote { 120 | return t.fetchEmotes(fmt.Sprintf("https://api.twitch.tv/helix/chat/emotes?broadcaster_id=%d", id), temotes.ChannelEmotesTtl, fmt.Sprintf("twitch-channel-emotes-%d", id)) 121 | } 122 | 123 | func (t TwitchFetcher) parseEmoteUrls(emote twitchEmote) []temotes.EmoteUrl { 124 | var urls []temotes.EmoteUrl 125 | 126 | getEmoteSize := func(scale string) temotes.EmoteSize { 127 | switch scale { 128 | case "1.0": 129 | return temotes.Size1x 130 | case "2.0": 131 | return temotes.Size2x 132 | case "3.0": 133 | return temotes.Size4x 134 | default: 135 | return temotes.Size1x 136 | } 137 | } 138 | 139 | getEmoteTheme := func(themes []string) string { 140 | if len(themes) == 0 { 141 | panic("Twitch Emote Error: No themes defined") 142 | } 143 | 144 | if temotes.Contains(emote.Themes, "light") { 145 | return "light" 146 | } else { 147 | return emote.Themes[0] 148 | } 149 | } 150 | 151 | theme := getEmoteTheme(emote.Themes) 152 | for _, scale := range emote.Scales { 153 | urls = append(urls, temotes.EmoteUrl{ 154 | Size: getEmoteSize(scale), 155 | Url: fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v2/%s/default/%s/%s", emote.ID, theme, scale), 156 | }) 157 | } 158 | 159 | return urls 160 | } 161 | 162 | func (t TwitchFetcher) parseEmote(emote twitchEmote) temotes.Emote { 163 | return temotes.Emote{ 164 | Provider: temotes.ProviderTwitch, 165 | Code: emote.Code, 166 | Urls: t.parseEmoteUrls(emote), 167 | } 168 | } 169 | 170 | type twitchUsersResponse struct { 171 | Data []twitchUser `json:"data"` 172 | } 173 | 174 | type twitchUser struct { 175 | ID string `json:"id"` 176 | Login string `json:"login"` 177 | DisplayName string `json:"display_name"` 178 | Avatar string `json:"profile_image_url"` 179 | } 180 | 181 | type TwitchUser struct { 182 | ID temotes.TwitchUserId `json:"id"` 183 | Login string `json:"login"` 184 | DisplayName string `json:"display_name"` 185 | Avatar string `json:"avatar"` 186 | } 187 | 188 | func (t TwitchFetcher) FetchUserIdentifiers(identifier string) (*TwitchUser, error) { 189 | id, err := strconv.ParseInt(strings.ToLower(identifier), 10, 64) 190 | var request *http.Request 191 | var cacheKey string 192 | 193 | if id == 0 || err != nil { 194 | request = getAuthorizedRequest(fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", identifier)) 195 | cacheKey = fmt.Sprintf("twitch-user-identifiers-login-%s", identifier) 196 | } else { 197 | request = getAuthorizedRequest(fmt.Sprintf("https://api.twitch.tv/helix/users?id=%d", id)) 198 | cacheKey = fmt.Sprintf("twitch-user-identifiers-id-%d", id) 199 | } 200 | 201 | response, err := temotes.CachedFetcher{}.FetchDataRequest(request, temotes.TwitchIdTtl, cacheKey) 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | var twitchUsers twitchUsersResponse 207 | jsonErr := json.Unmarshal(response, &twitchUsers) 208 | if jsonErr != nil { 209 | log.Fatal(jsonErr) 210 | } 211 | 212 | if len(twitchUsers.Data) == 0 { 213 | return nil, errors.New("user not found") 214 | } 215 | 216 | userId, err := strconv.ParseInt(twitchUsers.Data[0].ID, 10, 64) 217 | if err != nil { 218 | return nil, errors.New("user not found") 219 | } 220 | 221 | user := &TwitchUser{ 222 | ID: temotes.TwitchUserId(userId), 223 | Login: twitchUsers.Data[0].Login, 224 | DisplayName: twitchUsers.Data[0].DisplayName, 225 | Avatar: twitchUsers.Data[0].Avatar, 226 | } 227 | 228 | return user, nil 229 | } 230 | -------------------------------------------------------------------------------- /agpl-3.0.md: -------------------------------------------------------------------------------- 1 | ### GNU AFFERO GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 19 November 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU Affero General Public License is a free, copyleft license for 14 | software and other kinds of works, specifically designed to ensure 15 | cooperation with the community in the case of network server software. 16 | 17 | The licenses for most software and other practical works are designed 18 | to take away your freedom to share and change the works. By contrast, 19 | our General Public Licenses are intended to guarantee your freedom to 20 | share and change all versions of a program--to make sure it remains 21 | free software for all its users. 22 | 23 | When we speak of free software, we are referring to freedom, not 24 | price. Our General Public Licenses are designed to make sure that you 25 | have the freedom to distribute copies of free software (and charge for 26 | them if you wish), that you receive source code or can get it if you 27 | want it, that you can change the software or use pieces of it in new 28 | free programs, and that you know you can do these things. 29 | 30 | Developers that use our General Public Licenses protect your rights 31 | with two steps: (1) assert copyright on the software, and (2) offer 32 | you this License which gives you legal permission to copy, distribute 33 | and/or modify the software. 34 | 35 | A secondary benefit of defending all users' freedom is that 36 | improvements made in alternate versions of the program, if they 37 | receive widespread use, become available for other developers to 38 | incorporate. Many developers of free software are heartened and 39 | encouraged by the resulting cooperation. However, in the case of 40 | software used on network servers, this result may fail to come about. 41 | The GNU General Public License permits making a modified version and 42 | letting the public access it on a server without ever releasing its 43 | source code to the public. 44 | 45 | The GNU Affero General Public License is designed specifically to 46 | ensure that, in such cases, the modified source code becomes available 47 | to the community. It requires the operator of a network server to 48 | provide the source code of the modified version running there to the 49 | users of that server. Therefore, public use of a modified version, on 50 | a publicly accessible server, gives the public access to the source 51 | code of the modified version. 52 | 53 | An older license, called the Affero General Public License and 54 | published by Affero, was designed to accomplish similar goals. This is 55 | a different license, not a version of the Affero GPL, but Affero has 56 | released a new version of the Affero GPL which permits relicensing 57 | under this license. 58 | 59 | The precise terms and conditions for copying, distribution and 60 | modification follow. 61 | 62 | ### TERMS AND CONDITIONS 63 | 64 | #### 0. Definitions. 65 | 66 | "This License" refers to version 3 of the GNU Affero General Public 67 | License. 68 | 69 | "Copyright" also means copyright-like laws that apply to other kinds 70 | of works, such as semiconductor masks. 71 | 72 | "The Program" refers to any copyrightable work licensed under this 73 | License. Each licensee is addressed as "you". "Licensees" and 74 | "recipients" may be individuals or organizations. 75 | 76 | To "modify" a work means to copy from or adapt all or part of the work 77 | in a fashion requiring copyright permission, other than the making of 78 | an exact copy. The resulting work is called a "modified version" of 79 | the earlier work or a work "based on" the earlier work. 80 | 81 | A "covered work" means either the unmodified Program or a work based 82 | on the Program. 83 | 84 | To "propagate" a work means to do anything with it that, without 85 | permission, would make you directly or secondarily liable for 86 | infringement under applicable copyright law, except executing it on a 87 | computer or modifying a private copy. Propagation includes copying, 88 | distribution (with or without modification), making available to the 89 | public, and in some countries other activities as well. 90 | 91 | To "convey" a work means any kind of propagation that enables other 92 | parties to make or receive copies. Mere interaction with a user 93 | through a computer network, with no transfer of a copy, is not 94 | conveying. 95 | 96 | An interactive user interface displays "Appropriate Legal Notices" to 97 | the extent that it includes a convenient and prominently visible 98 | feature that (1) displays an appropriate copyright notice, and (2) 99 | tells the user that there is no warranty for the work (except to the 100 | extent that warranties are provided), that licensees may convey the 101 | work under this License, and how to view a copy of this License. If 102 | the interface presents a list of user commands or options, such as a 103 | menu, a prominent item in the list meets this criterion. 104 | 105 | #### 1. Source Code. 106 | 107 | The "source code" for a work means the preferred form of the work for 108 | making modifications to it. "Object code" means any non-source form of 109 | a work. 110 | 111 | A "Standard Interface" means an interface that either is an official 112 | standard defined by a recognized standards body, or, in the case of 113 | interfaces specified for a particular programming language, one that 114 | is widely used among developers working in that language. 115 | 116 | The "System Libraries" of an executable work include anything, other 117 | than the work as a whole, that (a) is included in the normal form of 118 | packaging a Major Component, but which is not part of that Major 119 | Component, and (b) serves only to enable use of the work with that 120 | Major Component, or to implement a Standard Interface for which an 121 | implementation is available to the public in source code form. A 122 | "Major Component", in this context, means a major essential component 123 | (kernel, window system, and so on) of the specific operating system 124 | (if any) on which the executable work runs, or a compiler used to 125 | produce the work, or an object code interpreter used to run it. 126 | 127 | The "Corresponding Source" for a work in object code form means all 128 | the source code needed to generate, install, and (for an executable 129 | work) run the object code and to modify the work, including scripts to 130 | control those activities. However, it does not include the work's 131 | System Libraries, or general-purpose tools or generally available free 132 | programs which are used unmodified in performing those activities but 133 | which are not part of the work. For example, Corresponding Source 134 | includes interface definition files associated with source files for 135 | the work, and the source code for shared libraries and dynamically 136 | linked subprograms that the work is specifically designed to require, 137 | such as by intimate data communication or control flow between those 138 | subprograms and other parts of the work. 139 | 140 | The Corresponding Source need not include anything that users can 141 | regenerate automatically from other parts of the Corresponding Source. 142 | 143 | The Corresponding Source for a work in source code form is that same 144 | work. 145 | 146 | #### 2. Basic Permissions. 147 | 148 | All rights granted under this License are granted for the term of 149 | copyright on the Program, and are irrevocable provided the stated 150 | conditions are met. This License explicitly affirms your unlimited 151 | permission to run the unmodified Program. The output from running a 152 | covered work is covered by this License only if the output, given its 153 | content, constitutes a covered work. This License acknowledges your 154 | rights of fair use or other equivalent, as provided by copyright law. 155 | 156 | You may make, run and propagate covered works that you do not convey, 157 | without conditions so long as your license otherwise remains in force. 158 | You may convey covered works to others for the sole purpose of having 159 | them make modifications exclusively for you, or provide you with 160 | facilities for running those works, provided that you comply with the 161 | terms of this License in conveying all material for which you do not 162 | control copyright. Those thus making or running the covered works for 163 | you must do so exclusively on your behalf, under your direction and 164 | control, on terms that prohibit them from making any copies of your 165 | copyrighted material outside their relationship with you. 166 | 167 | Conveying under any other circumstances is permitted solely under the 168 | conditions stated below. Sublicensing is not allowed; section 10 makes 169 | it unnecessary. 170 | 171 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 172 | 173 | No covered work shall be deemed part of an effective technological 174 | measure under any applicable law fulfilling obligations under article 175 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 176 | similar laws prohibiting or restricting circumvention of such 177 | measures. 178 | 179 | When you convey a covered work, you waive any legal power to forbid 180 | circumvention of technological measures to the extent such 181 | circumvention is effected by exercising rights under this License with 182 | respect to the covered work, and you disclaim any intention to limit 183 | operation or modification of the work as a means of enforcing, against 184 | the work's users, your or third parties' legal rights to forbid 185 | circumvention of technological measures. 186 | 187 | #### 4. Conveying Verbatim Copies. 188 | 189 | You may convey verbatim copies of the Program's source code as you 190 | receive it, in any medium, provided that you conspicuously and 191 | appropriately publish on each copy an appropriate copyright notice; 192 | keep intact all notices stating that this License and any 193 | non-permissive terms added in accord with section 7 apply to the code; 194 | keep intact all notices of the absence of any warranty; and give all 195 | recipients a copy of this License along with the Program. 196 | 197 | You may charge any price or no price for each copy that you convey, 198 | and you may offer support or warranty protection for a fee. 199 | 200 | #### 5. Conveying Modified Source Versions. 201 | 202 | You may convey a work based on the Program, or the modifications to 203 | produce it from the Program, in the form of source code under the 204 | terms of section 4, provided that you also meet all of these 205 | conditions: 206 | 207 | - a) The work must carry prominent notices stating that you modified 208 | it, and giving a relevant date. 209 | - b) The work must carry prominent notices stating that it is 210 | released under this License and any conditions added under 211 | section 7. This requirement modifies the requirement in section 4 212 | to "keep intact all notices". 213 | - c) You must license the entire work, as a whole, under this 214 | License to anyone who comes into possession of a copy. This 215 | License will therefore apply, along with any applicable section 7 216 | additional terms, to the whole of the work, and all its parts, 217 | regardless of how they are packaged. This License gives no 218 | permission to license the work in any other way, but it does not 219 | invalidate such permission if you have separately received it. 220 | - d) If the work has interactive user interfaces, each must display 221 | Appropriate Legal Notices; however, if the Program has interactive 222 | interfaces that do not display Appropriate Legal Notices, your 223 | work need not make them do so. 224 | 225 | A compilation of a covered work with other separate and independent 226 | works, which are not by their nature extensions of the covered work, 227 | and which are not combined with it such as to form a larger program, 228 | in or on a volume of a storage or distribution medium, is called an 229 | "aggregate" if the compilation and its resulting copyright are not 230 | used to limit the access or legal rights of the compilation's users 231 | beyond what the individual works permit. Inclusion of a covered work 232 | in an aggregate does not cause this License to apply to the other 233 | parts of the aggregate. 234 | 235 | #### 6. Conveying Non-Source Forms. 236 | 237 | You may convey a covered work in object code form under the terms of 238 | sections 4 and 5, provided that you also convey the machine-readable 239 | Corresponding Source under the terms of this License, in one of these 240 | ways: 241 | 242 | - a) Convey the object code in, or embodied in, a physical product 243 | (including a physical distribution medium), accompanied by the 244 | Corresponding Source fixed on a durable physical medium 245 | customarily used for software interchange. 246 | - b) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by a 248 | written offer, valid for at least three years and valid for as 249 | long as you offer spare parts or customer support for that product 250 | model, to give anyone who possesses the object code either (1) a 251 | copy of the Corresponding Source for all the software in the 252 | product that is covered by this License, on a durable physical 253 | medium customarily used for software interchange, for a price no 254 | more than your reasonable cost of physically performing this 255 | conveying of source, or (2) access to copy the Corresponding 256 | Source from a network server at no charge. 257 | - c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | - d) Convey the object code by offering access from a designated 263 | place (gratis or for a charge), and offer equivalent access to the 264 | Corresponding Source in the same way through the same place at no 265 | further charge. You need not require recipients to copy the 266 | Corresponding Source along with the object code. If the place to 267 | copy the object code is a network server, the Corresponding Source 268 | may be on a different server (operated by you or a third party) 269 | that supports equivalent copying facilities, provided you maintain 270 | clear directions next to the object code saying where to find the 271 | Corresponding Source. Regardless of what server hosts the 272 | Corresponding Source, you remain obligated to ensure that it is 273 | available for as long as needed to satisfy these requirements. 274 | - e) Convey the object code using peer-to-peer transmission, 275 | provided you inform other peers where the object code and 276 | Corresponding Source of the work are being offered to the general 277 | public at no charge under subsection 6d. 278 | 279 | A separable portion of the object code, whose source code is excluded 280 | from the Corresponding Source as a System Library, need not be 281 | included in conveying the object code work. 282 | 283 | A "User Product" is either (1) a "consumer product", which means any 284 | tangible personal property which is normally used for personal, 285 | family, or household purposes, or (2) anything designed or sold for 286 | incorporation into a dwelling. In determining whether a product is a 287 | consumer product, doubtful cases shall be resolved in favor of 288 | coverage. For a particular product received by a particular user, 289 | "normally used" refers to a typical or common use of that class of 290 | product, regardless of the status of the particular user or of the way 291 | in which the particular user actually uses, or expects or is expected 292 | to use, the product. A product is a consumer product regardless of 293 | whether the product has substantial commercial, industrial or 294 | non-consumer uses, unless such uses represent the only significant 295 | mode of use of the product. 296 | 297 | "Installation Information" for a User Product means any methods, 298 | procedures, authorization keys, or other information required to 299 | install and execute modified versions of a covered work in that User 300 | Product from a modified version of its Corresponding Source. The 301 | information must suffice to ensure that the continued functioning of 302 | the modified object code is in no case prevented or interfered with 303 | solely because modification has been made. 304 | 305 | If you convey an object code work under this section in, or with, or 306 | specifically for use in, a User Product, and the conveying occurs as 307 | part of a transaction in which the right of possession and use of the 308 | User Product is transferred to the recipient in perpetuity or for a 309 | fixed term (regardless of how the transaction is characterized), the 310 | Corresponding Source conveyed under this section must be accompanied 311 | by the Installation Information. But this requirement does not apply 312 | if neither you nor any third party retains the ability to install 313 | modified object code on the User Product (for example, the work has 314 | been installed in ROM). 315 | 316 | The requirement to provide Installation Information does not include a 317 | requirement to continue to provide support service, warranty, or 318 | updates for a work that has been modified or installed by the 319 | recipient, or for the User Product in which it has been modified or 320 | installed. Access to a network may be denied when the modification 321 | itself materially and adversely affects the operation of the network 322 | or violates the rules and protocols for communication across the 323 | network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | #### 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders 351 | of that material) supplement the terms of this License with terms: 352 | 353 | - a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | - b) Requiring preservation of specified reasonable legal notices or 356 | author attributions in that material or in the Appropriate Legal 357 | Notices displayed by works containing it; or 358 | - c) Prohibiting misrepresentation of the origin of that material, 359 | or requiring that modified versions of such material be marked in 360 | reasonable ways as different from the original version; or 361 | - d) Limiting the use for publicity purposes of names of licensors 362 | or authors of the material; or 363 | - e) Declining to grant rights under trademark law for use of some 364 | trade names, trademarks, or service marks; or 365 | - f) Requiring indemnification of licensors and authors of that 366 | material by anyone who conveys the material (or modified versions 367 | of it) with contractual assumptions of liability to the recipient, 368 | for any liability that these contractual assumptions directly 369 | impose on those licensors and authors. 370 | 371 | All other non-permissive additional terms are considered "further 372 | restrictions" within the meaning of section 10. If the Program as you 373 | received it, or any part of it, contains a notice stating that it is 374 | governed by this License along with a term that is a further 375 | restriction, you may remove that term. If a license document contains 376 | a further restriction but permits relicensing or conveying under this 377 | License, you may add to a covered work material governed by the terms 378 | of that license document, provided that the further restriction does 379 | not survive such relicensing or conveying. 380 | 381 | If you add terms to a covered work in accord with this section, you 382 | must place, in the relevant source files, a statement of the 383 | additional terms that apply to those files, or a notice indicating 384 | where to find the applicable terms. 385 | 386 | Additional terms, permissive or non-permissive, may be stated in the 387 | form of a separately written license, or stated as exceptions; the 388 | above requirements apply either way. 389 | 390 | #### 8. Termination. 391 | 392 | You may not propagate or modify a covered work except as expressly 393 | provided under this License. Any attempt otherwise to propagate or 394 | modify it is void, and will automatically terminate your rights under 395 | this License (including any patent licenses granted under the third 396 | paragraph of section 11). 397 | 398 | However, if you cease all violation of this License, then your license 399 | from a particular copyright holder is reinstated (a) provisionally, 400 | unless and until the copyright holder explicitly and finally 401 | terminates your license, and (b) permanently, if the copyright holder 402 | fails to notify you of the violation by some reasonable means prior to 403 | 60 days after the cessation. 404 | 405 | Moreover, your license from a particular copyright holder is 406 | reinstated permanently if the copyright holder notifies you of the 407 | violation by some reasonable means, this is the first time you have 408 | received notice of violation of this License (for any work) from that 409 | copyright holder, and you cure the violation prior to 30 days after 410 | your receipt of the notice. 411 | 412 | Termination of your rights under this section does not terminate the 413 | licenses of parties who have received copies or rights from you under 414 | this License. If your rights have been terminated and not permanently 415 | reinstated, you do not qualify to receive new licenses for the same 416 | material under section 10. 417 | 418 | #### 9. Acceptance Not Required for Having Copies. 419 | 420 | You are not required to accept this License in order to receive or run 421 | a copy of the Program. Ancillary propagation of a covered work 422 | occurring solely as a consequence of using peer-to-peer transmission 423 | to receive a copy likewise does not require acceptance. However, 424 | nothing other than this License grants you permission to propagate or 425 | modify any covered work. These actions infringe copyright if you do 426 | not accept this License. Therefore, by modifying or propagating a 427 | covered work, you indicate your acceptance of this License to do so. 428 | 429 | #### 10. Automatic Licensing of Downstream Recipients. 430 | 431 | Each time you convey a covered work, the recipient automatically 432 | receives a license from the original licensors, to run, modify and 433 | propagate that work, subject to this License. You are not responsible 434 | for enforcing compliance by third parties with this License. 435 | 436 | An "entity transaction" is a transaction transferring control of an 437 | organization, or substantially all assets of one, or subdividing an 438 | organization, or merging organizations. If propagation of a covered 439 | work results from an entity transaction, each party to that 440 | transaction who receives a copy of the work also receives whatever 441 | licenses to the work the party's predecessor in interest had or could 442 | give under the previous paragraph, plus a right to possession of the 443 | Corresponding Source of the work from the predecessor in interest, if 444 | the predecessor has it or can get it with reasonable efforts. 445 | 446 | You may not impose any further restrictions on the exercise of the 447 | rights granted or affirmed under this License. For example, you may 448 | not impose a license fee, royalty, or other charge for exercise of 449 | rights granted under this License, and you may not initiate litigation 450 | (including a cross-claim or counterclaim in a lawsuit) alleging that 451 | any patent claim is infringed by making, using, selling, offering for 452 | sale, or importing the Program or any portion of it. 453 | 454 | #### 11. Patents. 455 | 456 | A "contributor" is a copyright holder who authorizes use under this 457 | License of the Program or a work on which the Program is based. The 458 | work thus licensed is called the contributor's "contributor version". 459 | 460 | A contributor's "essential patent claims" are all patent claims owned 461 | or controlled by the contributor, whether already acquired or 462 | hereafter acquired, that would be infringed by some manner, permitted 463 | by this License, of making, using, or selling its contributor version, 464 | but do not include claims that would be infringed only as a 465 | consequence of further modification of the contributor version. For 466 | purposes of this definition, "control" includes the right to grant 467 | patent sublicenses in a manner consistent with the requirements of 468 | this License. 469 | 470 | Each contributor grants you a non-exclusive, worldwide, royalty-free 471 | patent license under the contributor's essential patent claims, to 472 | make, use, sell, offer for sale, import and otherwise run, modify and 473 | propagate the contents of its contributor version. 474 | 475 | In the following three paragraphs, a "patent license" is any express 476 | agreement or commitment, however denominated, not to enforce a patent 477 | (such as an express permission to practice a patent or covenant not to 478 | sue for patent infringement). To "grant" such a patent license to a 479 | party means to make such an agreement or commitment not to enforce a 480 | patent against the party. 481 | 482 | If you convey a covered work, knowingly relying on a patent license, 483 | and the Corresponding Source of the work is not available for anyone 484 | to copy, free of charge and under the terms of this License, through a 485 | publicly available network server or other readily accessible means, 486 | then you must either (1) cause the Corresponding Source to be so 487 | available, or (2) arrange to deprive yourself of the benefit of the 488 | patent license for this particular work, or (3) arrange, in a manner 489 | consistent with the requirements of this License, to extend the patent 490 | license to downstream recipients. "Knowingly relying" means you have 491 | actual knowledge that, but for the patent license, your conveying the 492 | covered work in a country, or your recipient's use of the covered work 493 | in a country, would infringe one or more identifiable patents in that 494 | country that you have reason to believe are valid. 495 | 496 | If, pursuant to or in connection with a single transaction or 497 | arrangement, you convey, or propagate by procuring conveyance of, a 498 | covered work, and grant a patent license to some of the parties 499 | receiving the covered work authorizing them to use, propagate, modify 500 | or convey a specific copy of the covered work, then the patent license 501 | you grant is automatically extended to all recipients of the covered 502 | work and works based on it. 503 | 504 | A patent license is "discriminatory" if it does not include within the 505 | scope of its coverage, prohibits the exercise of, or is conditioned on 506 | the non-exercise of one or more of the rights that are specifically 507 | granted under this License. You may not convey a covered work if you 508 | are a party to an arrangement with a third party that is in the 509 | business of distributing software, under which you make payment to the 510 | third party based on the extent of your activity of conveying the 511 | work, and under which the third party grants, to any of the parties 512 | who would receive the covered work from you, a discriminatory patent 513 | license (a) in connection with copies of the covered work conveyed by 514 | you (or copies made from those copies), or (b) primarily for and in 515 | connection with specific products or compilations that contain the 516 | covered work, unless you entered into that arrangement, or that patent 517 | license was granted, prior to 28 March 2007. 518 | 519 | Nothing in this License shall be construed as excluding or limiting 520 | any implied license or other defenses to infringement that may 521 | otherwise be available to you under applicable patent law. 522 | 523 | #### 12. No Surrender of Others' Freedom. 524 | 525 | If conditions are imposed on you (whether by court order, agreement or 526 | otherwise) that contradict the conditions of this License, they do not 527 | excuse you from the conditions of this License. If you cannot convey a 528 | covered work so as to satisfy simultaneously your obligations under 529 | this License and any other pertinent obligations, then as a 530 | consequence you may not convey it at all. For example, if you agree to 531 | terms that obligate you to collect a royalty for further conveying 532 | from those to whom you convey the Program, the only way you could 533 | satisfy both those terms and this License would be to refrain entirely 534 | from conveying the Program. 535 | 536 | #### 13. Remote Network Interaction; Use with the GNU General Public License. 537 | 538 | Notwithstanding any other provision of this License, if you modify the 539 | Program, your modified version must prominently offer all users 540 | interacting with it remotely through a computer network (if your 541 | version supports such interaction) an opportunity to receive the 542 | Corresponding Source of your version by providing access to the 543 | Corresponding Source from a network server at no charge, through some 544 | standard or customary means of facilitating copying of software. This 545 | Corresponding Source shall include the Corresponding Source for any 546 | work covered by version 3 of the GNU General Public License that is 547 | incorporated pursuant to the following paragraph. 548 | 549 | Notwithstanding any other provision of this License, you have 550 | permission to link or combine any covered work with a work licensed 551 | under version 3 of the GNU General Public License into a single 552 | combined work, and to convey the resulting work. The terms of this 553 | License will continue to apply to the part which is the covered work, 554 | but the work with which it is combined will remain governed by version 555 | 3 of the GNU General Public License. 556 | 557 | #### 14. Revised Versions of this License. 558 | 559 | The Free Software Foundation may publish revised and/or new versions 560 | of the GNU Affero General Public License from time to time. Such new 561 | versions will be similar in spirit to the present version, but may 562 | differ in detail to address new problems or concerns. 563 | 564 | Each version is given a distinguishing version number. If the Program 565 | specifies that a certain numbered version of the GNU Affero General 566 | Public License "or any later version" applies to it, you have the 567 | option of following the terms and conditions either of that numbered 568 | version or of any later version published by the Free Software 569 | Foundation. If the Program does not specify a version number of the 570 | GNU Affero General Public License, you may choose any version ever 571 | published by the Free Software Foundation. 572 | 573 | If the Program specifies that a proxy can decide which future versions 574 | of the GNU Affero General Public License can be used, that proxy's 575 | public statement of acceptance of a version permanently authorizes you 576 | to choose that version for the Program. 577 | 578 | Later license versions may give you additional or different 579 | permissions. However, no additional obligations are imposed on any 580 | author or copyright holder as a result of your choosing to follow a 581 | later version. 582 | 583 | #### 15. Disclaimer of Warranty. 584 | 585 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 586 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 587 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 588 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 589 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 590 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 591 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 592 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 593 | CORRECTION. 594 | 595 | #### 16. Limitation of Liability. 596 | 597 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 598 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 599 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 600 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 601 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 602 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 603 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 604 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 605 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 606 | 607 | #### 17. Interpretation of Sections 15 and 16. 608 | 609 | If the disclaimer of warranty and limitation of liability provided 610 | above cannot be given local legal effect according to their terms, 611 | reviewing courts shall apply local law that most closely approximates 612 | an absolute waiver of all civil liability in connection with the 613 | Program, unless a warranty or assumption of liability accompanies a 614 | copy of the Program in return for a fee. 615 | 616 | END OF TERMS AND CONDITIONS 617 | 618 | ### How to Apply These Terms to Your New Programs 619 | 620 | If you develop a new program, and you want it to be of the greatest 621 | possible use to the public, the best way to achieve this is to make it 622 | free software which everyone can redistribute and change under these 623 | terms. 624 | 625 | To do so, attach the following notices to the program. It is safest to 626 | attach them to the start of each source file to most effectively state 627 | the exclusion of warranty; and each file should have at least the 628 | "copyright" line and a pointer to where the full notice is found. 629 | 630 | 631 | Copyright (C) 632 | 633 | This program is free software: you can redistribute it and/or modify 634 | it under the terms of the GNU Affero General Public License as 635 | published by the Free Software Foundation, either version 3 of the 636 | License, or (at your option) any later version. 637 | 638 | This program is distributed in the hope that it will be useful, 639 | but WITHOUT ANY WARRANTY; without even the implied warranty of 640 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 641 | GNU Affero General Public License for more details. 642 | 643 | You should have received a copy of the GNU Affero General Public License 644 | along with this program. If not, see . 645 | 646 | Also add information on how to contact you by electronic and paper 647 | mail. 648 | 649 | If your software can interact with users remotely through a computer 650 | network, you should also make sure that it provides a way for users to 651 | get its source. For example, if your program is a web application, its 652 | interface could display a "Source" link that leads users to an archive 653 | of the code. There are many ways you could offer source, and different 654 | solutions will be better for different programs; see section 13 for 655 | the specific requirements. 656 | 657 | You should also get your employer (if you work as a programmer) or 658 | school, if any, to sign a "copyright disclaimer" for the program, if 659 | necessary. For more information on this, and how to apply and follow 660 | the GNU AGPL, see . -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | tEmotes is an open-source API providing unified Twitch emotes data 2 | 3 | Copyright (C) 2021, Adrian Zmenda 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as 7 | published by the Free Software Foundation, either version 3 of the 8 | License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | 18 | Adrian Zmenda 19 | adrian@zmenda.com 20 | 21 | 22 | GNU AFFERO GENERAL PUBLIC LICENSE 23 | Version 3, 19 November 2007 24 | 25 | Copyright (C) 2007 Free Software Foundation, Inc. 26 | Everyone is permitted to copy and distribute verbatim copies 27 | of this license document, but changing it is not allowed. 28 | 29 | Preamble 30 | 31 | The GNU Affero General Public License is a free, copyleft license for 32 | software and other kinds of works, specifically designed to ensure 33 | cooperation with the community in the case of network server software. 34 | 35 | The licenses for most software and other practical works are designed 36 | to take away your freedom to share and change the works. By contrast, 37 | our General Public Licenses are intended to guarantee your freedom to 38 | share and change all versions of a program--to make sure it remains free 39 | software for all its users. 40 | 41 | When we speak of free software, we are referring to freedom, not 42 | price. Our General Public Licenses are designed to make sure that you 43 | have the freedom to distribute copies of free software (and charge for 44 | them if you wish), that you receive source code or can get it if you 45 | want it, that you can change the software or use pieces of it in new 46 | free programs, and that you know you can do these things. 47 | 48 | Developers that use our General Public Licenses protect your rights 49 | with two steps: (1) assert copyright on the software, and (2) offer 50 | you this License which gives you legal permission to copy, distribute 51 | and/or modify the software. 52 | 53 | A secondary benefit of defending all users' freedom is that 54 | improvements made in alternate versions of the program, if they 55 | receive widespread use, become available for other developers to 56 | incorporate. Many developers of free software are heartened and 57 | encouraged by the resulting cooperation. However, in the case of 58 | software used on network servers, this result may fail to come about. 59 | The GNU General Public License permits making a modified version and 60 | letting the public access it on a server without ever releasing its 61 | source code to the public. 62 | 63 | The GNU Affero General Public License is designed specifically to 64 | ensure that, in such cases, the modified source code becomes available 65 | to the community. It requires the operator of a network server to 66 | provide the source code of the modified version running there to the 67 | users of that server. Therefore, public use of a modified version, on 68 | a publicly accessible server, gives the public access to the source 69 | code of the modified version. 70 | 71 | An older license, called the Affero General Public License and 72 | published by Affero, was designed to accomplish similar goals. This is 73 | a different license, not a version of the Affero GPL, but Affero has 74 | released a new version of the Affero GPL which permits relicensing under 75 | this license. 76 | 77 | The precise terms and conditions for copying, distribution and 78 | modification follow. 79 | 80 | TERMS AND CONDITIONS 81 | 82 | 0. Definitions. 83 | 84 | "This License" refers to version 3 of the GNU Affero General Public License. 85 | 86 | "Copyright" also means copyright-like laws that apply to other kinds of 87 | works, such as semiconductor masks. 88 | 89 | "The Program" refers to any copyrightable work licensed under this 90 | License. Each licensee is addressed as "you". "Licensees" and 91 | "recipients" may be individuals or organizations. 92 | 93 | To "modify" a work means to copy from or adapt all or part of the work 94 | in a fashion requiring copyright permission, other than the making of an 95 | exact copy. The resulting work is called a "modified version" of the 96 | earlier work or a work "based on" the earlier work. 97 | 98 | A "covered work" means either the unmodified Program or a work based 99 | on the Program. 100 | 101 | To "propagate" a work means to do anything with it that, without 102 | permission, would make you directly or secondarily liable for 103 | infringement under applicable copyright law, except executing it on a 104 | computer or modifying a private copy. Propagation includes copying, 105 | distribution (with or without modification), making available to the 106 | public, and in some countries other activities as well. 107 | 108 | To "convey" a work means any kind of propagation that enables other 109 | parties to make or receive copies. Mere interaction with a user through 110 | a computer network, with no transfer of a copy, is not conveying. 111 | 112 | An interactive user interface displays "Appropriate Legal Notices" 113 | to the extent that it includes a convenient and prominently visible 114 | feature that (1) displays an appropriate copyright notice, and (2) 115 | tells the user that there is no warranty for the work (except to the 116 | extent that warranties are provided), that licensees may convey the 117 | work under this License, and how to view a copy of this License. If 118 | the interface presents a list of user commands or options, such as a 119 | menu, a prominent item in the list meets this criterion. 120 | 121 | 1. Source Code. 122 | 123 | The "source code" for a work means the preferred form of the work 124 | for making modifications to it. "Object code" means any non-source 125 | form of a work. 126 | 127 | A "Standard Interface" means an interface that either is an official 128 | standard defined by a recognized standards body, or, in the case of 129 | interfaces specified for a particular programming language, one that 130 | is widely used among developers working in that language. 131 | 132 | The "System Libraries" of an executable work include anything, other 133 | than the work as a whole, that (a) is included in the normal form of 134 | packaging a Major Component, but which is not part of that Major 135 | Component, and (b) serves only to enable use of the work with that 136 | Major Component, or to implement a Standard Interface for which an 137 | implementation is available to the public in source code form. A 138 | "Major Component", in this context, means a major essential component 139 | (kernel, window system, and so on) of the specific operating system 140 | (if any) on which the executable work runs, or a compiler used to 141 | produce the work, or an object code interpreter used to run it. 142 | 143 | The "Corresponding Source" for a work in object code form means all 144 | the source code needed to generate, install, and (for an executable 145 | work) run the object code and to modify the work, including scripts to 146 | control those activities. However, it does not include the work's 147 | System Libraries, or general-purpose tools or generally available free 148 | programs which are used unmodified in performing those activities but 149 | which are not part of the work. For example, Corresponding Source 150 | includes interface definition files associated with source files for 151 | the work, and the source code for shared libraries and dynamically 152 | linked subprograms that the work is specifically designed to require, 153 | such as by intimate data communication or control flow between those 154 | subprograms and other parts of the work. 155 | 156 | The Corresponding Source need not include anything that users 157 | can regenerate automatically from other parts of the Corresponding 158 | Source. 159 | 160 | The Corresponding Source for a work in source code form is that 161 | same work. 162 | 163 | 2. Basic Permissions. 164 | 165 | All rights granted under this License are granted for the term of 166 | copyright on the Program, and are irrevocable provided the stated 167 | conditions are met. This License explicitly affirms your unlimited 168 | permission to run the unmodified Program. The output from running a 169 | covered work is covered by this License only if the output, given its 170 | content, constitutes a covered work. This License acknowledges your 171 | rights of fair use or other equivalent, as provided by copyright law. 172 | 173 | You may make, run and propagate covered works that you do not 174 | convey, without conditions so long as your license otherwise remains 175 | in force. You may convey covered works to others for the sole purpose 176 | of having them make modifications exclusively for you, or provide you 177 | with facilities for running those works, provided that you comply with 178 | the terms of this License in conveying all material for which you do 179 | not control copyright. Those thus making or running the covered works 180 | for you must do so exclusively on your behalf, under your direction 181 | and control, on terms that prohibit them from making any copies of 182 | your copyrighted material outside their relationship with you. 183 | 184 | Conveying under any other circumstances is permitted solely under 185 | the conditions stated below. Sublicensing is not allowed; section 10 186 | makes it unnecessary. 187 | 188 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 189 | 190 | No covered work shall be deemed part of an effective technological 191 | measure under any applicable law fulfilling obligations under article 192 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 193 | similar laws prohibiting or restricting circumvention of such 194 | measures. 195 | 196 | When you convey a covered work, you waive any legal power to forbid 197 | circumvention of technological measures to the extent such circumvention 198 | is effected by exercising rights under this License with respect to 199 | the covered work, and you disclaim any intention to limit operation or 200 | modification of the work as a means of enforcing, against the work's 201 | users, your or third parties' legal rights to forbid circumvention of 202 | technological measures. 203 | 204 | 4. Conveying Verbatim Copies. 205 | 206 | You may convey verbatim copies of the Program's source code as you 207 | receive it, in any medium, provided that you conspicuously and 208 | appropriately publish on each copy an appropriate copyright notice; 209 | keep intact all notices stating that this License and any 210 | non-permissive terms added in accord with section 7 apply to the code; 211 | keep intact all notices of the absence of any warranty; and give all 212 | recipients a copy of this License along with the Program. 213 | 214 | You may charge any price or no price for each copy that you convey, 215 | and you may offer support or warranty protection for a fee. 216 | 217 | 5. Conveying Modified Source Versions. 218 | 219 | You may convey a work based on the Program, or the modifications to 220 | produce it from the Program, in the form of source code under the 221 | terms of section 4, provided that you also meet all of these conditions: 222 | 223 | a) The work must carry prominent notices stating that you modified 224 | it, and giving a relevant date. 225 | 226 | b) The work must carry prominent notices stating that it is 227 | released under this License and any conditions added under section 228 | 7. This requirement modifies the requirement in section 4 to 229 | "keep intact all notices". 230 | 231 | c) You must license the entire work, as a whole, under this 232 | License to anyone who comes into possession of a copy. This 233 | License will therefore apply, along with any applicable section 7 234 | additional terms, to the whole of the work, and all its parts, 235 | regardless of how they are packaged. This License gives no 236 | permission to license the work in any other way, but it does not 237 | invalidate such permission if you have separately received it. 238 | 239 | d) If the work has interactive user interfaces, each must display 240 | Appropriate Legal Notices; however, if the Program has interactive 241 | interfaces that do not display Appropriate Legal Notices, your 242 | work need not make them do so. 243 | 244 | A compilation of a covered work with other separate and independent 245 | works, which are not by their nature extensions of the covered work, 246 | and which are not combined with it such as to form a larger program, 247 | in or on a volume of a storage or distribution medium, is called an 248 | "aggregate" if the compilation and its resulting copyright are not 249 | used to limit the access or legal rights of the compilation's users 250 | beyond what the individual works permit. Inclusion of a covered work 251 | in an aggregate does not cause this License to apply to the other 252 | parts of the aggregate. 253 | 254 | 6. Conveying Non-Source Forms. 255 | 256 | You may convey a covered work in object code form under the terms 257 | of sections 4 and 5, provided that you also convey the 258 | machine-readable Corresponding Source under the terms of this License, 259 | in one of these ways: 260 | 261 | a) Convey the object code in, or embodied in, a physical product 262 | (including a physical distribution medium), accompanied by the 263 | Corresponding Source fixed on a durable physical medium 264 | customarily used for software interchange. 265 | 266 | b) Convey the object code in, or embodied in, a physical product 267 | (including a physical distribution medium), accompanied by a 268 | written offer, valid for at least three years and valid for as 269 | long as you offer spare parts or customer support for that product 270 | model, to give anyone who possesses the object code either (1) a 271 | copy of the Corresponding Source for all the software in the 272 | product that is covered by this License, on a durable physical 273 | medium customarily used for software interchange, for a price no 274 | more than your reasonable cost of physically performing this 275 | conveying of source, or (2) access to copy the 276 | Corresponding Source from a network server at no charge. 277 | 278 | c) Convey individual copies of the object code with a copy of the 279 | written offer to provide the Corresponding Source. This 280 | alternative is allowed only occasionally and noncommercially, and 281 | only if you received the object code with such an offer, in accord 282 | with subsection 6b. 283 | 284 | d) Convey the object code by offering access from a designated 285 | place (gratis or for a charge), and offer equivalent access to the 286 | Corresponding Source in the same way through the same place at no 287 | further charge. You need not require recipients to copy the 288 | Corresponding Source along with the object code. If the place to 289 | copy the object code is a network server, the Corresponding Source 290 | may be on a different server (operated by you or a third party) 291 | that supports equivalent copying facilities, provided you maintain 292 | clear directions next to the object code saying where to find the 293 | Corresponding Source. Regardless of what server hosts the 294 | Corresponding Source, you remain obligated to ensure that it is 295 | available for as long as needed to satisfy these requirements. 296 | 297 | e) Convey the object code using peer-to-peer transmission, provided 298 | you inform other peers where the object code and Corresponding 299 | Source of the work are being offered to the general public at no 300 | charge under subsection 6d. 301 | 302 | A separable portion of the object code, whose source code is excluded 303 | from the Corresponding Source as a System Library, need not be 304 | included in conveying the object code work. 305 | 306 | A "User Product" is either (1) a "consumer product", which means any 307 | tangible personal property which is normally used for personal, family, 308 | or household purposes, or (2) anything designed or sold for incorporation 309 | into a dwelling. In determining whether a product is a consumer product, 310 | doubtful cases shall be resolved in favor of coverage. For a particular 311 | product received by a particular user, "normally used" refers to a 312 | typical or common use of that class of product, regardless of the status 313 | of the particular user or of the way in which the particular user 314 | actually uses, or expects or is expected to use, the product. A product 315 | is a consumer product regardless of whether the product has substantial 316 | commercial, industrial or non-consumer uses, unless such uses represent 317 | the only significant mode of use of the product. 318 | 319 | "Installation Information" for a User Product means any methods, 320 | procedures, authorization keys, or other information required to install 321 | and execute modified versions of a covered work in that User Product from 322 | a modified version of its Corresponding Source. The information must 323 | suffice to ensure that the continued functioning of the modified object 324 | code is in no case prevented or interfered with solely because 325 | modification has been made. 326 | 327 | If you convey an object code work under this section in, or with, or 328 | specifically for use in, a User Product, and the conveying occurs as 329 | part of a transaction in which the right of possession and use of the 330 | User Product is transferred to the recipient in perpetuity or for a 331 | fixed term (regardless of how the transaction is characterized), the 332 | Corresponding Source conveyed under this section must be accompanied 333 | by the Installation Information. But this requirement does not apply 334 | if neither you nor any third party retains the ability to install 335 | modified object code on the User Product (for example, the work has 336 | been installed in ROM). 337 | 338 | The requirement to provide Installation Information does not include a 339 | requirement to continue to provide support service, warranty, or updates 340 | for a work that has been modified or installed by the recipient, or for 341 | the User Product in which it has been modified or installed. Access to a 342 | network may be denied when the modification itself materially and 343 | adversely affects the operation of the network or violates the rules and 344 | protocols for communication across the network. 345 | 346 | Corresponding Source conveyed, and Installation Information provided, 347 | in accord with this section must be in a format that is publicly 348 | documented (and with an implementation available to the public in 349 | source code form), and must require no special password or key for 350 | unpacking, reading or copying. 351 | 352 | 7. Additional Terms. 353 | 354 | "Additional permissions" are terms that supplement the terms of this 355 | License by making exceptions from one or more of its conditions. 356 | Additional permissions that are applicable to the entire Program shall 357 | be treated as though they were included in this License, to the extent 358 | that they are valid under applicable law. If additional permissions 359 | apply only to part of the Program, that part may be used separately 360 | under those permissions, but the entire Program remains governed by 361 | this License without regard to the additional permissions. 362 | 363 | When you convey a copy of a covered work, you may at your option 364 | remove any additional permissions from that copy, or from any part of 365 | it. (Additional permissions may be written to require their own 366 | removal in certain cases when you modify the work.) You may place 367 | additional permissions on material, added by you to a covered work, 368 | for which you have or can give appropriate copyright permission. 369 | 370 | Notwithstanding any other provision of this License, for material you 371 | add to a covered work, you may (if authorized by the copyright holders of 372 | that material) supplement the terms of this License with terms: 373 | 374 | a) Disclaiming warranty or limiting liability differently from the 375 | terms of sections 15 and 16 of this License; or 376 | 377 | b) Requiring preservation of specified reasonable legal notices or 378 | author attributions in that material or in the Appropriate Legal 379 | Notices displayed by works containing it; or 380 | 381 | c) Prohibiting misrepresentation of the origin of that material, or 382 | requiring that modified versions of such material be marked in 383 | reasonable ways as different from the original version; or 384 | 385 | d) Limiting the use for publicity purposes of names of licensors or 386 | authors of the material; or 387 | 388 | e) Declining to grant rights under trademark law for use of some 389 | trade names, trademarks, or service marks; or 390 | 391 | f) Requiring indemnification of licensors and authors of that 392 | material by anyone who conveys the material (or modified versions of 393 | it) with contractual assumptions of liability to the recipient, for 394 | any liability that these contractual assumptions directly impose on 395 | those licensors and authors. 396 | 397 | All other non-permissive additional terms are considered "further 398 | restrictions" within the meaning of section 10. If the Program as you 399 | received it, or any part of it, contains a notice stating that it is 400 | governed by this License along with a term that is a further 401 | restriction, you may remove that term. If a license document contains 402 | a further restriction but permits relicensing or conveying under this 403 | License, you may add to a covered work material governed by the terms 404 | of that license document, provided that the further restriction does 405 | not survive such relicensing or conveying. 406 | 407 | If you add terms to a covered work in accord with this section, you 408 | must place, in the relevant source files, a statement of the 409 | additional terms that apply to those files, or a notice indicating 410 | where to find the applicable terms. 411 | 412 | Additional terms, permissive or non-permissive, may be stated in the 413 | form of a separately written license, or stated as exceptions; 414 | the above requirements apply either way. 415 | 416 | 8. Termination. 417 | 418 | You may not propagate or modify a covered work except as expressly 419 | provided under this License. Any attempt otherwise to propagate or 420 | modify it is void, and will automatically terminate your rights under 421 | this License (including any patent licenses granted under the third 422 | paragraph of section 11). 423 | 424 | However, if you cease all violation of this License, then your 425 | license from a particular copyright holder is reinstated (a) 426 | provisionally, unless and until the copyright holder explicitly and 427 | finally terminates your license, and (b) permanently, if the copyright 428 | holder fails to notify you of the violation by some reasonable means 429 | prior to 60 days after the cessation. 430 | 431 | Moreover, your license from a particular copyright holder is 432 | reinstated permanently if the copyright holder notifies you of the 433 | violation by some reasonable means, this is the first time you have 434 | received notice of violation of this License (for any work) from that 435 | copyright holder, and you cure the violation prior to 30 days after 436 | your receipt of the notice. 437 | 438 | Termination of your rights under this section does not terminate the 439 | licenses of parties who have received copies or rights from you under 440 | this License. If your rights have been terminated and not permanently 441 | reinstated, you do not qualify to receive new licenses for the same 442 | material under section 10. 443 | 444 | 9. Acceptance Not Required for Having Copies. 445 | 446 | You are not required to accept this License in order to receive or 447 | run a copy of the Program. Ancillary propagation of a covered work 448 | occurring solely as a consequence of using peer-to-peer transmission 449 | to receive a copy likewise does not require acceptance. However, 450 | nothing other than this License grants you permission to propagate or 451 | modify any covered work. These actions infringe copyright if you do 452 | not accept this License. Therefore, by modifying or propagating a 453 | covered work, you indicate your acceptance of this License to do so. 454 | 455 | 10. Automatic Licensing of Downstream Recipients. 456 | 457 | Each time you convey a covered work, the recipient automatically 458 | receives a license from the original licensors, to run, modify and 459 | propagate that work, subject to this License. You are not responsible 460 | for enforcing compliance by third parties with this License. 461 | 462 | An "entity transaction" is a transaction transferring control of an 463 | organization, or substantially all assets of one, or subdividing an 464 | organization, or merging organizations. If propagation of a covered 465 | work results from an entity transaction, each party to that 466 | transaction who receives a copy of the work also receives whatever 467 | licenses to the work the party's predecessor in interest had or could 468 | give under the previous paragraph, plus a right to possession of the 469 | Corresponding Source of the work from the predecessor in interest, if 470 | the predecessor has it or can get it with reasonable efforts. 471 | 472 | You may not impose any further restrictions on the exercise of the 473 | rights granted or affirmed under this License. For example, you may 474 | not impose a license fee, royalty, or other charge for exercise of 475 | rights granted under this License, and you may not initiate litigation 476 | (including a cross-claim or counterclaim in a lawsuit) alleging that 477 | any patent claim is infringed by making, using, selling, offering for 478 | sale, or importing the Program or any portion of it. 479 | 480 | 11. Patents. 481 | 482 | A "contributor" is a copyright holder who authorizes use under this 483 | License of the Program or a work on which the Program is based. The 484 | work thus licensed is called the contributor's "contributor version". 485 | 486 | A contributor's "essential patent claims" are all patent claims 487 | owned or controlled by the contributor, whether already acquired or 488 | hereafter acquired, that would be infringed by some manner, permitted 489 | by this License, of making, using, or selling its contributor version, 490 | but do not include claims that would be infringed only as a 491 | consequence of further modification of the contributor version. For 492 | purposes of this definition, "control" includes the right to grant 493 | patent sublicenses in a manner consistent with the requirements of 494 | this License. 495 | 496 | Each contributor grants you a non-exclusive, worldwide, royalty-free 497 | patent license under the contributor's essential patent claims, to 498 | make, use, sell, offer for sale, import and otherwise run, modify and 499 | propagate the contents of its contributor version. 500 | 501 | In the following three paragraphs, a "patent license" is any express 502 | agreement or commitment, however denominated, not to enforce a patent 503 | (such as an express permission to practice a patent or covenant not to 504 | sue for patent infringement). To "grant" such a patent license to a 505 | party means to make such an agreement or commitment not to enforce a 506 | patent against the party. 507 | 508 | If you convey a covered work, knowingly relying on a patent license, 509 | and the Corresponding Source of the work is not available for anyone 510 | to copy, free of charge and under the terms of this License, through a 511 | publicly available network server or other readily accessible means, 512 | then you must either (1) cause the Corresponding Source to be so 513 | available, or (2) arrange to deprive yourself of the benefit of the 514 | patent license for this particular work, or (3) arrange, in a manner 515 | consistent with the requirements of this License, to extend the patent 516 | license to downstream recipients. "Knowingly relying" means you have 517 | actual knowledge that, but for the patent license, your conveying the 518 | covered work in a country, or your recipient's use of the covered work 519 | in a country, would infringe one or more identifiable patents in that 520 | country that you have reason to believe are valid. 521 | 522 | If, pursuant to or in connection with a single transaction or 523 | arrangement, you convey, or propagate by procuring conveyance of, a 524 | covered work, and grant a patent license to some of the parties 525 | receiving the covered work authorizing them to use, propagate, modify 526 | or convey a specific copy of the covered work, then the patent license 527 | you grant is automatically extended to all recipients of the covered 528 | work and works based on it. 529 | 530 | A patent license is "discriminatory" if it does not include within 531 | the scope of its coverage, prohibits the exercise of, or is 532 | conditioned on the non-exercise of one or more of the rights that are 533 | specifically granted under this License. You may not convey a covered 534 | work if you are a party to an arrangement with a third party that is 535 | in the business of distributing software, under which you make payment 536 | to the third party based on the extent of your activity of conveying 537 | the work, and under which the third party grants, to any of the 538 | parties who would receive the covered work from you, a discriminatory 539 | patent license (a) in connection with copies of the covered work 540 | conveyed by you (or copies made from those copies), or (b) primarily 541 | for and in connection with specific products or compilations that 542 | contain the covered work, unless you entered into that arrangement, 543 | or that patent license was granted, prior to 28 March 2007. 544 | 545 | Nothing in this License shall be construed as excluding or limiting 546 | any implied license or other defenses to infringement that may 547 | otherwise be available to you under applicable patent law. 548 | 549 | 12. No Surrender of Others' Freedom. 550 | 551 | If conditions are imposed on you (whether by court order, agreement or 552 | otherwise) that contradict the conditions of this License, they do not 553 | excuse you from the conditions of this License. If you cannot convey a 554 | covered work so as to satisfy simultaneously your obligations under this 555 | License and any other pertinent obligations, then as a consequence you may 556 | not convey it at all. For example, if you agree to terms that obligate you 557 | to collect a royalty for further conveying from those to whom you convey 558 | the Program, the only way you could satisfy both those terms and this 559 | License would be to refrain entirely from conveying the Program. 560 | 561 | 13. Remote Network Interaction; Use with the GNU General Public License. 562 | 563 | Notwithstanding any other provision of this License, if you modify the 564 | Program, your modified version must prominently offer all users 565 | interacting with it remotely through a computer network (if your version 566 | supports such interaction) an opportunity to receive the Corresponding 567 | Source of your version by providing access to the Corresponding Source 568 | from a network server at no charge, through some standard or customary 569 | means of facilitating copying of software. This Corresponding Source 570 | shall include the Corresponding Source for any work covered by version 3 571 | of the GNU General Public License that is incorporated pursuant to the 572 | following paragraph. 573 | 574 | Notwithstanding any other provision of this License, you have 575 | permission to link or combine any covered work with a work licensed 576 | under version 3 of the GNU General Public License into a single 577 | combined work, and to convey the resulting work. The terms of this 578 | License will continue to apply to the part which is the covered work, 579 | but the work with which it is combined will remain governed by version 580 | 3 of the GNU General Public License. 581 | 582 | 14. Revised Versions of this License. 583 | 584 | The Free Software Foundation may publish revised and/or new versions of 585 | the GNU Affero General Public License from time to time. Such new versions 586 | will be similar in spirit to the present version, but may differ in detail to 587 | address new problems or concerns. 588 | 589 | Each version is given a distinguishing version number. If the 590 | Program specifies that a certain numbered version of the GNU Affero General 591 | Public License "or any later version" applies to it, you have the 592 | option of following the terms and conditions either of that numbered 593 | version or of any later version published by the Free Software 594 | Foundation. If the Program does not specify a version number of the 595 | GNU Affero General Public License, you may choose any version ever published 596 | by the Free Software Foundation. 597 | 598 | If the Program specifies that a proxy can decide which future 599 | versions of the GNU Affero General Public License can be used, that proxy's 600 | public statement of acceptance of a version permanently authorizes you 601 | to choose that version for the Program. 602 | 603 | Later license versions may give you additional or different 604 | permissions. However, no additional obligations are imposed on any 605 | author or copyright holder as a result of your choosing to follow a 606 | later version. 607 | 608 | 15. Disclaimer of Warranty. 609 | 610 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 611 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 612 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 613 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 614 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 615 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 616 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 617 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 618 | 619 | 16. Limitation of Liability. 620 | 621 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 622 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 623 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 624 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 625 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 626 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 627 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 628 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 629 | SUCH DAMAGES. 630 | 631 | 17. Interpretation of Sections 15 and 16. 632 | 633 | If the disclaimer of warranty and limitation of liability provided 634 | above cannot be given local legal effect according to their terms, 635 | reviewing courts shall apply local law that most closely approximates 636 | an absolute waiver of all civil liability in connection with the 637 | Program, unless a warranty or assumption of liability accompanies a 638 | copy of the Program in return for a fee. 639 | 640 | END OF TERMS AND CONDITIONS 641 | 642 | How to Apply These Terms to Your New Programs 643 | 644 | If you develop a new program, and you want it to be of the greatest 645 | possible use to the public, the best way to achieve this is to make it 646 | free software which everyone can redistribute and change under these terms. 647 | 648 | To do so, attach the following notices to the program. It is safest 649 | to attach them to the start of each source file to most effectively 650 | state the exclusion of warranty; and each file should have at least 651 | the "copyright" line and a pointer to where the full notice is found. 652 | 653 | 654 | Copyright (C) 655 | 656 | This program is free software: you can redistribute it and/or modify 657 | it under the terms of the GNU Affero General Public License as published 658 | by the Free Software Foundation, either version 3 of the License, or 659 | (at your option) any later version. 660 | 661 | This program is distributed in the hope that it will be useful, 662 | but WITHOUT ANY WARRANTY; without even the implied warranty of 663 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 664 | GNU Affero General Public License for more details. 665 | 666 | You should have received a copy of the GNU Affero General Public License 667 | along with this program. If not, see . 668 | 669 | Also add information on how to contact you by electronic and paper mail. 670 | 671 | If your software can interact with users remotely through a computer 672 | network, you should also make sure that it provides a way for users to 673 | get its source. For example, if your program is a web application, its 674 | interface could display a "Source" link that leads users to an archive 675 | of the code. There are many ways you could offer source, and different 676 | solutions will be better for different programs; see section 13 for the 677 | specific requirements. 678 | 679 | You should also get your employer (if you work as a programmer) or school, 680 | if any, to sign a "copyright disclaimer" for the program, if necessary. 681 | For more information on this, and how to apply and follow the GNU AGPL, see 682 | . --------------------------------------------------------------------------------