masto.go (view raw)
1package main
2
3import (
4 "database/sql"
5 "fmt"
6 "log"
7 "net/http"
8 "slices"
9 "strings"
10 "time"
11
12 "humungus.tedunangst.com/r/webs/junk"
13 "humungus.tedunangst.com/r/webs/login"
14)
15
16type MastoApp struct {
17 Name string `db:"clientname"`
18 RedirectURI string `db:"redirecturis"`
19 ClientID string `db:"clientid"`
20 ClientSecret string `db:"clientsecret"`
21 VapidKey string `db:"vapidkey"`
22 AuthToken string `db:"authtoken"`
23 Scopes string `db:"scopes"`
24}
25
26func showoauthlogin(rw http.ResponseWriter, r *http.Request) {
27 templinfo := make(map[string]interface{})
28 templinfo = getInfo(r)
29 templinfo["ClientID"] = r.URL.Query().Get("client_id")
30 templinfo["RedirectURI"] = r.URL.Query().Get("redirect_uri")
31
32 if err := readviews.Execute(rw, "oauthlogin.html", templinfo); err != nil {
33 elog.Println(err)
34 }
35}
36
37// https://docs.joinmastodon.org/methods/apps/#create
38func apiapps(rw http.ResponseWriter, r *http.Request) {
39 if err := r.ParseForm(); err != nil {
40 http.Error(rw, "invalid input", http.StatusUnprocessableEntity)
41 elog.Println(err)
42 return
43 }
44 clientName := r.Form.Get("client_name")
45 redirectURI := r.Form.Get("redirect_uris")
46 scopes := r.Form.Get("scopes")
47 website := r.Form.Get("website")
48 clientID := tokengen()
49 clientSecret := tokengen()
50 vapidKey := tokengen()
51
52 _, err := stmtSaveMastoApp.Exec(clientName, redirectURI, scopes, clientID, clientSecret, vapidKey, "")
53 if err != nil {
54 elog.Printf("error saving masto app: %v", err)
55 http.Error(rw, "error saving masto app", http.StatusUnprocessableEntity)
56 return
57 }
58
59 j := junk.New()
60 j["id"] = "19"
61 j["website"] = website
62 j["name"] = clientName
63 j["redirect_uri"] = redirectURI
64 j["client_id"] = clientID
65 j["client_secret"] = clientSecret
66 j["vapid_key"] = vapidKey
67
68 fmt.Println(j.ToString())
69 goodjunk(rw, j)
70}
71
72// https://docs.joinmastodon.org/methods/oauth/#authorize
73func oauthorize(rw http.ResponseWriter, r *http.Request) {
74 clientID := r.FormValue("client_id")
75 redirectURI := r.FormValue("redirect_uri")
76
77 if !checkClientID(clientID) {
78 elog.Println("oauth: no such client:", clientID)
79 rw.WriteHeader(http.StatusUnauthorized)
80 return
81 }
82
83 var nrw NotResponseWriter
84 login.LoginFunc(&nrw, r)
85
86 _, err := stmtSaveMastoAppToken.Exec(nrw.auth)
87 if err != nil {
88 elog.Println("oauth: failed to save masto app token", err)
89 rw.WriteHeader(http.StatusInternalServerError)
90 return
91 }
92
93 uri := fmt.Sprintf("%s?code=%s", redirectURI, nrw.auth)
94
95 log.Println("redirecting to", uri)
96 rw.Header().Set("Content-Type", "")
97 rw.Header().Set("Location", uri)
98 rw.WriteHeader(302)
99}
100
101// https://docs.joinmastodon.org/methods/oauth/#token
102func oauthtoken(rw http.ResponseWriter, r *http.Request) {
103 grantType := r.FormValue("grant_type")
104 code := r.FormValue("code")
105 clientID := r.FormValue("client_id")
106 clientSecret := r.FormValue("client_secret")
107 redirectURI := r.FormValue("redirect_uri")
108 gotScope := r.FormValue("scope")
109
110 if !checkClient(clientID, clientSecret) {
111 elog.Println("oauth: no such client:", clientID)
112 rw.WriteHeader(http.StatusBadRequest)
113 return
114 }
115
116 app := MastoApp{}
117 row := stmtGetMastoApp.QueryRowx(clientID)
118 err := row.StructScan(&app)
119 if err == sql.ErrNoRows {
120 elog.Printf("oauth: invalid client: %s\n", clientID)
121 rw.WriteHeader(http.StatusBadRequest)
122 return
123 }
124 log.Printf("%#v", app)
125
126 possibleScopes := strings.Split(app.Scopes, " ")
127 requestedScopes := strings.Split(gotScope, " ")
128 for _, scope := range requestedScopes {
129 if !slices.Contains(possibleScopes, scope) {
130 elog.Printf("oauth: invalid scope: %s", scope)
131 rw.WriteHeader(http.StatusBadRequest)
132 return
133 }
134 }
135
136 if app.Scopes != gotScope {
137 elog.Printf("oauth: bad scopes: got %s; want %s", gotScope, app.Scopes)
138 rw.WriteHeader(http.StatusBadRequest)
139 return
140 }
141
142 if app.RedirectURI != redirectURI {
143 elog.Println("oauth: incorrect redirect URI")
144 rw.WriteHeader(http.StatusBadRequest)
145 return
146 }
147
148 if grantType == "authorization_code" {
149 // idk if this is ok? should code be reset?
150 if app.AuthToken == code {
151 accessToken := tokengen()
152 _, err := stmtSaveMastoAccessToken.Exec(app.ClientID, accessToken)
153 if err != nil {
154 elog.Println("oauth: failed to save masto access token", err)
155 rw.WriteHeader(http.StatusInternalServerError)
156 return
157 }
158 j := junk.New()
159 j["access_token"] = accessToken
160 j["token_type"] = "Bearer"
161 j["scope"] = app.Scopes
162 j["created_at"] = time.Now().UTC().Unix()
163 goodjunk(rw, j)
164 }
165 } else {
166 // gaslight the client
167 elog.Println("oauth: bad grant_type: must be authorization_code")
168 rw.WriteHeader(http.StatusBadRequest)
169 return
170 }
171}
172
173// https://docs.joinmastodon.org/methods/instance/#v2
174func instance(rw http.ResponseWriter, r *http.Request) {
175 j := junk.New()
176
177 var servername string
178 if err := getconfig("servername", &servername); err != nil {
179 http.Error(rw, "getting servername", http.StatusInternalServerError)
180 return
181 }
182
183 j["uri"] = servername
184 j["title"] = "honk"
185 j["description"] = "federated honk conveyance"
186 j["version"] = "develop"
187
188 thumbnail := junk.New()
189 thumbnail["url"] = fmt.Sprintf("https://%s/icon.png", servername)
190 j["thumbnail"] = thumbnail
191 j["languages"] = []string{"en"}
192
193 config := junk.New()
194
195 a := junk.New()
196 a["max_featured_tags"] = 10
197 config["accounts"] = a
198
199 s := junk.New()
200 s["max_characters"] = 5000
201 s["max_media_attachments"] = 1
202 s["characters_reserved_per_url"] = 23
203 config["statuses"] = s
204
205 m := junk.New()
206 m["supported_mime_types"] = []string{
207 "image/jpeg",
208 "image/png",
209 "image/gif",
210 "image/heic",
211 "image/heif",
212 "image/webp",
213 "image/avif",
214 "video/webm",
215 "video/mp4",
216 "video/quicktime",
217 "video/ogg",
218 "audio/wave",
219 "audio/wav",
220 "audio/x-wav",
221 "audio/x-pn-wave",
222 "audio/vnd.wave",
223 "audio/ogg",
224 "audio/vorbis",
225 "audio/mpeg",
226 "audio/mp3",
227 "audio/webm",
228 "audio/flac",
229 "audio/aac",
230 "audio/m4a",
231 "audio/x-m4a",
232 "audio/mp4",
233 "audio/3gpp",
234 "video/x-ms-asf",
235 }
236
237 m["image_size_limit"] = 10485760
238 m["image_matrix_limit"] = 16777216
239 m["video_size_limit"] = 41943040
240 m["video_frame_rate_limit"] = 60
241 m["video_matrix_limit"] = 2304000
242 j["media_attachments"] = m
243
244 goodjunk(rw, j)
245
246 return
247}
248
249// https://docs.joinmastodon.org/methods/accounts/#verify_credentials
250func verifycreds(rw http.ResponseWriter, r *http.Request) {
251 user, ok := somenumberedusers.Get(UserID(1))
252 if !ok {
253 elog.Fatalf("masto: no user number 1???")
254 }
255 j := junk.New()
256 j["id"] = user.ID
257 j["username"] = user.Name
258 j["acct"] = user.Name
259 j["display_name"] = user.Options.CustomDisplay
260 j["url"] = user.URL
261 j["note"] = user.HTAbout
262 j["avatar"] = user.Options.Avatar
263 j["header"] = user.Options.Banner
264 j["followers_count"] = 0
265 j["following_count"] = 0
266 j["statuses_count"] = getlocalhonkcount()
267 j["fields"] = []interface{}{}
268
269 s := junk.New()
270 s["note"] = user.About
271 s["privacy"] = "public"
272 s["note"] = user.About
273 s["fields"] = []interface{}{}
274 s["emojis"] = []interface{}{}
275 j["source"] = s
276
277 goodjunk(rw, j)
278}