main.go (7657B)
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 /*lrb*/_, err := doReadJsonRequestOrSendErr(w, r, &LoginJSONRequestBody{}) 133 if err != nil { 134 return 135 } 136 // TODO comment back in auth check 137 // if lrb.Email == email && lrb.Password == "P@55word" { 138 139 doWriteJsonResponse(w, LoginResponseDto{ 140 AccessToken: "foobar", 141 IsAdmin: true, 142 IsOnboarded: true, 143 Name: name, 144 ProfileImagePath: "", 145 ShouldChangePassword: false, 146 UserEmail: email, 147 UserId: userid, 148 }) 149 // } else { 150 // w.WriteHeader(401) 151 // doWriteResponse(w, func(w http.ResponseWriter) error { 152 // _, e := w.Write([]byte("unauthorized")) 153 // return e 154 // }) 155 // } 156 } 157 158 // Logout implements [ServerInterface]. 159 func (m *MyServer) Logout(w http.ResponseWriter, r *http.Request) { 160 doWriteJsonResponse(w, LogoutResponseDto{ 161 RedirectUri: "/auth/login", 162 Successful: true, 163 }) 164 } 165 166 // ValidateAccessToken implements [ServerInterface]. 167 func (m *MyServer) ValidateAccessToken(w http.ResponseWriter, r *http.Request) { 168 bytes, err := io.ReadAll(r.Body) 169 if err != nil { 170 slog.Error("ValidateAccessToken", slog.Any("err", err)) 171 serverError(w) 172 return 173 } 174 for k, v := range r.Header { 175 slog.Info("ValidateAccessToken", slog.String("key", k), slog.Any("value", v)) 176 } 177 slog.Info("ValidateAccessToken", slog.String("body", string(bytes))) 178 doWriteJsonResponse(w, ValidateAccessTokenResponseDto{ 179 AuthStatus: true, 180 }) 181 } 182 183 // PingServer implements [ServerInterface]. 184 func (m *MyServer) PingServer(w http.ResponseWriter, r *http.Request) { 185 doWriteJsonResponse(w, ServerPingResponse{ 186 Res: "pong", 187 }) 188 } 189 190 // GetServerVersion implements [ServerInterface]. 191 func (m *MyServer) GetServerVersion(w http.ResponseWriter, r *http.Request) { 192 doWriteJsonResponse(w, ServerVersionResponseDto{ 193 Major: 3, 194 Minor: 2, 195 Patch: 2, 196 }) 197 } 198 199 // GetServerFeatures implements [ServerInterface]. 200 func (m *MyServer) GetServerFeatures(w http.ResponseWriter, r *http.Request) { 201 doWriteJsonResponse(w, ServerFeaturesDto{ 202 ConfigFile: false, 203 DuplicateDetection: false, 204 Email: false, 205 FacialRecognition: false, 206 ImportFaces: false, 207 Map: false, 208 Oauth: false, 209 OauthAutoLaunch: false, 210 Ocr: false, 211 PasswordLogin: true, 212 RealtimeTranscoding: false, 213 ReverseGeocoding: false, 214 Search: false, 215 Sidecar: false, 216 SmartSearch: false, 217 Trash: false, 218 }) 219 } 220 221 222 // GetServerConfig implements [ServerInterface]. 223 func (m *MyServer) GetServerConfig(w http.ResponseWriter, r *http.Request) { 224 hh := r.Header.Get("host") 225 if hh == "" { 226 hh = "localhost" 227 } 228 doWriteJsonResponse(w, ServerConfigDto{ 229 ExternalDomain: hh, 230 IsInitialized: true, 231 IsOnboarded: true, 232 LoginPageMessage: "Hello", 233 MaintenanceMode: false, 234 MapDarkStyleUrl: "", 235 MapLightStyleUrl: "", 236 MinFaces: 0, 237 OauthButtonText: "", 238 PublicUsers: false, 239 TrashDays: 0, 240 UserDeleteDelay: 0, 241 }) 242 } 243 244 // GetMyUser implements [ServerInterface]. 245 func (m *MyServer) GetMyUser(w http.ResponseWriter, r *http.Request) { 246 quotasizebytes := 10_000_000_000 247 quotausagebytes := 1_000_000_000 248 created := must(func() (time.Time, error) { return time.Parse(time.RFC3339, "2026-09-28T12:00:00Z")} ) 249 doWriteJsonResponse(w, UserAdminResponseDto{ 250 AvatarColor: "blue", 251 CreatedAt: created, 252 DeletedAt: nil, 253 Email: email, 254 Id: userid, 255 IsAdmin: false, 256 License: nil, 257 Name: name, 258 OauthId: "", 259 ProfileChangedAt: time.Time{}, 260 ProfileImagePath: "", 261 QuotaSizeInBytes: "asizebytes, 262 QuotaUsageInBytes: "ausagebytes, 263 ShouldChangePassword: false, 264 Status: "active", 265 StorageLabel: nil, 266 UpdatedAt: created, 267 }) 268 } 269 270 // GetMyPreferences implements [ServerInterface]. 271 func (m *MyServer) GetMyPreferences(w http.ResponseWriter, r *http.Request) { 272 doWriteJsonResponse(w, UserPreferencesResponseDto{ 273 Albums: AlbumsResponse{}, 274 Cast: CastResponse{}, 275 Download: DownloadResponse{}, 276 EmailNotifications: EmailNotificationsResponse{}, 277 Folders: FoldersResponse{}, 278 Memories: MemoriesResponse{}, 279 People: PeopleResponse{}, 280 Purchase: PurchaseResponse{}, 281 Ratings: RatingsResponse{}, 282 RecentlyAdded: RecentlyAddedResponse{}, 283 SharedLinks: SharedLinksResponse{}, 284 Tags: TagsResponse{}, 285 }) 286 } 287 288 // DeleteSyncAck implements [ServerInterface]. 289 func (m *MyServer) DeleteSyncAck(w http.ResponseWriter, r *http.Request) { 290 w.WriteHeader(204) 291 } 292