main.go (7652B)
1 package main 2 3 import ( 4 "encoding/json" 5 "io" 6 "log" 7 "log/slog" 8 "net/http" 9 "strconv" 10 "time" 11 12 "database/sql" 13 "os" 14 15 "github.com/google/uuid" 16 _ "github.com/mattn/go-sqlite3" 17 ) 18 19 //go:generate curl -SsL -o immich-openapi-specs.json https://github.com/immich-app/immich/raw/refs/tags/v3.0.3/open-api/immich-openapi-specs.json 20 //go:generate go tool oapi-codegen -config cfg.yaml immich-openapi-specs.json 21 22 const ( 23 address = "127.0.0.1:8080" 24 api = "/api" 25 email = "martin@mfashby.net" 26 name = "Martin Ashby" 27 ) 28 29 var ( 30 userid = must(func() (uuid.UUID, error) { return uuid.Parse("8e251dd2-2d10-484b-bdc3-0128d855f204") }) 31 ) 32 33 func main() { 34 RegisterCustomFunctions() 35 db, err := sql.Open("sqlite3_custom", "gimmich.db") 36 if err != nil { 37 slog.Error("error opening database", "err", err.Error()) 38 os.Exit(1) 39 } 40 defer func() { 41 _ = db.Close() 42 }() 43 _, err = db.Exec("create table if not exists foo(col1 int)") 44 if err != nil { 45 slog.Error("error opening db", slog.Any("err", err)) 46 os.Exit(1) 47 } 48 m := http.NewServeMux() 49 m.HandleFunc("/.well-known/immich", func(w http.ResponseWriter, r *http.Request) { 50 type Api struct { 51 Endpoint string `json:"endpoint"` 52 } 53 type WellKnown struct { 54 Api Api `json:"api"` 55 } 56 doWriteJsonResponse(w, WellKnown{ 57 Api: Api{ 58 Endpoint: api, 59 }, 60 }) 61 }) 62 log.Printf("Starting gimmich on %s", address) 63 log.Fatal(http.ListenAndServe(address, HandlerWithOptions(&MyServer{}, StdHTTPServerOptions{ 64 BaseURL: api, 65 BaseRouter: m, 66 }))) 67 } 68 69 func doReadJsonRequestOrSendErr[k any](w http.ResponseWriter, r *http.Request, p *k) (*k, error) { 70 err := json.NewDecoder(r.Body).Decode(p) 71 if err != nil { 72 badRequest(w) 73 return nil, err 74 } 75 return p, nil 76 } 77 78 func doWriteJsonResponse(w http.ResponseWriter, o any) { 79 doWriteResponse(w, func(w http.ResponseWriter) error { 80 slog.Info("writing json response...") 81 res, err := json.Marshal(o) 82 if err != nil { 83 serverError(w) 84 return err 85 } 86 h := w.Header() 87 h.Set("content-length", strconv.Itoa(len(res))) 88 h.Set("content-type", "application/json") 89 w.WriteHeader(200) 90 _, err = w.Write(res) 91 return err 92 }) 93 } 94 95 func doWriteResponse(w http.ResponseWriter, f func(w http.ResponseWriter) error) { 96 err := f(w) 97 if err != nil { 98 slog.Error("error writing response", "err", err.Error()) 99 } 100 } 101 102 func badRequest(w http.ResponseWriter) { 103 w.WriteHeader(400) 104 doWriteResponse(w, func(w http.ResponseWriter) error { 105 _, err := w.Write([]byte("bad request")) 106 return err 107 }) 108 } 109 110 func serverError(w http.ResponseWriter) { 111 w.WriteHeader(500) 112 doWriteResponse(w, func(w http.ResponseWriter) error { 113 _, err := w.Write([]byte("server error")) 114 return err 115 }) 116 } 117 118 func must[E any](f func() (E, error)) E { 119 e, err := f() 120 if err != nil { 121 log.Fatal(err) 122 } 123 return e 124 } 125 126 type MyServer struct{ 127 128 } 129 130 // Login implements [ServerInterface]. 131 func (m *MyServer) Login(w http.ResponseWriter, r *http.Request) { 132 _, err := doReadJsonRequestOrSendErr(w, r, &LoginJSONRequestBody{}) 133 if err != nil { 134 return 135 } 136 137 // TODO comment back in auth check 138 // if lrb.Email == email && lrb.Password == "P@55word" { 139 140 doWriteJsonResponse(w, LoginResponseDto{ 141 AccessToken: "foobar", 142 IsAdmin: true, 143 IsOnboarded: true, 144 Name: name, 145 ProfileImagePath: "", 146 ShouldChangePassword: false, 147 UserEmail: email, 148 UserId: userid, 149 }) 150 // } else { 151 // w.WriteHeader(401) 152 // doWriteResponse(w, func(w http.ResponseWriter) error { 153 // _, e := w.Write([]byte("unauthorized")) 154 // return e 155 // }) 156 // } 157 } 158 159 // Logout implements [ServerInterface]. 160 func (m *MyServer) Logout(w http.ResponseWriter, r *http.Request) { 161 doWriteJsonResponse(w, LogoutResponseDto{ 162 RedirectUri: "/auth/login", 163 Successful: true, 164 }) 165 } 166 167 // ValidateAccessToken implements [ServerInterface]. 168 func (m *MyServer) ValidateAccessToken(w http.ResponseWriter, r *http.Request) { 169 bytes, err := io.ReadAll(r.Body) 170 if err != nil { 171 slog.Error("ValidateAccessToken", slog.Any("err", err)) 172 serverError(w) 173 return 174 } 175 for k, v := range r.Header { 176 slog.Info("ValidateAccessToken", slog.String("key", k), slog.Any("value", v)) 177 } 178 slog.Info("ValidateAccessToken", slog.String("body", string(bytes))) 179 doWriteJsonResponse(w, ValidateAccessTokenResponseDto{ 180 AuthStatus: true, 181 }) 182 } 183 184 // PingServer implements [ServerInterface]. 185 func (m *MyServer) PingServer(w http.ResponseWriter, r *http.Request) { 186 doWriteJsonResponse(w, ServerPingResponse{ 187 Res: "pong", 188 }) 189 } 190 191 // GetServerVersion implements [ServerInterface]. 192 func (m *MyServer) GetServerVersion(w http.ResponseWriter, r *http.Request) { 193 doWriteJsonResponse(w, ServerVersionResponseDto{ 194 Major: 3, 195 Minor: 2, 196 Patch: 2, 197 }) 198 } 199 200 // GetServerFeatures implements [ServerInterface]. 201 func (m *MyServer) GetServerFeatures(w http.ResponseWriter, r *http.Request) { 202 doWriteJsonResponse(w, ServerFeaturesDto{ 203 ConfigFile: false, 204 DuplicateDetection: false, 205 Email: false, 206 FacialRecognition: false, 207 ImportFaces: false, 208 Map: false, 209 Oauth: false, 210 OauthAutoLaunch: false, 211 Ocr: false, 212 PasswordLogin: true, 213 RealtimeTranscoding: false, 214 ReverseGeocoding: false, 215 Search: false, 216 Sidecar: false, 217 SmartSearch: false, 218 Trash: false, 219 }) 220 } 221 222 223 // GetServerConfig implements [ServerInterface]. 224 func (m *MyServer) GetServerConfig(w http.ResponseWriter, r *http.Request) { 225 hh := r.Header.Get("host") 226 if hh == "" { 227 hh = "localhost" 228 } 229 doWriteJsonResponse(w, ServerConfigDto{ 230 ExternalDomain: hh, 231 IsInitialized: true, 232 IsOnboarded: true, 233 LoginPageMessage: "Hello", 234 MaintenanceMode: false, 235 MapDarkStyleUrl: "", 236 MapLightStyleUrl: "", 237 MinFaces: 0, 238 OauthButtonText: "", 239 PublicUsers: false, 240 TrashDays: 0, 241 UserDeleteDelay: 0, 242 }) 243 } 244 245 // GetMyUser implements [ServerInterface]. 246 func (m *MyServer) GetMyUser(w http.ResponseWriter, r *http.Request) { 247 quotasizebytes := 10_000_000_000 248 quotausagebytes := 1_000_000_000 249 created := must(func() (time.Time, error) { return time.Parse(time.RFC3339, "2026-09-28T12:00:00Z")} ) 250 doWriteJsonResponse(w, UserAdminResponseDto{ 251 AvatarColor: "blue", 252 CreatedAt: created, 253 DeletedAt: nil, 254 Email: email, 255 Id: userid, 256 IsAdmin: false, 257 License: nil, 258 Name: name, 259 OauthId: "", 260 ProfileChangedAt: time.Time{}, 261 ProfileImagePath: "", 262 QuotaSizeInBytes: "asizebytes, 263 QuotaUsageInBytes: "ausagebytes, 264 ShouldChangePassword: false, 265 Status: "active", 266 StorageLabel: nil, 267 UpdatedAt: created, 268 }) 269 } 270 271 // GetMyPreferences implements [ServerInterface]. 272 func (m *MyServer) GetMyPreferences(w http.ResponseWriter, r *http.Request) { 273 doWriteJsonResponse(w, UserPreferencesResponseDto{ 274 Albums: AlbumsResponse{}, 275 Cast: CastResponse{}, 276 Download: DownloadResponse{}, 277 EmailNotifications: EmailNotificationsResponse{}, 278 Folders: FoldersResponse{}, 279 Memories: MemoriesResponse{}, 280 People: PeopleResponse{}, 281 Purchase: PurchaseResponse{}, 282 Ratings: RatingsResponse{}, 283 RecentlyAdded: RecentlyAddedResponse{}, 284 SharedLinks: SharedLinksResponse{}, 285 Tags: TagsResponse{}, 286 }) 287 } 288 289 // DeleteSyncAck implements [ServerInterface]. 290 func (m *MyServer) DeleteSyncAck(w http.ResponseWriter, r *http.Request) { 291 w.WriteHeader(204) 292 } 293