masto.go (view raw)
1package main
2
3import (
4 "bytes"
5 "database/sql"
6 "fmt"
7 "log"
8 "net/http"
9 "slices"
10 "strconv"
11 "strings"
12 "time"
13
14 "humungus.tedunangst.com/r/webs/junk"
15 "humungus.tedunangst.com/r/webs/login"
16)
17
18type MastoApp struct {
19 Name string `db:"clientname"`
20 RedirectURI string `db:"redirecturis"`
21 ClientID string `db:"clientid"`
22 ClientSecret string `db:"clientsecret"`
23 VapidKey string `db:"vapidkey"`
24 AuthToken string `db:"authtoken"`
25 Scopes string `db:"scopes"`
26}
27
28func showoauthlogin(rw http.ResponseWriter, r *http.Request) {
29 templinfo := make(map[string]interface{})
30 templinfo = getInfo(r)
31 templinfo["ClientID"] = r.URL.Query().Get("client_id")
32 templinfo["RedirectURI"] = r.URL.Query().Get("redirect_uri")
33
34 if err := readviews.Execute(rw, "oauthlogin.html", templinfo); err != nil {
35 elog.Println(err)
36 }
37}
38
39// https://docs.joinmastodon.org/methods/apps/#create
40func apiapps(rw http.ResponseWriter, r *http.Request) {
41 if err := r.ParseForm(); err != nil {
42 http.Error(rw, "invalid input", http.StatusUnprocessableEntity)
43 elog.Println(err)
44 return
45 }
46 clientName := r.Form.Get("client_name")
47 redirectURI := r.Form.Get("redirect_uris")
48 scopes := r.Form.Get("scopes")
49 website := r.Form.Get("website")
50 clientID := tokengen()
51 clientSecret := tokengen()
52 vapidKey := tokengen()
53
54 _, err := stmtSaveMastoApp.Exec(clientName, redirectURI, scopes, clientID, clientSecret, vapidKey, "")
55 if err != nil {
56 elog.Printf("error saving masto app: %v", err)
57 http.Error(rw, "error saving masto app", http.StatusUnprocessableEntity)
58 return
59 }
60
61 j := junk.New()
62 j["id"] = "19"
63 j["website"] = website
64 j["name"] = clientName
65 j["redirect_uri"] = redirectURI
66 j["client_id"] = clientID
67 j["client_secret"] = clientSecret
68 j["vapid_key"] = vapidKey
69
70 fmt.Println(j.ToString())
71 goodjunk(rw, j)
72}
73
74// https://docs.joinmastodon.org/methods/oauth/#authorize
75func oauthorize(rw http.ResponseWriter, r *http.Request) {
76 clientID := r.FormValue("client_id")
77 redirectURI := r.FormValue("redirect_uri")
78
79 if !checkClientID(clientID) {
80 elog.Println("oauth: no such client:", clientID)
81 rw.WriteHeader(http.StatusUnauthorized)
82 return
83 }
84
85 var nrw NotResponseWriter
86 login.LoginFunc(&nrw, r)
87
88 _, err := stmtSaveMastoAppToken.Exec(nrw.auth)
89 if err != nil {
90 elog.Println("oauth: failed to save masto app token", err)
91 rw.WriteHeader(http.StatusInternalServerError)
92 return
93 }
94
95 uri := fmt.Sprintf("%s?code=%s", redirectURI, nrw.auth)
96
97 log.Println("redirecting to", uri)
98 rw.Header().Set("Content-Type", "")
99 rw.Header().Set("Location", uri)
100 rw.WriteHeader(302)
101}
102
103// https://docs.joinmastodon.org/methods/oauth/#token
104func oauthtoken(rw http.ResponseWriter, r *http.Request) {
105 grantType := r.FormValue("grant_type")
106 code := r.FormValue("code")
107 clientID := r.FormValue("client_id")
108 clientSecret := r.FormValue("client_secret")
109 redirectURI := r.FormValue("redirect_uri")
110 gotScope := r.FormValue("scope")
111
112 if !checkClient(clientID, clientSecret) {
113 elog.Println("oauth: no such client:", clientID)
114 rw.WriteHeader(http.StatusBadRequest)
115 return
116 }
117
118 app := MastoApp{}
119 row := stmtGetMastoApp.QueryRowx(clientID)
120 err := row.StructScan(&app)
121 if err == sql.ErrNoRows {
122 elog.Printf("oauth: invalid client: %s\n", clientID)
123 rw.WriteHeader(http.StatusBadRequest)
124 return
125 }
126 log.Printf("%#v", app)
127
128 possibleScopes := strings.Split(app.Scopes, " ")
129 requestedScopes := strings.Split(gotScope, " ")
130 for _, scope := range requestedScopes {
131 if !slices.Contains(possibleScopes, scope) {
132 elog.Printf("oauth: invalid scope: %s", scope)
133 rw.WriteHeader(http.StatusBadRequest)
134 return
135 }
136 }
137
138 if app.Scopes != gotScope {
139 elog.Printf("oauth: bad scopes: got %s; want %s", gotScope, app.Scopes)
140 rw.WriteHeader(http.StatusBadRequest)
141 return
142 }
143
144 if app.RedirectURI != redirectURI {
145 elog.Println("oauth: incorrect redirect URI")
146 rw.WriteHeader(http.StatusBadRequest)
147 return
148 }
149
150 if grantType == "authorization_code" {
151 // idk if this is ok? should code be reset?
152 if app.AuthToken == code {
153 accessToken := tokengen()
154 _, err := stmtSaveMastoAccessToken.Exec(app.ClientID, accessToken)
155 if err != nil {
156 elog.Println("oauth: failed to save masto access token", err)
157 rw.WriteHeader(http.StatusInternalServerError)
158 return
159 }
160 j := junk.New()
161 j["access_token"] = accessToken
162 j["token_type"] = "Bearer"
163 j["scope"] = app.Scopes
164 j["created_at"] = time.Now().UTC().Unix()
165 goodjunk(rw, j)
166 }
167 } else {
168 // gaslight the client
169 elog.Println("oauth: bad grant_type: must be authorization_code")
170 rw.WriteHeader(http.StatusBadRequest)
171 return
172 }
173}
174
175// https://docs.joinmastodon.org/methods/instance/#v2
176func instance(rw http.ResponseWriter, r *http.Request) {
177 j := junk.New()
178
179 var servername string
180 if err := getconfig("servername", &servername); err != nil {
181 http.Error(rw, "getting servername", http.StatusInternalServerError)
182 return
183 }
184
185 j["uri"] = servername
186 j["title"] = "honk"
187 j["description"] = "federated honk conveyance"
188 j["version"] = "develop"
189
190 thumbnail := junk.New()
191 thumbnail["url"] = fmt.Sprintf("https://%s/icon.png", servername)
192 j["thumbnail"] = thumbnail
193 j["languages"] = []string{"en"}
194
195 config := junk.New()
196
197 a := junk.New()
198 a["max_featured_tags"] = 10
199 config["accounts"] = a
200
201 s := junk.New()
202 s["max_characters"] = 5000
203 s["max_media_attachments"] = 1
204 s["characters_reserved_per_url"] = 23
205 config["statuses"] = s
206
207 m := junk.New()
208 m["supported_mime_types"] = []string{
209 "image/jpeg",
210 "image/png",
211 "image/gif",
212 "image/heic",
213 "image/heif",
214 "image/webp",
215 "image/avif",
216 "video/webm",
217 "video/mp4",
218 "video/quicktime",
219 "video/ogg",
220 "audio/wave",
221 "audio/wav",
222 "audio/x-wav",
223 "audio/x-pn-wave",
224 "audio/vnd.wave",
225 "audio/ogg",
226 "audio/vorbis",
227 "audio/mpeg",
228 "audio/mp3",
229 "audio/webm",
230 "audio/flac",
231 "audio/aac",
232 "audio/m4a",
233 "audio/x-m4a",
234 "audio/mp4",
235 "audio/3gpp",
236 "video/x-ms-asf",
237 }
238
239 m["image_size_limit"] = 10485760
240 m["image_matrix_limit"] = 16777216
241 m["video_size_limit"] = 41943040
242 m["video_frame_rate_limit"] = 60
243 m["video_matrix_limit"] = 2304000
244 j["media_attachments"] = m
245
246 goodjunk(rw, j)
247
248 return
249}
250
251// /// The ID of the account.
252// public let id: String
253// /// The username of the account.
254// public let username: String
255// /// Equals username for local users, includes @domain for remote ones.
256// public let acct: String
257// /// The account's display name.
258// public let displayName: String
259// /// Biography of user.
260// public let note: String
261// /// URL of the user's profile page (can be remote).
262// public let url: String
263// /// URL to the avatar image.
264// public let avatar: String
265// /// URL to the avatar static image
266// public let avatarStatic: String
267// /// URL to the header image.
268// public let header: String
269// /// URL to the header static image
270// public let headerStatic: String
271// /// Boolean for when the account cannot be followed without waiting for approval first.
272// public let locked: Bool
273// /// The time the account was created.
274// public let createdAt: String?
275// /// The number of followers for the account.
276// public var followersCount: Int
277// /// The number of accounts the given account is following.
278// public var followingCount: Int
279// /// The number of statuses the account has made.
280// public let statusesCount: Int
281// /// An array of Emoji.
282// public let emojis: [Emoji]
283//
284// public let fields: [HashType]
285//
286// /// A 3-5 word description of the account
287// /// Optional, as this is a Mammoth addition
288// public let summary: String?
289//
290// public let bot: Bool
291// public let lastStatusAt: String?
292// public let discoverable: Bool?
293//
294// public let source: AccountSource?
295
296// https://docs.joinmastodon.org/methods/accounts/#verify_credentials
297func verifycreds(rw http.ResponseWriter, r *http.Request) {
298 user, ok := somenumberedusers.Get(UserID(1))
299 if !ok {
300 elog.Fatalf("masto: no user number 1???")
301 }
302 j := junk.New()
303 j["id"] = fmt.Sprintf("%d", user.ID)
304 j["username"] = user.Name
305 j["created_at"] = ""
306 j["acct"] = user.Name
307 j["display_name"] = user.Options.CustomDisplay
308 j["url"] = user.URL
309 j["note"] = user.About
310 j["avatar"] = user.Options.Avatar
311 j["avatar_static"] = user.Options.Avatar
312 j["header"] = user.Options.Banner
313 j["header_static"] = user.Options.Banner
314 j["followers_count"] = 0
315 j["following_count"] = 0
316 j["statuses_count"] = getlocalhonkcount()
317 j["locked"] = false
318 j["bot"] = false
319 j["last_status_at"] = ""
320 j["discoverable"] = false
321 j["source"] = nil
322 j["emojis"] = []junk.Junk{}
323 j["fields"] = []junk.Junk{}
324
325 log.Println(j.ToString())
326
327 goodjunk(rw, j)
328}
329
330// https://docs.joinmastodon.org/methods/timelines/#home
331func hometimeline(rw http.ResponseWriter, r *http.Request) {
332 limit := r.URL.Query().Get("limit")
333 minid := r.URL.Query().Get("min_id")
334 maxid := r.URL.Query().Get("max_id")
335
336 dlog.Println("limit", limit, "minid", minid, "maxid", maxid)
337
338 var (
339 minidInt int
340 maxidInt int
341 limitInt int
342 err error
343 )
344
345 list := []junk.Junk{}
346 me, ok := somenumberedusers.Get(UserID(1))
347 if !ok {
348 elog.Fatalf("masto: no user number 1???")
349 }
350
351 if limit == "" {
352 limit = "5"
353 }
354 limitInt, _ = strconv.Atoi(limit)
355
356 if minid != "" {
357 minidInt, err = strconv.Atoi(minid)
358 if err != nil {
359 elog.Printf("masto: invalid min_id: %s: %s", minid, err)
360 http.Error(rw, "invalid min_id", http.StatusBadRequest)
361 return
362 }
363 }
364 if maxid != "" {
365 maxidInt, err = strconv.Atoi(maxid)
366 if err != nil {
367 elog.Printf("masto: invalid max_id: %s: %s", minid, err)
368 http.Error(rw, "invalid max_id", http.StatusBadRequest)
369 return
370 }
371 }
372
373 honks := getlimitedhonksforuser(me.ID, int64(minidInt), int64(maxidInt), limitInt)
374 reverbolate(me.ID, honks)
375 for _, h := range honks {
376 hj := honktomasto(h)
377 if hj == nil {
378 elog.Printf("masto: failed to convert honk %d to masto", h.ID)
379 http.Error(rw, "failed to convert honk", http.StatusInternalServerError)
380 }
381 list = append(list, hj)
382 }
383
384 buf := bytes.Buffer{}
385 err = listjunk(&buf, list)
386 if err != nil {
387 elog.Println(err)
388 http.Error(rw, "error encoding json", http.StatusInternalServerError)
389 }
390
391 rw.Header().Set("Content-Type", "application/json; charset=utf-8")
392 rw.WriteHeader(http.StatusOK)
393 rw.Write(buf.Bytes())
394}