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// /// The ID of the account.
250// public let id: String
251// /// The username of the account.
252// public let username: String
253// /// Equals username for local users, includes @domain for remote ones.
254// public let acct: String
255// /// The account's display name.
256// public let displayName: String
257// /// Biography of user.
258// public let note: String
259// /// URL of the user's profile page (can be remote).
260// public let url: String
261// /// URL to the avatar image.
262// public let avatar: String
263// /// URL to the avatar static image
264// public let avatarStatic: String
265// /// URL to the header image.
266// public let header: String
267// /// URL to the header static image
268// public let headerStatic: String
269// /// Boolean for when the account cannot be followed without waiting for approval first.
270// public let locked: Bool
271// /// The time the account was created.
272// public let createdAt: String?
273// /// The number of followers for the account.
274// public var followersCount: Int
275// /// The number of accounts the given account is following.
276// public var followingCount: Int
277// /// The number of statuses the account has made.
278// public let statusesCount: Int
279// /// An array of Emoji.
280// public let emojis: [Emoji]
281//
282// public let fields: [HashType]
283//
284// /// A 3-5 word description of the account
285// /// Optional, as this is a Mammoth addition
286// public let summary: String?
287//
288// public let bot: Bool
289// public let lastStatusAt: String?
290// public let discoverable: Bool?
291//
292// public let source: AccountSource?
293
294// https://docs.joinmastodon.org/methods/accounts/#verify_credentials
295func verifycreds(rw http.ResponseWriter, r *http.Request) {
296 user, ok := somenumberedusers.Get(UserID(1))
297 if !ok {
298 elog.Fatalf("masto: no user number 1???")
299 }
300 j := junk.New()
301 j["id"] = fmt.Sprintf("%d", user.ID)
302 j["username"] = user.Name
303 j["created_at"] = ""
304 j["acct"] = user.Name
305 j["display_name"] = user.Options.CustomDisplay
306 j["url"] = user.URL
307 j["note"] = user.About
308 j["avatar"] = user.Options.Avatar
309 j["avatar_static"] = user.Options.Avatar
310 j["header"] = user.Options.Banner
311 j["header_static"] = user.Options.Banner
312 j["followers_count"] = 0
313 j["following_count"] = 0
314 j["statuses_count"] = getlocalhonkcount()
315 j["locked"] = false
316 j["bot"] = false
317 j["last_status_at"] = ""
318 j["discoverable"] = false
319 j["source"] = nil
320 j["emojis"] = []junk.Junk{}
321 j["fields"] = []junk.Junk{}
322
323 log.Println(j.ToString())
324
325 goodjunk(rw, j)
326}